From b76831e67797672f30abe84dd88e2e01cdcced61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Thu, 14 May 2026 13:22:00 -0700 Subject: [PATCH 1/3] Fall back to _turn_context_id in get_active_audio_context_id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TTS services whose wire protocol does not echo the context_id back on incoming audio (Sarvam, Smallest, Soniox, Inworld, ...) call ``get_active_audio_context_id()`` to tag each chunk. That accessor returned only ``_playing_context_id`` — the playback-side cursor set asynchronously by ``_audio_context_task_handler`` when it pops a context off the serialization queue. Result: incoming audio that arrived in the gap between contexts or at the very start of a turn (before the playback loop popped) had ``context_id=None`` and was dropped with ``unable to append audio to context: no context ID provided``. Fall back to ``_turn_context_id`` (the synthesis-side cursor, set as soon as the turn's context is created) so the gap is covered without prematurely nulling the playback cursor. --- src/pipecat/services/tts_service.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pipecat/services/tts_service.py b/src/pipecat/services/tts_service.py index 65718fee6..61eddb20c 100644 --- a/src/pipecat/services/tts_service.py +++ b/src/pipecat/services/tts_service.py @@ -1283,10 +1283,17 @@ class TTSService(AIService): def get_active_audio_context_id(self) -> str | None: """Get the active audio context ID. + Returns the playback cursor when set (during active playback), falling + back to the current turn's synthesis context_id. The fallback covers + the gap between contexts and the start of a turn before the playback + task has popped the just-created context off the serialization queue — + important for services whose wire protocol does not echo context_id + back on incoming audio. + Returns: - The active context ID, or None if no context is active. + The active context ID, or None if neither cursor is set. """ - return self._playing_context_id + return self._playing_context_id or self._turn_context_id async def remove_active_audio_context(self): """Remove the active audio context.""" From 22650b1b568e25e68a26c3d089fbc4ca3a662594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Thu, 14 May 2026 13:22:07 -0700 Subject: [PATCH 2/3] Move QwenLLMService model into Settings in the qwen example Mirrors the deprecation in ``QwenLLMService.__init__``: ``model`` should be passed via ``settings=QwenLLMService.Settings(model=...)`` instead of as a direct constructor arg. --- examples/function-calling/function-calling-qwen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/function-calling/function-calling-qwen.py b/examples/function-calling/function-calling-qwen.py index 0bbb7311d..78aa48adf 100644 --- a/examples/function-calling/function-calling-qwen.py +++ b/examples/function-calling/function-calling-qwen.py @@ -71,8 +71,8 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = QwenLLMService( api_key=os.environ["QWEN_API_KEY"], - model="qwen2.5-72b-instruct", settings=QwenLLMService.Settings( + model="qwen2.5-72b-instruct", system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) From ea97cb1a78a623c56bc04bd5c0e17db19415ea7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Thu, 14 May 2026 13:22:50 -0700 Subject: [PATCH 3/3] Add changelog for #4497 --- changelog/4497.fixed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/4497.fixed.md diff --git a/changelog/4497.fixed.md b/changelog/4497.fixed.md new file mode 100644 index 000000000..b9ba8b676 --- /dev/null +++ b/changelog/4497.fixed.md @@ -0,0 +1 @@ +- Fixed dropped audio in streaming TTS services whose wire protocol doesn't echo `context_id` back on incoming audio (Sarvam, Smallest, Soniox, Inworld, and others). Previously, audio that arrived between contexts or at the very start of a turn was tagged with `context_id=None` and silently dropped with an "unable to append audio to context: no context ID provided" debug log. `TTSService.get_active_audio_context_id()` now falls back to the synthesis-side `_turn_context_id` when the playback cursor isn't set yet.