From 097f9c089681738b67f1671ed828a1854ffde5e6 Mon Sep 17 00:00:00 2001 From: filipi87 Date: Mon, 9 Mar 2026 11:04:09 -0300 Subject: [PATCH 1/5] Fixed to push LLMAssistantPushAggregationFrame when the base TTSService class is responsible for pushing the TTSStoppedFrame. --- changelog/3936.fixed.md | 1 + src/pipecat/services/tts_service.py | 15 +++++---------- 2 files changed, 6 insertions(+), 10 deletions(-) create mode 100644 changelog/3936.fixed.md diff --git a/changelog/3936.fixed.md b/changelog/3936.fixed.md new file mode 100644 index 000000000..2d186ffd7 --- /dev/null +++ b/changelog/3936.fixed.md @@ -0,0 +1 @@ +- Fixed `TTSService` to push `LLMAssistantPushAggregationFrame` when the base class is responsible for pushing `TTSStoppedFrame`. diff --git a/src/pipecat/services/tts_service.py b/src/pipecat/services/tts_service.py index db695fd52..731b71581 100644 --- a/src/pipecat/services/tts_service.py +++ b/src/pipecat/services/tts_service.py @@ -768,6 +768,8 @@ class TTSService(AIService): # Clean up context when we see TTSStoppedFrame if isinstance(frame, TTSStoppedFrame) and frame.context_id: if frame.context_id in self._tts_contexts: + if self._tts_contexts[frame.context_id].push_assistant_aggregation: + await self.push_frame(LLMAssistantPushAggregationFrame()) logger.debug(f"{self} cleaning up TTS context {frame.context_id}") del self._tts_contexts[frame.context_id] @@ -1009,14 +1011,8 @@ class TTSService(AIService): # For services using the audio context we are appending to the context, so it preserves the ordering. if self.audio_context_available(context_id): await self.append_to_audio_context(context_id, frame) - if push_assistant_aggregation: - await self.append_to_audio_context( - context_id, LLMAssistantPushAggregationFrame() - ) else: await self.push_frame(frame) - if push_assistant_aggregation: - await self.push_frame(LLMAssistantPushAggregationFrame()) async def tts_process_generator( self, context_id: str, generator: AsyncGenerator[Frame | None, None] @@ -1047,18 +1043,20 @@ class TTSService(AIService): async def _stop_frame_handler(self): has_started = False + context_id = None while True: try: frame = await asyncio.wait_for( self._stop_frame_queue.get(), timeout=self._stop_frame_timeout_s ) + context_id = frame.context_id if isinstance(frame, TTSStartedFrame): has_started = True elif isinstance(frame, (TTSStoppedFrame, InterruptionFrame)): has_started = False except asyncio.TimeoutError: if has_started: - await self.push_frame(TTSStoppedFrame()) + await self.push_frame(TTSStoppedFrame(context_id=context_id)) has_started = False # @@ -1142,9 +1140,6 @@ class TTSService(AIService): frame.pts = self._word_last_pts frame.context_id = context_id await self.push_frame(frame) - if context_id in self._tts_contexts: - if self._tts_contexts[context_id].push_assistant_aggregation: - await self.push_frame(LLMAssistantPushAggregationFrame()) else: ts_ns = seconds_to_nanoseconds(timestamp) if self._initial_word_timestamp == -1: From c5ce667387b110f02bb856d9e5e492ef3e66bb53 Mon Sep 17 00:00:00 2001 From: filipi87 Date: Mon, 9 Mar 2026 11:10:42 -0300 Subject: [PATCH 2/5] Retrieving the context_id from the TTSStartedFrame --- src/pipecat/services/tts_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipecat/services/tts_service.py b/src/pipecat/services/tts_service.py index 731b71581..25a4c2735 100644 --- a/src/pipecat/services/tts_service.py +++ b/src/pipecat/services/tts_service.py @@ -1049,8 +1049,8 @@ class TTSService(AIService): frame = await asyncio.wait_for( self._stop_frame_queue.get(), timeout=self._stop_frame_timeout_s ) - context_id = frame.context_id if isinstance(frame, TTSStartedFrame): + context_id = frame.context_id has_started = True elif isinstance(frame, (TTSStoppedFrame, InterruptionFrame)): has_started = False From ba87d1609cce06ba3b3ced10c16feebf76d2b241 Mon Sep 17 00:00:00 2001 From: filipi87 Date: Mon, 9 Mar 2026 11:24:36 -0300 Subject: [PATCH 3/5] Only marking self._is_yielding_frames_synchronously if receiving TTSAudioRawFrame --- src/pipecat/services/tts_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pipecat/services/tts_service.py b/src/pipecat/services/tts_service.py index 25a4c2735..6a04062e5 100644 --- a/src/pipecat/services/tts_service.py +++ b/src/pipecat/services/tts_service.py @@ -1038,7 +1038,9 @@ class TTSService(AIService): async for frame in generator: if frame: await self.append_to_audio_context(context_id, frame) - is_yielding_frames = True + if isinstance(frame, TTSAudioRawFrame): + is_yielding_frames = True + self._is_yielding_frames_synchronously = is_yielding_frames async def _stop_frame_handler(self): From 8ec160f71e36a9f25a65db8528950d41f7a2f9ed Mon Sep 17 00:00:00 2001 From: filipi87 Date: Mon, 9 Mar 2026 11:37:11 -0300 Subject: [PATCH 4/5] Making the changelog more user friendly. --- changelog/3936.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/3936.fixed.md b/changelog/3936.fixed.md index 2d186ffd7..27e48815c 100644 --- a/changelog/3936.fixed.md +++ b/changelog/3936.fixed.md @@ -1 +1 @@ -- Fixed `TTSService` to push `LLMAssistantPushAggregationFrame` when the base class is responsible for pushing `TTSStoppedFrame`. +- Fixed TTS context not being appended to the assistant message history when using `TTSSpeakFrame` with `append_to_context=True` with some TTS providers. From f0c5925a79bcdfead695fad126a22c956edd5a06 Mon Sep 17 00:00:00 2001 From: filipi87 Date: Mon, 9 Mar 2026 12:07:45 -0300 Subject: [PATCH 5/5] Fixing Piper test. --- src/pipecat/services/piper/tts.py | 2 ++ tests/test_piper_tts.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pipecat/services/piper/tts.py b/src/pipecat/services/piper/tts.py index fb7b627cd..53990ff2e 100644 --- a/src/pipecat/services/piper/tts.py +++ b/src/pipecat/services/piper/tts.py @@ -17,6 +17,7 @@ from loguru import logger from pipecat.frames.frames import ( ErrorFrame, Frame, + TTSStoppedFrame, ) from pipecat.services.settings import TTSSettings, _warn_deprecated_param from pipecat.services.tts_service import TTSService @@ -287,6 +288,7 @@ class PiperHttpTTSService(TTSService): yield ErrorFrame( error=f"Error getting audio (status: {response.status}, error: {error})" ) + yield TTSStoppedFrame(context_id=context_id) return await self.start_tts_usage_metrics(text) diff --git a/tests/test_piper_tts.py b/tests/test_piper_tts.py index 2b1dae577..3b822f07c 100644 --- a/tests/test_piper_tts.py +++ b/tests/test_piper_tts.py @@ -136,7 +136,7 @@ async def test_run_piper_tts_error(aiohttp_client): TTSSpeakFrame(text="Error case.", append_to_context=False), ] - expected_down_frames = [AggregatedTextFrame, TTSStartedFrame, TTSTextFrame, TTSStoppedFrame] + expected_down_frames = [AggregatedTextFrame, TTSStartedFrame, TTSStoppedFrame, TTSTextFrame] expected_up_frames = [ErrorFrame]