Merge pull request #583 from pipecat-ai/aleix/add-pts-to-llm-full-response-end-frame

add pts to llm full response end frame
This commit is contained in:
Aleix Conchillo Flaqué
2024-10-15 10:39:50 -07:00
committed by GitHub
6 changed files with 24 additions and 78 deletions

View File

@@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Merge `RTVIBotLLMProcessor`/`RTVIBotLLMTextProcessor` and
`RTVIBotTTSProcessor`/`RTVIBotTTSTextProcessor` to avoid out of order issues.
- Fixed an issue in Daily transport that would cause tasks to be hanging if - Fixed an issue in Daily transport that would cause tasks to be hanging if
urgent transport messages were being sent from a transport event handler. urgent transport messages were being sent from a transport event handler.

View File

@@ -5,7 +5,6 @@
# #
import asyncio import asyncio
import base64
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Awaitable, Callable, Dict, List, Literal, Optional, Union from typing import Any, Awaitable, Callable, Dict, List, Literal, Optional, Union
@@ -25,7 +24,6 @@ from pipecat.frames.frames import (
InterimTranscriptionFrame, InterimTranscriptionFrame,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
OutputAudioRawFrame,
StartFrame, StartFrame,
SystemFrame, SystemFrame,
TextFrame, TextFrame,
@@ -484,6 +482,9 @@ class RTVIBotLLMProcessor(RTVIFrameProcessor):
await self._push_transport_message_urgent(RTVIBotLLMStartedMessage()) await self._push_transport_message_urgent(RTVIBotLLMStartedMessage())
elif isinstance(frame, LLMFullResponseEndFrame): elif isinstance(frame, LLMFullResponseEndFrame):
await self._push_transport_message_urgent(RTVIBotLLMStoppedMessage()) await self._push_transport_message_urgent(RTVIBotLLMStoppedMessage())
elif type(frame) is TextFrame:
message = RTVIBotLLMTextMessage(data=RTVITextMessageData(text=frame.text))
await self._push_transport_message_urgent(message)
class RTVIBotTTSProcessor(RTVIFrameProcessor): class RTVIBotTTSProcessor(RTVIFrameProcessor):
@@ -499,62 +500,9 @@ class RTVIBotTTSProcessor(RTVIFrameProcessor):
await self._push_transport_message_urgent(RTVIBotTTSStartedMessage()) await self._push_transport_message_urgent(RTVIBotTTSStartedMessage())
elif isinstance(frame, TTSStoppedFrame): elif isinstance(frame, TTSStoppedFrame):
await self._push_transport_message_urgent(RTVIBotTTSStoppedMessage()) await self._push_transport_message_urgent(RTVIBotTTSStoppedMessage())
elif type(frame) is TextFrame:
message = RTVIBotTTSTextMessage(data=RTVITextMessageData(text=frame.text))
class RTVIBotLLMTextProcessor(RTVIFrameProcessor): await self._push_transport_message_urgent(message)
def __init__(self, **kwargs):
super().__init__(**kwargs)
async def process_frame(self, frame: Frame, direction: FrameDirection):
await super().process_frame(frame, direction)
await self.push_frame(frame, direction)
if type(frame) is TextFrame:
await self._handle_text(frame)
async def _handle_text(self, frame: TextFrame):
message = RTVIBotLLMTextMessage(data=RTVITextMessageData(text=frame.text))
await self._push_transport_message_urgent(message)
class RTVIBotTTSTextProcessor(RTVIFrameProcessor):
def __init__(self, **kwargs):
super().__init__(**kwargs)
async def process_frame(self, frame: Frame, direction: FrameDirection):
await super().process_frame(frame, direction)
await self.push_frame(frame, direction)
if type(frame) is TextFrame:
await self._handle_text(frame)
async def _handle_text(self, frame: TextFrame):
message = RTVIBotTTSTextMessage(data=RTVITextMessageData(text=frame.text))
await self._push_transport_message_urgent(message)
class RTVIBotTTSAudioProcessor(RTVIFrameProcessor):
def __init__(self, **kwargs):
super().__init__(**kwargs)
async def process_frame(self, frame: Frame, direction: FrameDirection):
await super().process_frame(frame, direction)
await self.push_frame(frame, direction)
if isinstance(frame, OutputAudioRawFrame):
await self._handle_audio(frame)
async def _handle_audio(self, frame: OutputAudioRawFrame):
encoded = base64.b64encode(frame.audio).decode("utf-8")
message = RTVIBotTTSAudioMessage(
data=RTVIAudioMessageData(
audio=encoded, sample_rate=frame.sample_rate, num_channels=frame.num_channels
)
)
await self._push_transport_message_urgent(message)
class RTVIProcessor(FrameProcessor): class RTVIProcessor(FrameProcessor):

View File

@@ -427,15 +427,21 @@ class WordTTSService(TTSService):
self._words_task = None self._words_task = None
async def _words_task_handler(self): async def _words_task_handler(self):
last_pts = 0
while True: while True:
try: try:
(word, timestamp) = await self._words_queue.get() (word, timestamp) = await self._words_queue.get()
if word == "LLMFullResponseEndFrame" and timestamp == 0: if word == "LLMFullResponseEndFrame" and timestamp == 0:
await self.push_frame(LLMFullResponseEndFrame()) frame = LLMFullResponseEndFrame()
frame.pts = last_pts
elif word == "TTSStoppedFrame" and timestamp == 0:
frame = TTSStoppedFrame()
frame.pts = last_pts
else: else:
frame = TextFrame(word) frame = TextFrame(word)
frame.pts = self._initial_word_timestamp + timestamp frame.pts = self._initial_word_timestamp + timestamp
await self.push_frame(frame) last_pts = frame.pts
await self.push_frame(frame)
self._words_queue.task_done() self._words_queue.task_done()
except asyncio.CancelledError: except asyncio.CancelledError:
break break

View File

@@ -210,7 +210,6 @@ class CartesiaTTSService(WordTTSService):
async def _handle_interruption(self, frame: StartInterruptionFrame, direction: FrameDirection): async def _handle_interruption(self, frame: StartInterruptionFrame, direction: FrameDirection):
await super()._handle_interruption(frame, direction) await super()._handle_interruption(frame, direction)
await self.stop_all_metrics() await self.stop_all_metrics()
await self.push_frame(LLMFullResponseEndFrame())
self._context_id = None self._context_id = None
async def flush_audio(self): async def flush_audio(self):
@@ -228,12 +227,13 @@ class CartesiaTTSService(WordTTSService):
continue continue
if msg["type"] == "done": if msg["type"] == "done":
await self.stop_ttfb_metrics() await self.stop_ttfb_metrics()
await self.push_frame(TTSStoppedFrame())
# Unset _context_id but not the _context_id_start_timestamp # Unset _context_id but not the _context_id_start_timestamp
# because we are likely still playing out audio and need the # because we are likely still playing out audio and need the
# timestamp to set send context frames. # timestamp to set send context frames.
self._context_id = None self._context_id = None
await self.add_word_timestamps([("LLMFullResponseEndFrame", 0)]) await self.add_word_timestamps(
[("TTSStoppedFrame", 0), ("LLMFullResponseEndFrame", 0)]
)
elif msg["type"] == "timestamps": elif msg["type"] == "timestamps":
await self.add_word_timestamps( await self.add_word_timestamps(
list(zip(msg["word_timestamps"]["words"], msg["word_timestamps"]["start"])) list(zip(msg["word_timestamps"]["words"], msg["word_timestamps"]["start"]))

View File

@@ -364,7 +364,6 @@ class OpenAILLMServiceRealtimeBeta(LLMService):
) )
) )
# #
# frame processing # frame processing
# #
@@ -643,8 +642,8 @@ class OpenAILLMServiceRealtimeBeta(LLMService):
await self._truncate_current_audio_response() await self._truncate_current_audio_response()
# todo: might need to guard sending these when we fully support using either openai # todo: might need to guard sending these when we fully support using either openai
# turn detection of Pipecat turn detection # turn detection of Pipecat turn detection
await self._start_interruption() # cancels this processor task await self._start_interruption() # cancels this processor task
await self.push_frame(StartInterruptionFrame()) # cancels downstream tasks await self.push_frame(StartInterruptionFrame()) # cancels downstream tasks
await self.push_frame(UserStartedSpeakingFrame()) await self.push_frame(UserStartedSpeakingFrame())
async def _handle_evt_speech_stopped(self, evt): async def _handle_evt_speech_stopped(self, evt):

