turns: add UserTurnProcessor for advanced pipeline user turn management
This commit is contained in:
1
changelog/3372.added.2.md
Normal file
1
changelog/3372.added.2.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
- Added `UserTurnProcessor`, a frame processor built on `UserTurnController` that pushes `UserStartedSpeakingFrame` and `UserStoppedSpeakingFrame` frames and interruptions based on the controller's user turn strategies.
|
||||||
182
src/pipecat/turns/user_turn_processor.py
Normal file
182
src/pipecat/turns/user_turn_processor.py
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024-2026, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
"""Frame processor for managing the user turn lifecycle."""
|
||||||
|
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from pipecat.frames.frames import (
|
||||||
|
CancelFrame,
|
||||||
|
EndFrame,
|
||||||
|
Frame,
|
||||||
|
StartFrame,
|
||||||
|
UserStartedSpeakingFrame,
|
||||||
|
UserStoppedSpeakingFrame,
|
||||||
|
)
|
||||||
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
|
from pipecat.turns.user_start import BaseUserTurnStartStrategy, UserTurnStartedParams
|
||||||
|
from pipecat.turns.user_stop import BaseUserTurnStopStrategy, UserTurnStoppedParams
|
||||||
|
from pipecat.turns.user_turn_controller import UserTurnController
|
||||||
|
from pipecat.turns.user_turn_strategies import UserTurnStrategies
|
||||||
|
|
||||||
|
|
||||||
|
class UserTurnProcessor(FrameProcessor):
|
||||||
|
"""Frame processor for managing the user turn lifecycle.
|
||||||
|
|
||||||
|
This processor uses a turn controller to determine when a user turn starts
|
||||||
|
or stops. The actual frames emitted (e.g., UserStartedSpeakingFrame,
|
||||||
|
UserStoppedSpeakingFrame) or interruptions depend on the configured
|
||||||
|
strategies.
|
||||||
|
|
||||||
|
Event handlers available:
|
||||||
|
|
||||||
|
- on_user_turn_started: Emitted when a user turn starts.
|
||||||
|
- on_user_turn_stopped: Emitted when a user turn stops.
|
||||||
|
- on_user_turn_stop_timeout: Emitted if no stop strategy triggers before timeout.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
@processor.event_handler("on_user_turn_started")
|
||||||
|
async def on_user_turn_started(processor, strategy: BaseUserTurnStartStrategy):
|
||||||
|
...
|
||||||
|
|
||||||
|
@processor.event_handler("on_user_turn_stopped")
|
||||||
|
async def on_user_turn_stopped(processor, strategy: BaseUserTurnStopStrategy):
|
||||||
|
...
|
||||||
|
|
||||||
|
@processor.event_handler("on_user_turn_stop_timeout")
|
||||||
|
async def on_user_turn_stop_timeout(processor):
|
||||||
|
...
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
user_turn_strategies: Optional[UserTurnStrategies] = None,
|
||||||
|
user_turn_stop_timeout: float = 5.0,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""Initialize the user turn processor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_turn_strategies: Configured strategies for starting and stopping user turns.
|
||||||
|
user_turn_stop_timeout: Timeout in seconds to automatically stop a user turn
|
||||||
|
if no activity is detected.
|
||||||
|
**kwargs: Additional keyword arguments.
|
||||||
|
"""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self._register_event_handler("on_user_turn_started")
|
||||||
|
self._register_event_handler("on_user_turn_stopped")
|
||||||
|
self._register_event_handler("on_user_turn_stop_timeout")
|
||||||
|
|
||||||
|
self._user_turn_controller = UserTurnController(
|
||||||
|
user_turn_strategies=user_turn_strategies or UserTurnStrategies(),
|
||||||
|
user_turn_stop_timeout=user_turn_stop_timeout,
|
||||||
|
)
|
||||||
|
self._user_turn_controller.add_event_handler("on_push_frame", self._on_push_frame)
|
||||||
|
self._user_turn_controller.add_event_handler("on_broadcast_frame", self._on_broadcast_frame)
|
||||||
|
self._user_turn_controller.add_event_handler(
|
||||||
|
"on_user_turn_started", self._on_user_turn_started
|
||||||
|
)
|
||||||
|
self._user_turn_controller.add_event_handler(
|
||||||
|
"on_user_turn_stopped", self._on_user_turn_stopped
|
||||||
|
)
|
||||||
|
self._user_turn_controller.add_event_handler(
|
||||||
|
"on_user_turn_stop_timeout", self._on_user_turn_stop_timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
async def cleanup(self):
|
||||||
|
"""Clean up processor resources."""
|
||||||
|
await super().cleanup()
|
||||||
|
await self._cleanup()
|
||||||
|
|
||||||
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process an incoming frame to detect user turn start or stop.
|
||||||
|
|
||||||
|
The frame is passed to the user turn controlled which is responsible for
|
||||||
|
deciding when a user turn starts or stops and emitting the corresponding
|
||||||
|
events.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The frame to be processed.
|
||||||
|
direction: The direction of the incoming frame.
|
||||||
|
|
||||||
|
"""
|
||||||
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
|
if isinstance(frame, StartFrame):
|
||||||
|
# Push StartFrame before start(), because we want StartFrame to be
|
||||||
|
# processed by every processor before any other frame is processed.
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
await self._start(frame)
|
||||||
|
elif isinstance(frame, EndFrame):
|
||||||
|
# Push EndFrame before stop(), because stop() waits on the task to
|
||||||
|
# finish and the task finishes when EndFrame is processed.
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
await self._stop(frame)
|
||||||
|
elif isinstance(frame, CancelFrame):
|
||||||
|
await self._cancel(frame)
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
else:
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
|
await self._user_turn_controller.process_frame(frame)
|
||||||
|
|
||||||
|
async def _start(self, frame: StartFrame):
|
||||||
|
await self._user_turn_controller.setup(self.task_manager)
|
||||||
|
|
||||||
|
async def _stop(self, frame: EndFrame):
|
||||||
|
await self._cleanup()
|
||||||
|
|
||||||
|
async def _cancel(self, frame: CancelFrame):
|
||||||
|
await self._cleanup()
|
||||||
|
|
||||||
|
async def _cleanup(self):
|
||||||
|
await self._user_turn_controller.cleanup()
|
||||||
|
|
||||||
|
async def _on_push_frame(
|
||||||
|
self, controller, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM
|
||||||
|
):
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
|
async def _on_broadcast_frame(self, controller, frame_cls: Type[Frame], **kwargs):
|
||||||
|
await self.broadcast_frame(frame_cls, **kwargs)
|
||||||
|
|
||||||
|
async def _on_user_turn_started(
|
||||||
|
self,
|
||||||
|
controller: UserTurnController,
|
||||||
|
strategy: BaseUserTurnStartStrategy,
|
||||||
|
params: UserTurnStartedParams,
|
||||||
|
):
|
||||||
|
logger.debug(f"{self}: User started speaking (user turn start strategy: {strategy})")
|
||||||
|
|
||||||
|
if params.enable_user_speaking_frames:
|
||||||
|
await self.broadcast_frame(UserStartedSpeakingFrame)
|
||||||
|
|
||||||
|
if params.enable_interruptions and self._allow_interruptions:
|
||||||
|
await self.push_interruption_task_frame_and_wait()
|
||||||
|
|
||||||
|
await self._call_event_handler("on_user_turn_started", strategy)
|
||||||
|
|
||||||
|
async def _on_user_turn_stopped(
|
||||||
|
self,
|
||||||
|
controller: UserTurnController,
|
||||||
|
strategy: BaseUserTurnStopStrategy,
|
||||||
|
params: UserTurnStoppedParams,
|
||||||
|
):
|
||||||
|
logger.debug(f"{self}: User stopped speaking (user turn stop strategy: {strategy})")
|
||||||
|
|
||||||
|
if params.enable_user_speaking_frames:
|
||||||
|
await self.broadcast_frame(UserStoppedSpeakingFrame)
|
||||||
|
|
||||||
|
await self._call_event_handler("on_user_turn_stopped", strategy)
|
||||||
|
|
||||||
|
async def _on_user_turn_stop_timeout(self, controller):
|
||||||
|
await self._call_event_handler("on_user_turn_stop_timeout")
|
||||||
Reference in New Issue
Block a user