millions-gold-86808
03/07/2026, 7:35 AMtall-airline-16756
03/07/2026, 7:35 AMtall-airline-16756
03/07/2026, 7:36 AMsession.say() with an audio generator that yields rtc.AudioFrame).
• WebM is supported only when the audio inside the WebM is Opus-encoded (Opus is the codec WebRTC/LiveKit expects). Converting to WAV can simplify decoding and playback.
How to implement (high level)
1. Store the file (local disk or cloud). If remote, download it on the server before playback.
2. Decode the audio file into raw frames and create an rtc.AudioFrame (or equivalent structure used by your SDK). You can decode WebM/Opus with ffmpeg/PyAV, or load WAV directly with wave for ease.
3. Use session.say() and pass an async generator that yields the rtc.AudioFrame. Set text=None so only the audio plays.
4. After playback, let the agent continue (call generate_reply() or otherwise start the agent’s normal speech-to-text/response flow).
Practical notes and code-patterns
• Playing audio only (no TTS):
◦ Use session.say(text=None, audio=audio_generator()), where audio_generator() yields rtc.AudioFrame frames.
• WebM support:
◦ WebM is supported if the contained audio is Opus. If your WebM uses Opus, decode it server-side to produce frames for rtc.AudioFrame. If you prefer simpler handling, convert to WAV and load the WAV frames directly.
• Recommended approach:
◦ For reliability and simpler decoding on the backend, convert files to WAV (if feasible). WAV is straightforward to load and convert into audio frames.
• Example patterns (pseudo/Python):
a. Decode file into frames (WAV example shown conceptually):
with wave.open(str(audio_path), 'rb') as wav_file:
frames = wav_file.readframes(wav_file.getnframes())
sample_rate = wav_file.getframerate()
num_channels = wav_file.getnchannels()
audio_frame = rtc.AudioFrame(
data=frames,
sample_rate=sample_rate,
num_channels=num_channels,
samples_per_channel=wav_file.getnframes()
)
1. Yield frames in an async generator and call `session.say()`:
async def audio_generator():
yield audio_frame
await session.say(text=None, audio=audio_generator())
1. After playback, start the agent conversation:
await session.generate_reply()
Questions to narrow your setup
• Which SDK and language are you using (e.g., Python, Node, JS)? I can give a concrete code example for that environment.
• Is your pre-recorded file currently WebM/Opus, WAV, or MP3? If WebM, do you know whether it’s Opus-encoded?
• Where do you store the audio (local server, S3, other)? That affects whether you need to download/stream and how you decode.
If you tell me your SDK/language and file format/storage, I’ll provide a short, copy-paste example that fits your stack and sample code to decode WebM (Opus) or play WAV.
Sources: Playing Audio | LiveKit Documentation | Universal Egress | Agent speech and audio | LiveKit Documentationmillions-gold-86808
03/08/2026, 8:35 AMasync def _duration_guard() -> None:
await asyncio.sleep(CALL_DURATION)
<http://logger.info|logger.info>("Duration limit reached — call_id=%s, saying goodbye", call_id)
try:
await asyncio.wait_for(
session.say(
"Thank you so much for your time. Goodbye!",
allow_interruptions=False,
),
timeout=10.0,
)
except asyncio.TimeoutError:
logger.warning("Goodbye timed out — call_id=%s, forcing hangup", call_id)
except Exception as exc:
logger.warning("Goodbye error — call_id=%s: %s", call_id, exc)
await _hangup(ctx)
await session.aclose()
asyncio.create_task(_duration_guard())
this is a function i wrote now this successflly cuts the call butt the session.say does not execute, i can see the logs it says duration limit reacehd but the agent continues, i am using session.generate reply for the agent to start speaking.tall-airline-16756
03/08/2026, 8:35 AMsession.generate_reply() usage) is likely conflicting with session.say(). I’ll walk through why that happens and give a few targeted changes you can try.
Quick summary of the issue I’m responding to
• You call session.say() inside _duration_guard(), see the log, but no audio plays and the agent keeps talking. You also use session.generate_reply() elsewhere to start the agent speaking.
Why this is happening
• Only one speech operation can be active at a time. If generate_reply() is in progress (or the agent is still considered “speaking” or has queued output), a subsequent session.say() can be ignored/overridden.
• If the session is closed or session.aclose() runs before playback finishes, audio won’t be heard.
• Using allow_interruptions=False prevents interruptions of the new speech, but it doesn’t forcibly stop any speech already in progress unless you explicitly interrupt/cancel it first.
• Awaiting session.say() may not guarantee playout completed — you should wait for a playout/completion handle if available.
Concrete changes to try 1) Interrupt any active agent speech before calling `say()`:
session.interrupt() # stop current speech and clear queue
handle = session.say("Thank you so much for your time. Goodbye!", allow_interruptions=False)
await handle.wait_for_playout() # ensure audio finishes
2) If interrupt() is not available in your SDK version, explicitly cancel the task that runs generate_reply() (or await its completion) before calling `say()`:
• Ensure no generate_reply() call is active or queued when your duration guard runs.
3) Make sure you do NOT call session.aclose() until after the goodbye playout completes:
• In your code you call session.aclose() immediately after _hangup(ctx). Move that after you wait for playout.
4) Use explicit timeouts only to avoid hanging, but ensure you allow enough time for playout:
try:
handle = session.say("Thank you ...", allow_interruptions=False)
await asyncio.wait_for(handle.wait_for_playout(), timeout=10.0)
except asyncio.TimeoutError:
logger.warning("Goodbye timed out — call_id=%s, forcing hangup", call_id)
Additional checks
• Confirm the session is still connected when _duration_guard() runs.
• Confirm the SDK you’re on exposes a SpeechHandle or similar interface; if not, confirm how session.say() reports completion in your version.
• If generate_reply() is invoked in a loop or via events, verify those loops stop when you interrupt or when the guard runs.
If you want, share:
• How you call session.generate_reply() (a short snippet of that flow).
• Which SDK and version (Python / Node / other).
• Whether session.interrupt() or SpeechHandle.wait_for_playout() are available in your SDK.
Tell me your SDK/version and I’ll provide a concrete, copy‑paste snippet that fits your stack and ensures the goodbye plays before hangup.
Sources: examples/voice_agents | livekit/livekit_composite | DeepWiki | How to detect when an agent has finished speaking | LiveKit | Agent speech and audio | LiveKit Documentation | Turns overview | LiveKit Documentationmillions-gold-86808
03/28/2026, 2:44 PMtall-airline-16756
03/28/2026, 2:44 PM