- Add begin_response and finish_after_current_speech methods to CallEndCoordinator for better management of speech events. - Update PromptBrain to utilize new methods, ensuring proper handling of generated closing speech and tool-only calls. - Enhance tests to verify the correct behavior of speech tracking and response handling in various scenarios, including waiting for audio to finish before ending calls. - Introduce a new test suite for CallEndCoordinator to validate the interaction with speech frames.
76 lines
2.5 KiB
Python
76 lines
2.5 KiB
Python
"""Shared call termination timing for prompt tools and workflow end nodes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Awaitable, Callable
|
|
|
|
from loguru import logger
|
|
from pipecat.frames.frames import BotStartedSpeakingFrame, BotStoppedSpeakingFrame
|
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
|
|
|
|
|
class CallEndCoordinator:
|
|
"""End immediately or after the currently armed closing speech finishes."""
|
|
|
|
def __init__(self, queue_end: Callable[[str], Awaitable[None]]):
|
|
self._queue_end = queue_end
|
|
self._ending = False
|
|
self._armed = False
|
|
self._speaking = False
|
|
self._response_speech_started = False
|
|
self._finished = False
|
|
self._reason = "completed"
|
|
|
|
@property
|
|
def ending(self) -> bool:
|
|
return self._ending
|
|
|
|
def begin(self, reason: str) -> None:
|
|
self._ending = True
|
|
self._reason = reason or "completed"
|
|
|
|
def begin_response(self) -> None:
|
|
"""Start tracking speech produced by one LLM response."""
|
|
self._response_speech_started = False
|
|
|
|
def arm_after_speech(self) -> None:
|
|
"""Wait for the next observed bot speech to finish."""
|
|
self._armed = True
|
|
|
|
async def finish_after_current_speech(self, *, has_text: bool) -> None:
|
|
"""Finish now if speech is absent/done, otherwise wait for its stop."""
|
|
if not has_text:
|
|
await self.finish()
|
|
return
|
|
if self._response_speech_started and not self._speaking:
|
|
await self.finish()
|
|
return
|
|
self._armed = True
|
|
|
|
async def finish(self) -> None:
|
|
if self._finished:
|
|
return
|
|
self._finished = True
|
|
await self._queue_end(self._reason)
|
|
|
|
async def observe(self, frame) -> None:
|
|
if isinstance(frame, BotStartedSpeakingFrame):
|
|
self._speaking = True
|
|
self._response_speech_started = True
|
|
elif isinstance(frame, BotStoppedSpeakingFrame) and self._speaking:
|
|
self._speaking = False
|
|
if self._armed:
|
|
logger.info("结束语播报完毕,挂断通话")
|
|
await self.finish()
|
|
|
|
|
|
class EndCallAfterSpeechProcessor(FrameProcessor):
|
|
def __init__(self, coordinator: CallEndCoordinator):
|
|
super().__init__()
|
|
self._coordinator = coordinator
|
|
|
|
async def process_frame(self, frame, direction: FrameDirection):
|
|
await super().process_frame(frame, direction)
|
|
await self.push_frame(frame, direction)
|
|
await self._coordinator.observe(frame)
|