View File

@@ -1,6 +1,5 @@
# #
# Copyright (c) 2024, Daily # Copyright (c) 2024, Daily
# #
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
@@ -31,7 +30,6 @@ from pipecat.frames.frames import (
SystemFrame, SystemFrame,
TTSStartedFrame, TTSStartedFrame,
TTSStoppedFrame, TTSStoppedFrame,
TextFrame,
TransportMessageFrame, TransportMessageFrame,
TransportMessageUrgentFrame, TransportMessageUrgentFrame,
) )
@@ -296,12 +294,6 @@ class BaseOutputTransport(FrameProcessor):
except Exception as e: except Exception as e:
logger.exception(f"{self} error processing sink queue: {e}") logger.exception(f"{self} error processing sink queue: {e}")
async def _sink_clock_frame_handler(self, frame: Frame):
# TODO(aleix): For now we just process TextFrame. But we should process
# audio and video as well.
if isinstance(frame, TextFrame):
await self.push_frame(frame)
async def _sink_clock_task_handler(self): async def _sink_clock_task_handler(self):
running = True running = True
while running: while running:
@@ -316,12 +308,10 @@ class BaseOutputTransport(FrameProcessor):
# time to process it. # time to process it.
if running: if running:
current_time = self.get_clock().get_time() current_time = self.get_clock().get_time()
if timestamp <= current_time: if timestamp > current_time:
await self._sink_clock_frame_handler(frame)
else:
wait_time = nanoseconds_to_seconds(timestamp - current_time) wait_time = nanoseconds_to_seconds(timestamp - current_time)
await asyncio.sleep(wait_time) await asyncio.sleep(wait_time)
await self._sink_frame_handler(frame) await self._sink_frame_handler(frame)
self._sink_clock_queue.task_done() self._sink_clock_queue.task_done()
except asyncio.CancelledError: except asyncio.CancelledError: