Allow controlling ServiceSwitcher with either immediate frames (SystemFrames) or in-order frames (ControlFrames).
Immediate is the "default", i.e. has the more obvious name (e.g. `ManuallySwitchServiceFrame` v `ManuallySwitchServiceControlFrame`), since that's *probably* what users will want to reach for. Also, the immediate frames are more likely to behave like what we had before the last few commits, where the service switch would always "jump the queue" by having an immediate effect once it hit the `ServiceSwitcher` in the pipeline, jumping ahead of frames in front of it destined for the service.
This commit is contained in:
@@ -1603,17 +1603,46 @@ class MixerEnableFrame(MixerControlFrame):
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceSwitcherFrame(ControlFrame):
|
||||
"""A base class for frames that control ServiceSwitcher behavior."""
|
||||
class ManuallySwitchServiceBaseFrame:
|
||||
"""Base class for frames that request a manual service switch from a ServiceSwitcher.
|
||||
|
||||
Must be inherited alongside either ServiceSwitcherFrame or ServiceSwitcherControlFrame.
|
||||
"""
|
||||
|
||||
service: "FrameProcessor"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceSwitcherFrame(SystemFrame):
|
||||
"""A base class for frames that affect ServiceSwitcher behavior immediately."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ManuallySwitchServiceFrame(ServiceSwitcherFrame):
|
||||
"""A frame to request a manual switch in the active service in a ServiceSwitcher.
|
||||
class ManuallySwitchServiceFrame(ServiceSwitcherFrame, ManuallySwitchServiceBaseFrame):
|
||||
"""A frame to request an immediate manual switch in the active service in a ServiceSwitcher.
|
||||
|
||||
Handled by ServiceSwitcherStrategyManual to switch the active service.
|
||||
"""
|
||||
|
||||
service: "FrameProcessor"
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceSwitcherControlFrame(ControlFrame):
|
||||
"""A base class for frames that affect ServiceSwitcher behavior, in order."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ManuallySwitchServiceControlFrame(
|
||||
ServiceSwitcherControlFrame, ManuallySwitchServiceBaseFrame
|
||||
):
|
||||
"""A frame to request a manual switch in the active service in a ServiceSwitcher, in order.
|
||||
|
||||
Handled by ServiceSwitcherStrategyManual to switch the active service.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -12,8 +12,10 @@ from typing import Any, Generic, List, Optional, Type, TypeVar
|
||||
from pipecat.frames.frames import (
|
||||
ControlFrame,
|
||||
Frame,
|
||||
ManuallySwitchServiceFrame,
|
||||
ManuallySwitchServiceBaseFrame,
|
||||
ServiceSwitcherControlFrame,
|
||||
ServiceSwitcherFrame,
|
||||
SystemFrame,
|
||||
)
|
||||
from pipecat.pipeline.parallel_pipeline import ParallelPipeline
|
||||
from pipecat.processors.filters.function_filter import FunctionFilter
|
||||
@@ -28,7 +30,9 @@ class ServiceSwitcherStrategy:
|
||||
self.services = services
|
||||
self.active_service: Optional[FrameProcessor] = None
|
||||
|
||||
def handle_frame(self, frame: ServiceSwitcherFrame, direction: FrameDirection):
|
||||
def handle_frame(
|
||||
self, frame: ServiceSwitcherFrame | ServiceSwitcherControlFrame, direction: FrameDirection
|
||||
):
|
||||
"""Handle a frame that controls service switching.
|
||||
|
||||
This method can be overridden by subclasses to implement specific logic
|
||||
@@ -53,14 +57,16 @@ class ServiceSwitcherStrategyManual(ServiceSwitcherStrategy):
|
||||
super().__init__(services)
|
||||
self.active_service = services[0] if services else None
|
||||
|
||||
def handle_frame(self, frame: ServiceSwitcherFrame, direction: FrameDirection):
|
||||
def handle_frame(
|
||||
self, frame: ServiceSwitcherFrame | ServiceSwitcherControlFrame, direction: FrameDirection
|
||||
):
|
||||
"""Handle a frame that controls service switching.
|
||||
|
||||
Args:
|
||||
frame: The frame to handle.
|
||||
direction: The direction of the frame (upstream or downstream).
|
||||
"""
|
||||
if isinstance(frame, ManuallySwitchServiceFrame):
|
||||
if isinstance(frame, ManuallySwitchServiceBaseFrame):
|
||||
self._set_active(frame.service)
|
||||
else:
|
||||
raise ValueError(f"Unsupported frame type: {type(frame)}")
|
||||
@@ -110,7 +116,7 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]):
|
||||
|
||||
async def process_frame(self, frame, direction):
|
||||
"""Process a frame through the filter, handling special internal filter-updating frames."""
|
||||
if isinstance(frame, ServiceSwitcher.ServiceSwitcherFilterFrame):
|
||||
if isinstance(frame, ServiceSwitcher.ServiceSwitcherFilterBaseFrame):
|
||||
self.active_service = frame.active_service
|
||||
# Two ServiceSwitcherFilters "sandwich" a service. Push the
|
||||
# frame only to update the other side of the sandwich, but
|
||||
@@ -122,11 +128,23 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]):
|
||||
await super().process_frame(frame, direction)
|
||||
|
||||
@dataclass
|
||||
class ServiceSwitcherFilterFrame(ControlFrame):
|
||||
"""An internal frame used by ServiceSwitcher to filter frames based on active service."""
|
||||
class ServiceSwitcherFilterBaseFrame:
|
||||
"""Base class for internal frames used by ServiceSwitcher to filter frames based on active service."""
|
||||
|
||||
active_service: FrameProcessor
|
||||
|
||||
@dataclass
|
||||
class ServiceSwitcherFilterFrame(SystemFrame, ServiceSwitcherFilterBaseFrame):
|
||||
"""An internal frame used by ServiceSwitcher to filter frames based on active service."""
|
||||
|
||||
pass
|
||||
|
||||
@dataclass
|
||||
class ServiceSwitcherFilterControlFrame(ControlFrame, ServiceSwitcherFilterBaseFrame):
|
||||
"""An internal control frame used by ServiceSwitcher to filter frames based on active service."""
|
||||
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _make_pipeline_definitions(
|
||||
services: List[FrameProcessor], strategy: ServiceSwitcherStrategy
|
||||
@@ -163,11 +181,18 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]):
|
||||
"""
|
||||
await super().process_frame(frame, direction)
|
||||
|
||||
if isinstance(frame, ServiceSwitcherFrame):
|
||||
if isinstance(frame, (ServiceSwitcherFrame, ServiceSwitcherControlFrame)):
|
||||
self.strategy.handle_frame(frame, direction)
|
||||
service_switcher_filter_frame = ServiceSwitcher.ServiceSwitcherFilterFrame(
|
||||
active_service=self.strategy.active_service
|
||||
)
|
||||
if isinstance(frame, ServiceSwitcherFrame):
|
||||
# Apply immediate update (system frame)
|
||||
service_switcher_filter_frame = ServiceSwitcher.ServiceSwitcherFilterFrame(
|
||||
active_service=self.strategy.active_service
|
||||
)
|
||||
else:
|
||||
# Apply in-order update (control frame)
|
||||
service_switcher_filter_frame = ServiceSwitcher.ServiceSwitcherFilterControlFrame(
|
||||
active_service=self.strategy.active_service
|
||||
)
|
||||
# Queue frame that updates filters with new active service
|
||||
# (Hack: we need access ParallelPipeline internals here to queue the frame in each of the pipelines)
|
||||
for p in self._pipelines:
|
||||
|
||||
Reference in New Issue
Block a user