Merge pull request #3845 from pipecat-ai/filipi/fix_tts_speak_frame
Add TTSSpeakFrame.push_assistant_aggregation to force context flush after TTS.
This commit is contained in:
1
changelog/3845.fixed.md
Normal file
1
changelog/3845.fixed.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
- Fixed `TTSSpeakFrame` not committing spoken text to the conversation context when used outside of an LLM response (e.g., bot greetings or injected speech).
|
||||||
@@ -11,6 +11,7 @@ from dotenv import load_dotenv
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
|
from pipecat.frames.frames import TTSSpeakFrame
|
||||||
from pipecat.pipeline.pipeline import Pipeline
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
from pipecat.pipeline.runner import PipelineRunner
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
@@ -110,6 +111,14 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
|||||||
@transport.event_handler("on_client_connected")
|
@transport.event_handler("on_client_connected")
|
||||||
async def on_client_connected(transport, client):
|
async def on_client_connected(transport, client):
|
||||||
logger.info(f"Client connected")
|
logger.info(f"Client connected")
|
||||||
|
await task.queue_frames(
|
||||||
|
[
|
||||||
|
TTSSpeakFrame(
|
||||||
|
text="Hello, welcome to live translation. Everything you say will be automatically translated to Spanish. Let's begin!",
|
||||||
|
append_to_context=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@transport.event_handler("on_client_disconnected")
|
@transport.event_handler("on_client_disconnected")
|
||||||
async def on_client_disconnected(transport, client):
|
async def on_client_disconnected(transport, client):
|
||||||
|
|||||||
@@ -1990,6 +1990,16 @@ class LLMFullResponseEndFrame(ControlFrame):
|
|||||||
self.skip_tts = None
|
self.skip_tts = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LLMAssistantPushAggregationFrame(ControlFrame):
|
||||||
|
"""Frame that forces the LLM assistant aggregator to push its current aggregation to context.
|
||||||
|
|
||||||
|
When received by ``LLMAssistantAggregator``, any text that has been accumulated
|
||||||
|
in the aggregation buffer is immediately committed to the conversation context as
|
||||||
|
an assistant message, without waiting for an ``LLMFullResponseEndFrame``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMContextSummaryRequestFrame(ControlFrame):
|
class LLMContextSummaryRequestFrame(ControlFrame):
|
||||||
"""Frame requesting context summarization from an LLM service.
|
"""Frame requesting context summarization from an LLM service.
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from pipecat.frames.frames import (
|
|||||||
InputAudioRawFrame,
|
InputAudioRawFrame,
|
||||||
InterimTranscriptionFrame,
|
InterimTranscriptionFrame,
|
||||||
InterruptionFrame,
|
InterruptionFrame,
|
||||||
|
LLMAssistantPushAggregationFrame,
|
||||||
LLMContextAssistantTimestampFrame,
|
LLMContextAssistantTimestampFrame,
|
||||||
LLMContextFrame,
|
LLMContextFrame,
|
||||||
LLMContextSummaryRequestFrame,
|
LLMContextSummaryRequestFrame,
|
||||||
@@ -879,6 +880,8 @@ class LLMAssistantAggregator(LLMContextAggregator):
|
|||||||
elif isinstance(frame, (EndFrame, CancelFrame)):
|
elif isinstance(frame, (EndFrame, CancelFrame)):
|
||||||
await self._handle_end_or_cancel(frame)
|
await self._handle_end_or_cancel(frame)
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
|
elif isinstance(frame, LLMAssistantPushAggregationFrame):
|
||||||
|
await self.push_aggregation()
|
||||||
elif isinstance(frame, LLMFullResponseStartFrame):
|
elif isinstance(frame, LLMFullResponseStartFrame):
|
||||||
await self._handle_llm_start(frame)
|
await self._handle_llm_start(frame)
|
||||||
elif isinstance(frame, LLMFullResponseEndFrame):
|
elif isinstance(frame, LLMFullResponseEndFrame):
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ from pipecat.frames.frames import (
|
|||||||
Frame,
|
Frame,
|
||||||
InterimTranscriptionFrame,
|
InterimTranscriptionFrame,
|
||||||
InterruptionFrame,
|
InterruptionFrame,
|
||||||
|
LLMAssistantPushAggregationFrame,
|
||||||
LLMFullResponseEndFrame,
|
LLMFullResponseEndFrame,
|
||||||
LLMFullResponseStartFrame,
|
LLMFullResponseStartFrame,
|
||||||
StartFrame,
|
StartFrame,
|
||||||
@@ -67,10 +68,16 @@ class TTSContext:
|
|||||||
"""Context information for a TTS request.
|
"""Context information for a TTS request.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
append_to_context: Whether this TTS output should be appended to the conversation context.
|
append_to_context: Whether this TTS output should be appended to the
|
||||||
|
conversation context after it is spoken.
|
||||||
|
push_assistant_aggregation: Whether to push an
|
||||||
|
``LLMAssistantPushAggregationFrame`` after the TTS has finished
|
||||||
|
speaking, forcing the assistant aggregator to commit its current
|
||||||
|
text buffer to the conversation context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
append_to_context: bool = True
|
append_to_context: bool = True
|
||||||
|
push_assistant_aggregation: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
class TextAggregationMode(str, Enum):
|
class TextAggregationMode(str, Enum):
|
||||||
@@ -641,10 +648,13 @@ class TTSService(AIService):
|
|||||||
elif isinstance(frame, TTSSpeakFrame):
|
elif isinstance(frame, TTSSpeakFrame):
|
||||||
# Store if we were processing text or not so we can set it back.
|
# Store if we were processing text or not so we can set it back.
|
||||||
processing_text = self._processing_text
|
processing_text = self._processing_text
|
||||||
|
# If we are not receiving text from the LLM, we can assume that the SpeakFrame should be automatically added to the context
|
||||||
|
push_assistant_aggregation = frame.append_to_context and not self._llm_response_started
|
||||||
# Assumption: text in TTSSpeakFrame does not include inter-frame spaces
|
# Assumption: text in TTSSpeakFrame does not include inter-frame spaces
|
||||||
await self._push_tts_frames(
|
await self._push_tts_frames(
|
||||||
AggregatedTextFrame(frame.text, AggregationType.SENTENCE),
|
AggregatedTextFrame(frame.text, AggregationType.SENTENCE),
|
||||||
append_tts_text_to_context=frame.append_to_context,
|
append_tts_text_to_context=frame.append_to_context,
|
||||||
|
push_assistant_aggregation=push_assistant_aggregation,
|
||||||
)
|
)
|
||||||
# We pause processing incoming frames because we are sending data to
|
# We pause processing incoming frames because we are sending data to
|
||||||
# the TTS. We pause to avoid audio overlapping.
|
# the TTS. We pause to avoid audio overlapping.
|
||||||
@@ -809,6 +819,7 @@ class TTSService(AIService):
|
|||||||
src_frame: AggregatedTextFrame,
|
src_frame: AggregatedTextFrame,
|
||||||
includes_inter_frame_spaces: Optional[bool] = False,
|
includes_inter_frame_spaces: Optional[bool] = False,
|
||||||
append_tts_text_to_context: Optional[bool] = True,
|
append_tts_text_to_context: Optional[bool] = True,
|
||||||
|
push_assistant_aggregation: Optional[bool] = False,
|
||||||
):
|
):
|
||||||
type = src_frame.aggregated_by
|
type = src_frame.aggregated_by
|
||||||
text = src_frame.text
|
text = src_frame.text
|
||||||
@@ -876,7 +887,8 @@ class TTSService(AIService):
|
|||||||
self._tts_contexts[context_id] = TTSContext(
|
self._tts_contexts[context_id] = TTSContext(
|
||||||
append_to_context=append_tts_text_to_context
|
append_to_context=append_tts_text_to_context
|
||||||
if append_tts_text_to_context is not None
|
if append_tts_text_to_context is not None
|
||||||
else True
|
else True,
|
||||||
|
push_assistant_aggregation=push_assistant_aggregation,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apply any final text preparation (e.g., trailing space)
|
# Apply any final text preparation (e.g., trailing space)
|
||||||
@@ -905,6 +917,8 @@ class TTSService(AIService):
|
|||||||
if append_tts_text_to_context is not None:
|
if append_tts_text_to_context is not None:
|
||||||
frame.append_to_context = append_tts_text_to_context
|
frame.append_to_context = append_tts_text_to_context
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
if push_assistant_aggregation:
|
||||||
|
await self.push_frame(LLMAssistantPushAggregationFrame())
|
||||||
|
|
||||||
async def _stop_frame_handler(self):
|
async def _stop_frame_handler(self):
|
||||||
has_started = False
|
has_started = False
|
||||||
@@ -988,6 +1002,9 @@ class TTSService(AIService):
|
|||||||
frame = TTSStoppedFrame()
|
frame = TTSStoppedFrame()
|
||||||
frame.pts = last_pts
|
frame.pts = last_pts
|
||||||
frame.context_id = context_id
|
frame.context_id = context_id
|
||||||
|
if context_id in self._tts_contexts:
|
||||||
|
if self._tts_contexts[context_id].push_assistant_aggregation:
|
||||||
|
await self.push_frame(LLMAssistantPushAggregationFrame())
|
||||||
else:
|
else:
|
||||||
# Assumption: word-by-word text frames don't include spaces, so
|
# Assumption: word-by-word text frames don't include spaces, so
|
||||||
# we can rely on the default includes_inter_frame_spaces=False
|
# we can rely on the default includes_inter_frame_spaces=False
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ async def test_run_piper_tts_error(aiohttp_client):
|
|||||||
)
|
)
|
||||||
|
|
||||||
frames_to_send = [
|
frames_to_send = [
|
||||||
TTSSpeakFrame(text="Error case."),
|
TTSSpeakFrame(text="Error case.", append_to_context=False),
|
||||||
]
|
]
|
||||||
|
|
||||||
expected_down_frames = [AggregatedTextFrame, TTSStoppedFrame, TTSTextFrame]
|
expected_down_frames = [AggregatedTextFrame, TTSStoppedFrame, TTSTextFrame]
|
||||||
|
|||||||
Reference in New Issue
Block a user