Fixing TTS frame order.
This commit is contained in:
@@ -43,6 +43,7 @@ from pipecat.frames.frames import (
|
||||
LLMFullResponseEndFrame,
|
||||
LLMFullResponseStartFrame,
|
||||
StartFrame,
|
||||
SystemFrame,
|
||||
TextFrame,
|
||||
TranscriptionFrame,
|
||||
TTSAudioRawFrame,
|
||||
@@ -557,9 +558,9 @@ class TTSService(AIService):
|
||||
"""
|
||||
await super().stop(frame)
|
||||
if self._audio_context_task:
|
||||
# Indicate no more audio contexts are available; this will end the
|
||||
# task cleanly after all contexts have been processed.
|
||||
await self._contexts_queue.put(None)
|
||||
# Sentinel None shuts down the serialization queue once all
|
||||
# pending contexts and frames have been processed.
|
||||
await self._serialization_queue.put(None)
|
||||
await self._audio_context_task
|
||||
self._audio_context_task = None
|
||||
if self._stop_frame_task:
|
||||
@@ -791,7 +792,15 @@ class TTSService(AIService):
|
||||
await self._maybe_resume_frame_processing()
|
||||
await self.push_frame(frame, direction)
|
||||
else:
|
||||
await self.push_frame(frame, direction)
|
||||
if direction == FrameDirection.DOWNSTREAM and not isinstance(frame, SystemFrame):
|
||||
# Route non-system downstream frames through the serialization queue so they
|
||||
# are emitted in the same order they arrive relative to any audio contexts that
|
||||
# are already queued (e.g. a FooFrame sent right after a TTSSpeakFrame must
|
||||
# not overtake the TTSStartedFrame / TTSAudioRawFrame / TTSStoppedFrame
|
||||
# sequence from that speak frame).
|
||||
await self._serialization_queue.put(frame)
|
||||
else:
|
||||
await self.push_frame(frame, direction)
|
||||
|
||||
async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM):
|
||||
"""Push a frame downstream with TTS-specific handling.
|
||||
@@ -994,7 +1003,10 @@ class TTSService(AIService):
|
||||
# is spoken, so we set append_to_context to False.
|
||||
src_frame.append_to_context = False
|
||||
src_frame.context_id = context_id
|
||||
await self.push_frame(src_frame)
|
||||
# Route AggregatedTextFrame through the serialization queue so it is emitted
|
||||
# immediately before the TTSStartedFrame of the audio context it describes,
|
||||
# rather than racing ahead of audio frames from a previous context.
|
||||
await self._serialization_queue.put(src_frame)
|
||||
|
||||
# Note: Text transformations are meant to only affect the text sent to the TTS for
|
||||
# TTS-specific purposes. This allows for explicit TTS modifications (e.g., inserting
|
||||
@@ -1203,7 +1215,7 @@ class TTSService(AIService):
|
||||
Args:
|
||||
context_id: Unique identifier for the audio context.
|
||||
"""
|
||||
await self._contexts_queue.put(context_id)
|
||||
await self._serialization_queue.put(context_id)
|
||||
self._audio_contexts[context_id] = asyncio.Queue()
|
||||
logger.trace(f"{self} created audio context {context_id}")
|
||||
|
||||
@@ -1295,7 +1307,14 @@ class TTSService(AIService):
|
||||
|
||||
def _create_audio_context_task(self):
|
||||
if not self._audio_context_task:
|
||||
self._contexts_queue: asyncio.Queue = asyncio.Queue()
|
||||
# Single FIFO queue that serializes everything the TTS service emits downstream.
|
||||
# Items can be:
|
||||
# str – an audio context ID: process the per-context audio queue in full before
|
||||
# moving on (see _handle_audio_context).
|
||||
# Frame – a non-system downstream frame (e.g. AggregatedTextFrame, FooFrame) that
|
||||
# must be emitted in-order relative to surrounding audio contexts.
|
||||
# None – shutdown sentinel (sent by stop()).
|
||||
self._serialization_queue: asyncio.Queue = asyncio.Queue()
|
||||
self._audio_contexts: Dict[str, asyncio.Queue] = {}
|
||||
self._audio_context_task = self.create_task(self._audio_context_task_handler())
|
||||
|
||||
@@ -1305,13 +1324,26 @@ class TTSService(AIService):
|
||||
self._audio_context_task = None
|
||||
|
||||
async def _audio_context_task_handler(self):
|
||||
"""In this task we process audio contexts in order."""
|
||||
"""Drain the serialization queue, preserving downstream frame order.
|
||||
|
||||
The queue carries three kinds of items (see _create_audio_context_task):
|
||||
|
||||
* str – audio context ID: block until all audio for that context has been
|
||||
pushed downstream, then call on_audio_context_completed().
|
||||
* Frame – a non-system downstream frame that must be emitted at this exact
|
||||
position in the output stream (e.g. AggregatedTextFrame preceding
|
||||
its audio, or an arbitrary frame that arrived between two speak frames).
|
||||
* None – shutdown sentinel; exit the loop once reached.
|
||||
"""
|
||||
running = True
|
||||
while running:
|
||||
context_id = await self._contexts_queue.get()
|
||||
self._playing_context_id = context_id
|
||||
context_value = await self._serialization_queue.get()
|
||||
if isinstance(context_value, Frame):
|
||||
await self.push_frame(context_value)
|
||||
elif isinstance(context_value, str):
|
||||
context_id = context_value
|
||||
self._playing_context_id = context_id
|
||||
|
||||
if context_id:
|
||||
# Process the audio context until the context doesn't have more
|
||||
# audio available (i.e. we find None).
|
||||
await self._handle_audio_context(context_id)
|
||||
@@ -1323,7 +1355,7 @@ class TTSService(AIService):
|
||||
else:
|
||||
running = False
|
||||
|
||||
self._contexts_queue.task_done()
|
||||
self._serialization_queue.task_done()
|
||||
|
||||
async def _handle_audio_context(self, context_id: str):
|
||||
"""Process items from an audio context queue until it is exhausted."""
|
||||
|
||||
Reference in New Issue
Block a user