introduce synchronous and asynchronous frame processors
Pipecat has a pipeline-based architecture. The pipeline consists of frame processors linked to each other. The elements travelling across the pipeline are called frames. To have a deterministic behavior the frames travelling through the pipeline should always be ordered, except system frames which are out-of-band frames. To achieve that, each frame processor should only output frames from a single task. There are synchronous and asynchronous frame processors. The synchronous processors push output frames from the same task that they receive input frames, and therefore only pushing frames from one task. Asynchrnous frame processors can have internal tasks to perform things asynchrnously (e.g. receiving data from a websocket) but they also have a single task where they push frames from.
This commit is contained in:
@@ -70,7 +70,7 @@ async def main():
|
|||||||
async def user_idle_callback(user_idle: UserIdleProcessor):
|
async def user_idle_callback(user_idle: UserIdleProcessor):
|
||||||
messages.append(
|
messages.append(
|
||||||
{"role": "system", "content": "Ask the user if they are still there and try to prompt for some input, but be short."})
|
{"role": "system", "content": "Ask the user if they are still there and try to prompt for some input, but be short."})
|
||||||
await user_idle.queue_frame(LLMMessagesFrame(messages))
|
await user_idle.push_frame(LLMMessagesFrame(messages))
|
||||||
|
|
||||||
user_idle = UserIdleProcessor(callback=user_idle_callback, timeout=5.0)
|
user_idle = UserIdleProcessor(callback=user_idle_callback, timeout=5.0)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
import sys
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContextFrame, OpenAILLMContext
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContextFrame, OpenAILLMContext
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from pipecat.frames.frames import EndFrame, Frame, StartInterruptionFrame
|
|
||||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncFrameProcessor(FrameProcessor):
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
name: str | None = None,
|
|
||||||
loop: asyncio.AbstractEventLoop | None = None,
|
|
||||||
**kwargs):
|
|
||||||
super().__init__(name=name, loop=loop, **kwargs)
|
|
||||||
|
|
||||||
self._create_push_task()
|
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
||||||
await super().process_frame(frame, direction)
|
|
||||||
|
|
||||||
if isinstance(frame, StartInterruptionFrame):
|
|
||||||
await self._handle_interruptions(frame)
|
|
||||||
|
|
||||||
async def queue_frame(
|
|
||||||
self,
|
|
||||||
frame: Frame,
|
|
||||||
direction: FrameDirection = FrameDirection.DOWNSTREAM):
|
|
||||||
await self._push_queue.put((frame, direction))
|
|
||||||
|
|
||||||
async def cleanup(self):
|
|
||||||
self._push_frame_task.cancel()
|
|
||||||
await self._push_frame_task
|
|
||||||
|
|
||||||
async def _handle_interruptions(self, frame: Frame):
|
|
||||||
# Cancel the task. This will stop pushing frames downstream.
|
|
||||||
self._push_frame_task.cancel()
|
|
||||||
await self._push_frame_task
|
|
||||||
# Push an out-of-band frame (i.e. not using the ordered push
|
|
||||||
# frame task).
|
|
||||||
await self.push_frame(frame)
|
|
||||||
# Create a new queue and task.
|
|
||||||
self._create_push_task()
|
|
||||||
|
|
||||||
def _create_push_task(self):
|
|
||||||
self._push_queue = asyncio.Queue()
|
|
||||||
self._push_frame_task = self.get_event_loop().create_task(self._push_frame_task_handler())
|
|
||||||
|
|
||||||
async def _push_frame_task_handler(self):
|
|
||||||
running = True
|
|
||||||
while running:
|
|
||||||
try:
|
|
||||||
(frame, direction) = await self._push_queue.get()
|
|
||||||
await self.push_frame(frame, direction)
|
|
||||||
running = not isinstance(frame, EndFrame)
|
|
||||||
self._push_queue.task_done()
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
break
|
|
||||||
@@ -11,12 +11,14 @@ from enum import Enum
|
|||||||
|
|
||||||
from pipecat.clocks.base_clock import BaseClock
|
from pipecat.clocks.base_clock import BaseClock
|
||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
|
EndFrame,
|
||||||
ErrorFrame,
|
ErrorFrame,
|
||||||
Frame,
|
Frame,
|
||||||
MetricsFrame,
|
MetricsFrame,
|
||||||
StartFrame,
|
StartFrame,
|
||||||
StartInterruptionFrame,
|
StartInterruptionFrame,
|
||||||
UserStoppedSpeakingFrame)
|
StopInterruptionFrame,
|
||||||
|
SystemFrame)
|
||||||
from pipecat.utils.utils import obj_count, obj_id
|
from pipecat.utils.utils import obj_count, obj_id
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -88,6 +90,7 @@ class FrameProcessor:
|
|||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
|
sync: bool = True,
|
||||||
loop: asyncio.AbstractEventLoop | None = None,
|
loop: asyncio.AbstractEventLoop | None = None,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
self.id: int = obj_id()
|
self.id: int = obj_id()
|
||||||
@@ -96,6 +99,7 @@ class FrameProcessor:
|
|||||||
self._prev: "FrameProcessor" | None = None
|
self._prev: "FrameProcessor" | None = None
|
||||||
self._next: "FrameProcessor" | None = None
|
self._next: "FrameProcessor" | None = None
|
||||||
self._loop: asyncio.AbstractEventLoop = loop or asyncio.get_running_loop()
|
self._loop: asyncio.AbstractEventLoop = loop or asyncio.get_running_loop()
|
||||||
|
self._sync = sync
|
||||||
|
|
||||||
# Clock
|
# Clock
|
||||||
self._clock: BaseClock | None = None
|
self._clock: BaseClock | None = None
|
||||||
@@ -109,6 +113,14 @@ class FrameProcessor:
|
|||||||
# Metrics
|
# Metrics
|
||||||
self._metrics = FrameProcessorMetrics(name=self.name)
|
self._metrics = FrameProcessorMetrics(name=self.name)
|
||||||
|
|
||||||
|
# Every processor in Pipecat should only output frames from a single
|
||||||
|
# task. This avoid problems like audio overlapping. System frames are
|
||||||
|
# the exception to this rule.
|
||||||
|
#
|
||||||
|
# This create this task.
|
||||||
|
if not self._sync:
|
||||||
|
self.__create_push_task()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def interruptions_allowed(self):
|
def interruptions_allowed(self):
|
||||||
return self._allow_interruptions
|
return self._allow_interruptions
|
||||||
@@ -192,14 +204,38 @@ class FrameProcessor:
|
|||||||
self._enable_usage_metrics = frame.enable_usage_metrics
|
self._enable_usage_metrics = frame.enable_usage_metrics
|
||||||
self._report_only_initial_ttfb = frame.report_only_initial_ttfb
|
self._report_only_initial_ttfb = frame.report_only_initial_ttfb
|
||||||
elif isinstance(frame, StartInterruptionFrame):
|
elif isinstance(frame, StartInterruptionFrame):
|
||||||
|
await self._start_interruption()
|
||||||
await self.stop_all_metrics()
|
await self.stop_all_metrics()
|
||||||
elif isinstance(frame, UserStoppedSpeakingFrame):
|
elif isinstance(frame, StopInterruptionFrame):
|
||||||
self._should_report_ttfb = True
|
self._should_report_ttfb = True
|
||||||
|
|
||||||
async def push_error(self, error: ErrorFrame):
|
async def push_error(self, error: ErrorFrame):
|
||||||
await self.push_frame(error, FrameDirection.UPSTREAM)
|
await self.push_frame(error, FrameDirection.UPSTREAM)
|
||||||
|
|
||||||
async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM):
|
async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM):
|
||||||
|
if self._sync or isinstance(frame, SystemFrame):
|
||||||
|
await self.__internal_push_frame(frame, direction)
|
||||||
|
else:
|
||||||
|
await self.__push_queue.put((frame, direction))
|
||||||
|
|
||||||
|
#
|
||||||
|
# Handle interruptions
|
||||||
|
#
|
||||||
|
|
||||||
|
async def _start_interruption(self):
|
||||||
|
if not self._sync:
|
||||||
|
# Cancel the task. This will stop pushing frames downstream.
|
||||||
|
self.__push_frame_task.cancel()
|
||||||
|
await self.__push_frame_task
|
||||||
|
|
||||||
|
# Create a new queue and task.
|
||||||
|
self.__create_push_task()
|
||||||
|
|
||||||
|
async def _stop_interruption(self):
|
||||||
|
# Nothing to do right now.
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def __internal_push_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
try:
|
try:
|
||||||
if direction == FrameDirection.DOWNSTREAM and self._next:
|
if direction == FrameDirection.DOWNSTREAM and self._next:
|
||||||
logger.trace(f"Pushing {frame} from {self} to {self._next}")
|
logger.trace(f"Pushing {frame} from {self} to {self._next}")
|
||||||
@@ -210,5 +246,20 @@ class FrameProcessor:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Uncaught exception in {self}: {e}")
|
logger.exception(f"Uncaught exception in {self}: {e}")
|
||||||
|
|
||||||
|
def __create_push_task(self):
|
||||||
|
self.__push_queue = asyncio.Queue()
|
||||||
|
self.__push_frame_task = self.get_event_loop().create_task(self.__push_frame_task_handler())
|
||||||
|
|
||||||
|
async def __push_frame_task_handler(self):
|
||||||
|
running = True
|
||||||
|
while running:
|
||||||
|
try:
|
||||||
|
(frame, direction) = await self.__push_queue.get()
|
||||||
|
await self.__internal_push_frame(frame, direction)
|
||||||
|
running = not isinstance(frame, EndFrame)
|
||||||
|
self.__push_queue.task_done()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
break
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
@@ -272,8 +272,9 @@ class RTVIProcessor(FrameProcessor):
|
|||||||
def __init__(self,
|
def __init__(self,
|
||||||
*,
|
*,
|
||||||
config: RTVIConfig = RTVIConfig(config=[]),
|
config: RTVIConfig = RTVIConfig(config=[]),
|
||||||
params: RTVIProcessorParams = RTVIProcessorParams()):
|
params: RTVIProcessorParams = RTVIProcessorParams(),
|
||||||
super().__init__()
|
**kwargs):
|
||||||
|
super().__init__(sync=False, **kwargs)
|
||||||
self._config = config
|
self._config = config
|
||||||
self._params = params
|
self._params = params
|
||||||
|
|
||||||
@@ -286,9 +287,6 @@ class RTVIProcessor(FrameProcessor):
|
|||||||
self._registered_actions: Dict[str, RTVIAction] = {}
|
self._registered_actions: Dict[str, RTVIAction] = {}
|
||||||
self._registered_services: Dict[str, RTVIService] = {}
|
self._registered_services: Dict[str, RTVIService] = {}
|
||||||
|
|
||||||
self._push_frame_task = self.get_event_loop().create_task(self._push_frame_task_handler())
|
|
||||||
self._push_queue = asyncio.Queue()
|
|
||||||
|
|
||||||
self._message_task = self.get_event_loop().create_task(self._message_task_handler())
|
self._message_task = self.get_event_loop().create_task(self._message_task_handler())
|
||||||
self._message_queue = asyncio.Queue()
|
self._message_queue = asyncio.Queue()
|
||||||
|
|
||||||
@@ -335,12 +333,6 @@ class RTVIProcessor(FrameProcessor):
|
|||||||
message = RTVILLMFunctionCallStartMessage(data=fn)
|
message = RTVILLMFunctionCallStartMessage(data=fn)
|
||||||
await self._push_transport_message(message, exclude_none=False)
|
await self._push_transport_message(message, exclude_none=False)
|
||||||
|
|
||||||
async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM):
|
|
||||||
if isinstance(frame, SystemFrame):
|
|
||||||
await super().push_frame(frame, direction)
|
|
||||||
else:
|
|
||||||
await self._internal_push_frame(frame, direction)
|
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
@@ -394,30 +386,10 @@ class RTVIProcessor(FrameProcessor):
|
|||||||
# processing EndFrames.
|
# processing EndFrames.
|
||||||
self._message_task.cancel()
|
self._message_task.cancel()
|
||||||
await self._message_task
|
await self._message_task
|
||||||
await self._push_frame_task
|
|
||||||
|
|
||||||
async def _cancel(self, frame: CancelFrame):
|
async def _cancel(self, frame: CancelFrame):
|
||||||
self._message_task.cancel()
|
self._message_task.cancel()
|
||||||
await self._message_task
|
await self._message_task
|
||||||
self._push_frame_task.cancel()
|
|
||||||
await self._push_frame_task
|
|
||||||
|
|
||||||
async def _internal_push_frame(
|
|
||||||
self,
|
|
||||||
frame: Frame | None,
|
|
||||||
direction: FrameDirection | None = FrameDirection.DOWNSTREAM):
|
|
||||||
await self._push_queue.put((frame, direction))
|
|
||||||
|
|
||||||
async def _push_frame_task_handler(self):
|
|
||||||
running = True
|
|
||||||
while running:
|
|
||||||
try:
|
|
||||||
(frame, direction) = await self._push_queue.get()
|
|
||||||
await super().push_frame(frame, direction)
|
|
||||||
self._push_queue.task_done()
|
|
||||||
running = not isinstance(frame, EndFrame)
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
break
|
|
||||||
|
|
||||||
async def _push_transport_message(self, model: BaseModel, exclude_none: bool = True):
|
async def _push_transport_message(self, model: BaseModel, exclude_none: bool = True):
|
||||||
frame = TransportMessageFrame(
|
frame = TransportMessageFrame(
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class GStreamerPipelineSource(FrameProcessor):
|
|||||||
clock_sync: bool = True
|
clock_sync: bool = True
|
||||||
|
|
||||||
def __init__(self, *, pipeline: str, out_params: OutputParams = OutputParams(), **kwargs):
|
def __init__(self, *, pipeline: str, out_params: OutputParams = OutputParams(), **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(sync=False, **kwargs)
|
||||||
|
|
||||||
self._out_params = out_params
|
self._out_params = out_params
|
||||||
|
|
||||||
@@ -62,10 +62,6 @@ class GStreamerPipelineSource(FrameProcessor):
|
|||||||
bus.add_signal_watch()
|
bus.add_signal_watch()
|
||||||
bus.connect("message", self._on_gstreamer_message)
|
bus.connect("message", self._on_gstreamer_message)
|
||||||
|
|
||||||
# Create push frame task. This is the task that will push frames in
|
|
||||||
# order. We also guarantee that all frames are pushed in the same task.
|
|
||||||
self._create_push_task()
|
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
@@ -80,60 +76,28 @@ class GStreamerPipelineSource(FrameProcessor):
|
|||||||
elif isinstance(frame, StartFrame):
|
elif isinstance(frame, StartFrame):
|
||||||
# Push StartFrame before start(), because we want StartFrame to be
|
# Push StartFrame before start(), because we want StartFrame to be
|
||||||
# processed by every processor before any other frame is processed.
|
# processed by every processor before any other frame is processed.
|
||||||
await self._internal_push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
await self._start(frame)
|
await self._start(frame)
|
||||||
elif isinstance(frame, EndFrame):
|
elif isinstance(frame, EndFrame):
|
||||||
# Push EndFrame before stop(), because stop() waits on the task to
|
# Push EndFrame before stop(), because stop() waits on the task to
|
||||||
# finish and the task finishes when EndFrame is processed.
|
# finish and the task finishes when EndFrame is processed.
|
||||||
await self._internal_push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
await self._stop(frame)
|
await self._stop(frame)
|
||||||
# Other frames
|
# Other frames
|
||||||
else:
|
else:
|
||||||
await self._internal_push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
async def _start(self, frame: StartFrame):
|
async def _start(self, frame: StartFrame):
|
||||||
self._player.set_state(Gst.State.PLAYING)
|
self._player.set_state(Gst.State.PLAYING)
|
||||||
|
|
||||||
async def _stop(self, frame: EndFrame):
|
async def _stop(self, frame: EndFrame):
|
||||||
self._player.set_state(Gst.State.NULL)
|
self._player.set_state(Gst.State.NULL)
|
||||||
# Wait for the push frame task to finish. It will finish when the
|
|
||||||
# EndFrame is actually processed.
|
|
||||||
await self._push_frame_task
|
|
||||||
|
|
||||||
async def _cancel(self, frame: CancelFrame):
|
async def _cancel(self, frame: CancelFrame):
|
||||||
self._player.set_state(Gst.State.NULL)
|
self._player.set_state(Gst.State.NULL)
|
||||||
# Cancel all the tasks and wait for them to finish.
|
|
||||||
self._push_frame_task.cancel()
|
|
||||||
await self._push_frame_task
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Push frames task
|
# GStreamer
|
||||||
#
|
|
||||||
|
|
||||||
def _create_push_task(self):
|
|
||||||
loop = self.get_event_loop()
|
|
||||||
self._push_queue = asyncio.Queue()
|
|
||||||
self._push_frame_task = loop.create_task(self._push_frame_task_handler())
|
|
||||||
|
|
||||||
async def _internal_push_frame(
|
|
||||||
self,
|
|
||||||
frame: Frame | None,
|
|
||||||
direction: FrameDirection | None = FrameDirection.DOWNSTREAM):
|
|
||||||
await self._push_queue.put((frame, direction))
|
|
||||||
|
|
||||||
async def _push_frame_task_handler(self):
|
|
||||||
running = True
|
|
||||||
while running:
|
|
||||||
try:
|
|
||||||
(frame, direction) = await self._push_queue.get()
|
|
||||||
await self.push_frame(frame, direction)
|
|
||||||
running = not isinstance(frame, EndFrame)
|
|
||||||
self._push_queue.task_done()
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
break
|
|
||||||
|
|
||||||
#
|
|
||||||
# GStreaner
|
|
||||||
#
|
#
|
||||||
|
|
||||||
def _on_gstreamer_message(self, bus: Gst.Bus, message: Gst.Message):
|
def _on_gstreamer_message(self, bus: Gst.Bus, message: Gst.Message):
|
||||||
@@ -221,7 +185,7 @@ class GStreamerPipelineSource(FrameProcessor):
|
|||||||
frame = AudioRawFrame(audio=info.data,
|
frame = AudioRawFrame(audio=info.data,
|
||||||
sample_rate=self._out_params.audio_sample_rate,
|
sample_rate=self._out_params.audio_sample_rate,
|
||||||
num_channels=self._out_params.audio_channels)
|
num_channels=self._out_params.audio_channels)
|
||||||
asyncio.run_coroutine_threadsafe(self._internal_push_frame(frame), self.get_event_loop())
|
asyncio.run_coroutine_threadsafe(self.push_frame(frame), self.get_event_loop())
|
||||||
buffer.unmap(info)
|
buffer.unmap(info)
|
||||||
return Gst.FlowReturn.OK
|
return Gst.FlowReturn.OK
|
||||||
|
|
||||||
@@ -232,6 +196,6 @@ class GStreamerPipelineSource(FrameProcessor):
|
|||||||
image=info.data,
|
image=info.data,
|
||||||
size=(self._out_params.video_width, self._out_params.video_height),
|
size=(self._out_params.video_width, self._out_params.video_height),
|
||||||
format="RGB")
|
format="RGB")
|
||||||
asyncio.run_coroutine_threadsafe(self._internal_push_frame(frame), self.get_event_loop())
|
asyncio.run_coroutine_threadsafe(self.push_frame(frame), self.get_event_loop())
|
||||||
buffer.unmap(info)
|
buffer.unmap(info)
|
||||||
return Gst.FlowReturn.OK
|
return Gst.FlowReturn.OK
|
||||||
|
|||||||
@@ -8,19 +8,14 @@ import asyncio
|
|||||||
|
|
||||||
from typing import Awaitable, Callable, List
|
from typing import Awaitable, Callable, List
|
||||||
|
|
||||||
from pipecat.frames.frames import Frame, SystemFrame
|
from pipecat.frames.frames import Frame
|
||||||
from pipecat.processors.async_frame_processor import AsyncFrameProcessor
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
from pipecat.processors.frame_processor import FrameDirection
|
|
||||||
|
|
||||||
|
|
||||||
class IdleFrameProcessor(AsyncFrameProcessor):
|
class IdleFrameProcessor(FrameProcessor):
|
||||||
"""This class waits to receive any frame or list of desired frames within a
|
"""This class waits to receive any frame or list of desired frames within a
|
||||||
given timeout. If the timeout is reached before receiving any of those
|
given timeout. If the timeout is reached before receiving any of those
|
||||||
frames the provided callback will be called.
|
frames the provided callback will be called.
|
||||||
|
|
||||||
The callback can then be used to push frames downstream by using
|
|
||||||
`queue_frame()` (or `push_frame()` for system frames).
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -30,7 +25,7 @@ class IdleFrameProcessor(AsyncFrameProcessor):
|
|||||||
timeout: float,
|
timeout: float,
|
||||||
types: List[type] = [],
|
types: List[type] = [],
|
||||||
**kwargs):
|
**kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(sync=False, **kwargs)
|
||||||
|
|
||||||
self._callback = callback
|
self._callback = callback
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
@@ -41,10 +36,7 @@ class IdleFrameProcessor(AsyncFrameProcessor):
|
|||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
if isinstance(frame, SystemFrame):
|
await self.push_frame(frame, direction)
|
||||||
await self.push_frame(frame, direction)
|
|
||||||
else:
|
|
||||||
await self.queue_frame(frame, direction)
|
|
||||||
|
|
||||||
# If we are not waiting for any specific frame set the event, otherwise
|
# If we are not waiting for any specific frame set the event, otherwise
|
||||||
# check if we have received one of the desired frames.
|
# check if we have received one of the desired frames.
|
||||||
@@ -55,7 +47,6 @@ class IdleFrameProcessor(AsyncFrameProcessor):
|
|||||||
if isinstance(frame, t):
|
if isinstance(frame, t):
|
||||||
self._idle_event.set()
|
self._idle_event.set()
|
||||||
|
|
||||||
# If we are not waiting for any specific frame set the event, otherwise
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
self._idle_task.cancel()
|
self._idle_task.cancel()
|
||||||
await self._idle_task
|
await self._idle_task
|
||||||
|
|||||||
@@ -11,21 +11,16 @@ from typing import Awaitable, Callable
|
|||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
BotSpeakingFrame,
|
BotSpeakingFrame,
|
||||||
Frame,
|
Frame,
|
||||||
SystemFrame,
|
|
||||||
UserStartedSpeakingFrame,
|
UserStartedSpeakingFrame,
|
||||||
UserStoppedSpeakingFrame)
|
UserStoppedSpeakingFrame)
|
||||||
from pipecat.processors.async_frame_processor import AsyncFrameProcessor
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
from pipecat.processors.frame_processor import FrameDirection
|
|
||||||
|
|
||||||
|
|
||||||
class UserIdleProcessor(AsyncFrameProcessor):
|
class UserIdleProcessor(FrameProcessor):
|
||||||
"""This class is useful to check if the user is interacting with the bot
|
"""This class is useful to check if the user is interacting with the bot
|
||||||
within a given timeout. If the timeout is reached before any interaction
|
within a given timeout. If the timeout is reached before any interaction
|
||||||
occurred the provided callback will be called.
|
occurred the provided callback will be called.
|
||||||
|
|
||||||
The callback can then be used to push frames downstream by using
|
|
||||||
`queue_frame()` (or `push_frame()` for system frames).
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -34,7 +29,7 @@ class UserIdleProcessor(AsyncFrameProcessor):
|
|||||||
callback: Callable[["UserIdleProcessor"], Awaitable[None]],
|
callback: Callable[["UserIdleProcessor"], Awaitable[None]],
|
||||||
timeout: float,
|
timeout: float,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(sync=False, **kwargs)
|
||||||
|
|
||||||
self._callback = callback
|
self._callback = callback
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
@@ -46,10 +41,7 @@ class UserIdleProcessor(AsyncFrameProcessor):
|
|||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
if isinstance(frame, SystemFrame):
|
await self.push_frame(frame, direction)
|
||||||
await self.push_frame(frame, direction)
|
|
||||||
else:
|
|
||||||
await self.queue_frame(frame, direction)
|
|
||||||
|
|
||||||
# We shouldn't call the idle callback if the user or the bot are speaking.
|
# We shouldn't call the idle callback if the user or the bot are speaking.
|
||||||
if isinstance(frame, UserStartedSpeakingFrame):
|
if isinstance(frame, UserStartedSpeakingFrame):
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ from pipecat.frames.frames import (
|
|||||||
UserImageRequestFrame,
|
UserImageRequestFrame,
|
||||||
VisionImageRawFrame
|
VisionImageRawFrame
|
||||||
)
|
)
|
||||||
from pipecat.processors.async_frame_processor import AsyncFrameProcessor
|
|
||||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
from pipecat.transcriptions.language import Language
|
from pipecat.transcriptions.language import Language
|
||||||
from pipecat.utils.audio import calculate_audio_volume
|
from pipecat.utils.audio import calculate_audio_volume
|
||||||
@@ -67,7 +66,7 @@ class AIService(FrameProcessor):
|
|||||||
elif isinstance(frame, EndFrame):
|
elif isinstance(frame, EndFrame):
|
||||||
await self.stop(frame)
|
await self.stop(frame)
|
||||||
|
|
||||||
async def process_generator(self, generator: AsyncGenerator[Frame, None]):
|
async def process_generator(self, generator: AsyncGenerator[Frame | None, None]):
|
||||||
async for f in generator:
|
async for f in generator:
|
||||||
if f:
|
if f:
|
||||||
if isinstance(f, ErrorFrame):
|
if isinstance(f, ErrorFrame):
|
||||||
@@ -76,30 +75,6 @@ class AIService(FrameProcessor):
|
|||||||
await self.push_frame(f)
|
await self.push_frame(f)
|
||||||
|
|
||||||
|
|
||||||
class AsyncAIService(AsyncFrameProcessor):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def stop(self, frame: EndFrame):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def cancel(self, frame: CancelFrame):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
||||||
await super().process_frame(frame, direction)
|
|
||||||
|
|
||||||
if isinstance(frame, StartFrame):
|
|
||||||
await self.start(frame)
|
|
||||||
elif isinstance(frame, CancelFrame):
|
|
||||||
await self.cancel(frame)
|
|
||||||
elif isinstance(frame, EndFrame):
|
|
||||||
await self.stop(frame)
|
|
||||||
|
|
||||||
|
|
||||||
class LLMService(AIService):
|
class LLMService(AIService):
|
||||||
"""This class is a no-op but serves as a base class for LLM services."""
|
"""This class is a no-op but serves as a base class for LLM services."""
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,11 @@ from pipecat.frames.frames import (
|
|||||||
ErrorFrame,
|
ErrorFrame,
|
||||||
Frame,
|
Frame,
|
||||||
StartFrame,
|
StartFrame,
|
||||||
SystemFrame,
|
|
||||||
TTSStartedFrame,
|
TTSStartedFrame,
|
||||||
TTSStoppedFrame,
|
TTSStoppedFrame,
|
||||||
TranscriptionFrame,
|
TranscriptionFrame,
|
||||||
URLImageRawFrame)
|
URLImageRawFrame)
|
||||||
from pipecat.processors.frame_processor import FrameDirection
|
from pipecat.services.ai_services import STTService, TTSService, ImageGenService
|
||||||
from pipecat.services.ai_services import AsyncAIService, TTSService, ImageGenService
|
|
||||||
from pipecat.services.openai import BaseOpenAILLMService
|
from pipecat.services.openai import BaseOpenAILLMService
|
||||||
from pipecat.utils.time import time_now_iso8601
|
from pipecat.utils.time import time_now_iso8601
|
||||||
|
|
||||||
@@ -126,7 +124,7 @@ class AzureTTSService(TTSService):
|
|||||||
logger.error(f"{self} error: {cancellation_details.error_details}")
|
logger.error(f"{self} error: {cancellation_details.error_details}")
|
||||||
|
|
||||||
|
|
||||||
class AzureSTTService(AsyncAIService):
|
class AzureSTTService(STTService):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -149,15 +147,11 @@ class AzureSTTService(AsyncAIService):
|
|||||||
speech_config=speech_config, audio_config=audio_config)
|
speech_config=speech_config, audio_config=audio_config)
|
||||||
self._speech_recognizer.recognized.connect(self._on_handle_recognized)
|
self._speech_recognizer.recognized.connect(self._on_handle_recognized)
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]:
|
||||||
await super().process_frame(frame, direction)
|
await self.start_processing_metrics()
|
||||||
|
self._audio_stream.write(audio)
|
||||||
if isinstance(frame, SystemFrame):
|
await self.stop_processing_metrics()
|
||||||
await self.push_frame(frame, direction)
|
yield None
|
||||||
elif isinstance(frame, AudioRawFrame):
|
|
||||||
self._audio_stream.write(frame.audio)
|
|
||||||
else:
|
|
||||||
await self._push_queue.put((frame, direction))
|
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
@@ -176,7 +170,7 @@ class AzureSTTService(AsyncAIService):
|
|||||||
def _on_handle_recognized(self, event):
|
def _on_handle_recognized(self, event):
|
||||||
if event.result.reason == ResultReason.RecognizedSpeech and len(event.result.text) > 0:
|
if event.result.reason == ResultReason.RecognizedSpeech and len(event.result.text) > 0:
|
||||||
frame = TranscriptionFrame(event.result.text, "", time_now_iso8601())
|
frame = TranscriptionFrame(event.result.text, "", time_now_iso8601())
|
||||||
asyncio.run_coroutine_threadsafe(self.queue_frame(frame), self.get_event_loop())
|
asyncio.run_coroutine_threadsafe(self.push_frame(frame), self.get_event_loop())
|
||||||
|
|
||||||
|
|
||||||
class AzureImageGenServiceREST(ImageGenService):
|
class AzureImageGenServiceREST(ImageGenService):
|
||||||
|
|||||||
@@ -161,8 +161,8 @@ class DeepgramSTTService(STTService):
|
|||||||
async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]:
|
async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]:
|
||||||
await self.start_processing_metrics()
|
await self.start_processing_metrics()
|
||||||
await self._connection.send(audio)
|
await self._connection.send(audio)
|
||||||
yield None
|
|
||||||
await self.stop_processing_metrics()
|
await self.stop_processing_metrics()
|
||||||
|
yield None
|
||||||
|
|
||||||
async def _connect(self):
|
async def _connect(self):
|
||||||
if await self._connection.start(self._live_options):
|
if await self._connection.start(self._live_options):
|
||||||
|
|||||||
@@ -7,20 +7,17 @@
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from typing import Optional
|
from typing import AsyncGenerator, Optional
|
||||||
from pydantic.main import BaseModel
|
from pydantic.main import BaseModel
|
||||||
|
|
||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
AudioRawFrame,
|
|
||||||
CancelFrame,
|
CancelFrame,
|
||||||
EndFrame,
|
EndFrame,
|
||||||
Frame,
|
Frame,
|
||||||
InterimTranscriptionFrame,
|
InterimTranscriptionFrame,
|
||||||
StartFrame,
|
StartFrame,
|
||||||
SystemFrame,
|
|
||||||
TranscriptionFrame)
|
TranscriptionFrame)
|
||||||
from pipecat.processors.frame_processor import FrameDirection
|
from pipecat.services.ai_services import STTService
|
||||||
from pipecat.services.ai_services import AsyncAIService
|
|
||||||
from pipecat.utils.time import time_now_iso8601
|
from pipecat.utils.time import time_now_iso8601
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -35,7 +32,7 @@ except ModuleNotFoundError as e:
|
|||||||
raise Exception(f"Missing module: {e}")
|
raise Exception(f"Missing module: {e}")
|
||||||
|
|
||||||
|
|
||||||
class GladiaSTTService(AsyncAIService):
|
class GladiaSTTService(STTService):
|
||||||
class InputParams(BaseModel):
|
class InputParams(BaseModel):
|
||||||
sample_rate: Optional[int] = 16000
|
sample_rate: Optional[int] = 16000
|
||||||
language: Optional[str] = "english"
|
language: Optional[str] = "english"
|
||||||
@@ -50,23 +47,13 @@ class GladiaSTTService(AsyncAIService):
|
|||||||
confidence: float = 0.5,
|
confidence: float = 0.5,
|
||||||
params: InputParams = InputParams(),
|
params: InputParams = InputParams(),
|
||||||
**kwargs):
|
**kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(sync=False, **kwargs)
|
||||||
|
|
||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
self._url = url
|
self._url = url
|
||||||
self._params = params
|
self._params = params
|
||||||
self._confidence = confidence
|
self._confidence = confidence
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
||||||
await super().process_frame(frame, direction)
|
|
||||||
|
|
||||||
if isinstance(frame, SystemFrame):
|
|
||||||
await self.push_frame(frame, direction)
|
|
||||||
elif isinstance(frame, AudioRawFrame):
|
|
||||||
await self._send_audio(frame)
|
|
||||||
else:
|
|
||||||
await self.queue_frame(frame, direction)
|
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
self._websocket = await websockets.connect(self._url)
|
self._websocket = await websockets.connect(self._url)
|
||||||
@@ -81,6 +68,12 @@ class GladiaSTTService(AsyncAIService):
|
|||||||
await super().cancel(frame)
|
await super().cancel(frame)
|
||||||
await self._websocket.close()
|
await self._websocket.close()
|
||||||
|
|
||||||
|
async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]:
|
||||||
|
await self.start_processing_metrics()
|
||||||
|
await self._send_audio(audio)
|
||||||
|
await self.stop_processing_metrics()
|
||||||
|
yield None
|
||||||
|
|
||||||
async def _setup_gladia(self):
|
async def _setup_gladia(self):
|
||||||
configuration = {
|
configuration = {
|
||||||
"x_gladia_key": self._api_key,
|
"x_gladia_key": self._api_key,
|
||||||
@@ -92,9 +85,9 @@ class GladiaSTTService(AsyncAIService):
|
|||||||
|
|
||||||
await self._websocket.send(json.dumps(configuration))
|
await self._websocket.send(json.dumps(configuration))
|
||||||
|
|
||||||
async def _send_audio(self, frame: AudioRawFrame):
|
async def _send_audio(self, audio: bytes):
|
||||||
message = {
|
message = {
|
||||||
'frames': base64.b64encode(frame.audio).decode("utf-8")
|
'frames': base64.b64encode(audio).decode("utf-8")
|
||||||
}
|
}
|
||||||
await self._websocket.send(json.dumps(message))
|
await self._websocket.send(json.dumps(message))
|
||||||
|
|
||||||
@@ -113,6 +106,6 @@ class GladiaSTTService(AsyncAIService):
|
|||||||
transcript = utterance["transcription"]
|
transcript = utterance["transcription"]
|
||||||
if confidence >= self._confidence:
|
if confidence >= self._confidence:
|
||||||
if type == "final":
|
if type == "final":
|
||||||
await self.queue_frame(TranscriptionFrame(transcript, "", time_now_iso8601()))
|
await self.push_frame(TranscriptionFrame(transcript, "", time_now_iso8601()))
|
||||||
else:
|
else:
|
||||||
await self.queue_frame(InterimTranscriptionFrame(transcript, "", time_now_iso8601()))
|
await self.push_frame(InterimTranscriptionFrame(transcript, "", time_now_iso8601()))
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class LmntTTSService(AsyncTTSService):
|
|||||||
**kwargs):
|
**kwargs):
|
||||||
# Let TTSService produce TTSStoppedFrames after a short delay of
|
# Let TTSService produce TTSStoppedFrames after a short delay of
|
||||||
# no activity.
|
# no activity.
|
||||||
super().__init__(push_stop_frames=True, sample_rate=sample_rate, **kwargs)
|
super().__init__(sync=False, push_stop_frames=True, sample_rate=sample_rate, **kwargs)
|
||||||
|
|
||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
self._voice_id = voice_id
|
self._voice_id = voice_id
|
||||||
|
|||||||
@@ -31,16 +31,12 @@ from loguru import logger
|
|||||||
class BaseInputTransport(FrameProcessor):
|
class BaseInputTransport(FrameProcessor):
|
||||||
|
|
||||||
def __init__(self, params: TransportParams, **kwargs):
|
def __init__(self, params: TransportParams, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(sync=False, **kwargs)
|
||||||
|
|
||||||
self._params = params
|
self._params = params
|
||||||
|
|
||||||
self._executor = ThreadPoolExecutor(max_workers=5)
|
self._executor = ThreadPoolExecutor(max_workers=5)
|
||||||
|
|
||||||
# Create push frame task. This is the task that will push frames in
|
|
||||||
# order. We also guarantee that all frames are pushed in the same task.
|
|
||||||
self._create_push_task()
|
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
# Create audio input queue and task if needed.
|
# Create audio input queue and task if needed.
|
||||||
if self._params.audio_in_enabled or self._params.vad_enabled:
|
if self._params.audio_in_enabled or self._params.vad_enabled:
|
||||||
@@ -53,10 +49,6 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
self._audio_task.cancel()
|
self._audio_task.cancel()
|
||||||
await self._audio_task
|
await self._audio_task
|
||||||
|
|
||||||
# Wait for the push frame task to finish. It will finish when the
|
|
||||||
# EndFrame is actually processed.
|
|
||||||
await self._push_frame_task
|
|
||||||
|
|
||||||
async def cancel(self, frame: CancelFrame):
|
async def cancel(self, frame: CancelFrame):
|
||||||
# Cancel all the tasks and wait for them to finish.
|
# Cancel all the tasks and wait for them to finish.
|
||||||
|
|
||||||
@@ -64,9 +56,6 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
self._audio_task.cancel()
|
self._audio_task.cancel()
|
||||||
await self._audio_task
|
await self._audio_task
|
||||||
|
|
||||||
self._push_frame_task.cancel()
|
|
||||||
await self._push_frame_task
|
|
||||||
|
|
||||||
def vad_analyzer(self) -> VADAnalyzer | None:
|
def vad_analyzer(self) -> VADAnalyzer | None:
|
||||||
return self._params.vad_analyzer
|
return self._params.vad_analyzer
|
||||||
|
|
||||||
@@ -86,11 +75,8 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
await self.cancel(frame)
|
await self.cancel(frame)
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
elif isinstance(frame, BotInterruptionFrame):
|
elif isinstance(frame, BotInterruptionFrame):
|
||||||
await self._handle_interruptions(frame, False)
|
logger.debug("Bot interruption")
|
||||||
elif isinstance(frame, StartInterruptionFrame):
|
|
||||||
await self._start_interruption()
|
await self._start_interruption()
|
||||||
elif isinstance(frame, StopInterruptionFrame):
|
|
||||||
await self._stop_interruption()
|
|
||||||
# All other system frames
|
# All other system frames
|
||||||
elif isinstance(frame, SystemFrame):
|
elif isinstance(frame, SystemFrame):
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
@@ -98,12 +84,12 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
elif isinstance(frame, StartFrame):
|
elif isinstance(frame, StartFrame):
|
||||||
# Push StartFrame before start(), because we want StartFrame to be
|
# Push StartFrame before start(), because we want StartFrame to be
|
||||||
# processed by every processor before any other frame is processed.
|
# processed by every processor before any other frame is processed.
|
||||||
await self._internal_push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
await self.start(frame)
|
await self.start(frame)
|
||||||
elif isinstance(frame, EndFrame):
|
elif isinstance(frame, EndFrame):
|
||||||
# Push EndFrame before stop(), because stop() waits on the task to
|
# Push EndFrame before stop(), because stop() waits on the task to
|
||||||
# finish and the task finishes when EndFrame is processed.
|
# finish and the task finishes when EndFrame is processed.
|
||||||
await self._internal_push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
await self.stop(frame)
|
await self.stop(frame)
|
||||||
elif isinstance(frame, VADParamsUpdateFrame):
|
elif isinstance(frame, VADParamsUpdateFrame):
|
||||||
vad_analyzer = self.vad_analyzer()
|
vad_analyzer = self.vad_analyzer()
|
||||||
@@ -111,73 +97,28 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
vad_analyzer.set_params(frame.params)
|
vad_analyzer.set_params(frame.params)
|
||||||
# Other frames
|
# Other frames
|
||||||
else:
|
else:
|
||||||
await self._internal_push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
#
|
|
||||||
# Push frames task
|
|
||||||
#
|
|
||||||
|
|
||||||
def _create_push_task(self):
|
|
||||||
loop = self.get_event_loop()
|
|
||||||
self._push_queue = asyncio.Queue()
|
|
||||||
self._push_frame_task = loop.create_task(self._push_frame_task_handler())
|
|
||||||
|
|
||||||
async def _internal_push_frame(
|
|
||||||
self,
|
|
||||||
frame: Frame | None,
|
|
||||||
direction: FrameDirection | None = FrameDirection.DOWNSTREAM):
|
|
||||||
await self._push_queue.put((frame, direction))
|
|
||||||
|
|
||||||
async def _push_frame_task_handler(self):
|
|
||||||
running = True
|
|
||||||
while running:
|
|
||||||
try:
|
|
||||||
(frame, direction) = await self._push_queue.get()
|
|
||||||
await self.push_frame(frame, direction)
|
|
||||||
running = not isinstance(frame, EndFrame)
|
|
||||||
self._push_queue.task_done()
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
break
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Handle interruptions
|
# Handle interruptions
|
||||||
#
|
#
|
||||||
|
|
||||||
async def _start_interruption(self):
|
async def _handle_interruptions(self, frame: Frame):
|
||||||
if not self.interruptions_allowed:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Cancel the task. This will stop pushing frames downstream.
|
|
||||||
self._push_frame_task.cancel()
|
|
||||||
await self._push_frame_task
|
|
||||||
# Push an out-of-band frame (i.e. not using the ordered push
|
|
||||||
# frame task) to stop everything, specially at the output
|
|
||||||
# transport.
|
|
||||||
await self.push_frame(StartInterruptionFrame())
|
|
||||||
# Create a new queue and task.
|
|
||||||
self._create_push_task()
|
|
||||||
|
|
||||||
async def _stop_interruption(self):
|
|
||||||
if not self.interruptions_allowed:
|
|
||||||
return
|
|
||||||
|
|
||||||
await self.push_frame(StopInterruptionFrame())
|
|
||||||
|
|
||||||
async def _handle_interruptions(self, frame: Frame, push_frame: bool):
|
|
||||||
if self.interruptions_allowed:
|
if self.interruptions_allowed:
|
||||||
# Make sure we notify about interruptions quickly out-of-band
|
# Make sure we notify about interruptions quickly out-of-band.
|
||||||
if isinstance(frame, BotInterruptionFrame):
|
if isinstance(frame, UserStartedSpeakingFrame):
|
||||||
logger.debug("Bot interruption")
|
|
||||||
await self._start_interruption()
|
|
||||||
elif isinstance(frame, UserStartedSpeakingFrame):
|
|
||||||
logger.debug("User started speaking")
|
logger.debug("User started speaking")
|
||||||
await self._start_interruption()
|
await self._start_interruption()
|
||||||
|
# Push an out-of-band frame (i.e. not using the ordered push
|
||||||
|
# frame task) to stop everything, specially at the output
|
||||||
|
# transport.
|
||||||
|
await self.push_frame(StartInterruptionFrame())
|
||||||
elif isinstance(frame, UserStoppedSpeakingFrame):
|
elif isinstance(frame, UserStoppedSpeakingFrame):
|
||||||
logger.debug("User stopped speaking")
|
logger.debug("User stopped speaking")
|
||||||
await self._stop_interruption()
|
await self._stop_interruption()
|
||||||
|
await self.push_frame(StopInterruptionFrame())
|
||||||
|
|
||||||
if push_frame:
|
await self.push_frame(frame)
|
||||||
await self._internal_push_frame(frame)
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Audio input
|
# Audio input
|
||||||
@@ -201,7 +142,7 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
frame = UserStoppedSpeakingFrame()
|
frame = UserStoppedSpeakingFrame()
|
||||||
|
|
||||||
if frame:
|
if frame:
|
||||||
await self._handle_interruptions(frame, True)
|
await self._handle_interruptions(frame)
|
||||||
|
|
||||||
vad_state = new_vad_state
|
vad_state = new_vad_state
|
||||||
return vad_state
|
return vad_state
|
||||||
@@ -222,7 +163,7 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
|
|
||||||
# Push audio downstream if passthrough.
|
# Push audio downstream if passthrough.
|
||||||
if audio_passthrough:
|
if audio_passthrough:
|
||||||
await self._internal_push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|
||||||
self._audio_in_queue.task_done()
|
self._audio_in_queue.task_done()
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ from pipecat.utils.time import nanoseconds_to_seconds
|
|||||||
class BaseOutputTransport(FrameProcessor):
|
class BaseOutputTransport(FrameProcessor):
|
||||||
|
|
||||||
def __init__(self, params: TransportParams, **kwargs):
|
def __init__(self, params: TransportParams, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(sync=False, **kwargs)
|
||||||
|
|
||||||
self._params = params
|
self._params = params
|
||||||
|
|
||||||
@@ -70,10 +70,6 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
# generating frames upstream while, for example, the audio is playing.
|
# generating frames upstream while, for example, the audio is playing.
|
||||||
self._create_sink_tasks()
|
self._create_sink_tasks()
|
||||||
|
|
||||||
# Create push frame task. This is the task that will push frames in
|
|
||||||
# order. We also guarantee that all frames are pushed in the same task.
|
|
||||||
self._create_push_task()
|
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
# Create camera output queue and task if needed.
|
# Create camera output queue and task if needed.
|
||||||
if self._params.camera_out_enabled:
|
if self._params.camera_out_enabled:
|
||||||
@@ -95,9 +91,8 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
self._audio_out_task.cancel()
|
self._audio_out_task.cancel()
|
||||||
await self._audio_out_task
|
await self._audio_out_task
|
||||||
|
|
||||||
# Wait for the push frame and sink tasks to finish. They will finish when
|
# Wait for the sink task to finish. They will finish when the EndFrame
|
||||||
# the EndFrame is actually processed.
|
# is actually processed.
|
||||||
await self._push_frame_task
|
|
||||||
await self._sink_task
|
await self._sink_task
|
||||||
|
|
||||||
async def cancel(self, frame: CancelFrame):
|
async def cancel(self, frame: CancelFrame):
|
||||||
@@ -107,9 +102,6 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
self._camera_out_task.cancel()
|
self._camera_out_task.cancel()
|
||||||
await self._camera_out_task
|
await self._camera_out_task
|
||||||
|
|
||||||
self._push_frame_task.cancel()
|
|
||||||
await self._push_frame_task
|
|
||||||
|
|
||||||
self._sink_task.cancel()
|
self._sink_task.cancel()
|
||||||
await self._sink_task
|
await self._sink_task
|
||||||
|
|
||||||
@@ -182,10 +174,6 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
await self._sink_clock_task
|
await self._sink_clock_task
|
||||||
# Create sink tasks.
|
# Create sink tasks.
|
||||||
self._create_sink_tasks()
|
self._create_sink_tasks()
|
||||||
# Stop push task.
|
|
||||||
self._push_frame_task.cancel()
|
|
||||||
await self._push_frame_task
|
|
||||||
self._create_push_task()
|
|
||||||
# Let's send a bot stopped speaking if we have to.
|
# Let's send a bot stopped speaking if we have to.
|
||||||
if self._bot_speaking:
|
if self._bot_speaking:
|
||||||
await self._bot_stopped_speaking()
|
await self._bot_stopped_speaking()
|
||||||
@@ -227,7 +215,7 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
async def _sink_frame_handler(self, frame: Frame):
|
async def _sink_frame_handler(self, frame: Frame):
|
||||||
if isinstance(frame, AudioRawFrame):
|
if isinstance(frame, AudioRawFrame):
|
||||||
await self.write_raw_audio_frames(frame.audio)
|
await self.write_raw_audio_frames(frame.audio)
|
||||||
await self._internal_push_frame(frame)
|
await self.push_frame(frame)
|
||||||
await self.push_frame(BotSpeakingFrame(), FrameDirection.UPSTREAM)
|
await self.push_frame(BotSpeakingFrame(), FrameDirection.UPSTREAM)
|
||||||
elif isinstance(frame, ImageRawFrame):
|
elif isinstance(frame, ImageRawFrame):
|
||||||
await self._set_camera_image(frame)
|
await self._set_camera_image(frame)
|
||||||
@@ -237,12 +225,12 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
await self.send_message(frame)
|
await self.send_message(frame)
|
||||||
elif isinstance(frame, TTSStartedFrame):
|
elif isinstance(frame, TTSStartedFrame):
|
||||||
await self._bot_started_speaking()
|
await self._bot_started_speaking()
|
||||||
await self._internal_push_frame(frame)
|
await self.push_frame(frame)
|
||||||
elif isinstance(frame, TTSStoppedFrame):
|
elif isinstance(frame, TTSStoppedFrame):
|
||||||
await self._bot_stopped_speaking()
|
await self._bot_stopped_speaking()
|
||||||
await self._internal_push_frame(frame)
|
await self.push_frame(frame)
|
||||||
else:
|
else:
|
||||||
await self._internal_push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|
||||||
async def _sink_task_handler(self):
|
async def _sink_task_handler(self):
|
||||||
running = True
|
running = True
|
||||||
@@ -261,7 +249,7 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
# TODO(aleix): For now we just process TextFrame. But we should process
|
# TODO(aleix): For now we just process TextFrame. But we should process
|
||||||
# audio and video as well.
|
# audio and video as well.
|
||||||
if isinstance(frame, TextFrame):
|
if isinstance(frame, TextFrame):
|
||||||
await self._internal_push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|
||||||
async def _sink_clock_task_handler(self):
|
async def _sink_clock_task_handler(self):
|
||||||
running = True
|
running = True
|
||||||
@@ -293,38 +281,12 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
async def _bot_started_speaking(self):
|
async def _bot_started_speaking(self):
|
||||||
logger.debug("Bot started speaking")
|
logger.debug("Bot started speaking")
|
||||||
self._bot_speaking = True
|
self._bot_speaking = True
|
||||||
await self._internal_push_frame(BotStartedSpeakingFrame(), FrameDirection.UPSTREAM)
|
await self.push_frame(BotStartedSpeakingFrame(), FrameDirection.UPSTREAM)
|
||||||
|
|
||||||
async def _bot_stopped_speaking(self):
|
async def _bot_stopped_speaking(self):
|
||||||
logger.debug("Bot stopped speaking")
|
logger.debug("Bot stopped speaking")
|
||||||
self._bot_speaking = False
|
self._bot_speaking = False
|
||||||
await self._internal_push_frame(BotStoppedSpeakingFrame(), FrameDirection.UPSTREAM)
|
await self.push_frame(BotStoppedSpeakingFrame(), FrameDirection.UPSTREAM)
|
||||||
|
|
||||||
#
|
|
||||||
# Push frames task
|
|
||||||
#
|
|
||||||
|
|
||||||
def _create_push_task(self):
|
|
||||||
loop = self.get_event_loop()
|
|
||||||
self._push_queue = asyncio.Queue()
|
|
||||||
self._push_frame_task = loop.create_task(self._push_frame_task_handler())
|
|
||||||
|
|
||||||
async def _internal_push_frame(
|
|
||||||
self,
|
|
||||||
frame: Frame | None,
|
|
||||||
direction: FrameDirection | None = FrameDirection.DOWNSTREAM):
|
|
||||||
await self._push_queue.put((frame, direction))
|
|
||||||
|
|
||||||
async def _push_frame_task_handler(self):
|
|
||||||
running = True
|
|
||||||
while running:
|
|
||||||
try:
|
|
||||||
(frame, direction) = await self._push_queue.get()
|
|
||||||
await self.push_frame(frame, direction)
|
|
||||||
running = not isinstance(frame, EndFrame)
|
|
||||||
self._push_queue.task_done()
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
break
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Camera out
|
# Camera out
|
||||||
@@ -408,7 +370,7 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
try:
|
try:
|
||||||
frame = await self._audio_out_queue.get()
|
frame = await self._audio_out_queue.get()
|
||||||
await self.write_raw_audio_frames(frame.audio)
|
await self.write_raw_audio_frames(frame.audio)
|
||||||
await self._internal_push_frame(frame)
|
await self.push_frame(frame)
|
||||||
await self.push_frame(BotSpeakingFrame(), FrameDirection.UPSTREAM)
|
await self.push_frame(BotSpeakingFrame(), FrameDirection.UPSTREAM)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -98,9 +98,9 @@ class WebsocketServerInputTransport(BaseInputTransport):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(frame, AudioRawFrame):
|
if isinstance(frame, AudioRawFrame):
|
||||||
await self.push_audio_frame(frame)
|
await self.queue_audio_frame(frame)
|
||||||
else:
|
else:
|
||||||
await self._internal_push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|
||||||
# Notify disconnection
|
# Notify disconnection
|
||||||
await self._callbacks.on_client_disconnected(websocket)
|
await self._callbacks.on_client_disconnected(websocket)
|
||||||
|
|||||||
@@ -625,11 +625,11 @@ class DailyInputTransport(BaseInputTransport):
|
|||||||
#
|
#
|
||||||
|
|
||||||
async def push_transcription_frame(self, frame: TranscriptionFrame | InterimTranscriptionFrame):
|
async def push_transcription_frame(self, frame: TranscriptionFrame | InterimTranscriptionFrame):
|
||||||
await self._internal_push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|
||||||
async def push_app_message(self, message: Any, sender: str):
|
async def push_app_message(self, message: Any, sender: str):
|
||||||
frame = DailyTransportMessageFrame(message=message, participant_id=sender)
|
frame = DailyTransportMessageFrame(message=message, participant_id=sender)
|
||||||
await self._internal_push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Audio in
|
# Audio in
|
||||||
@@ -692,7 +692,7 @@ class DailyInputTransport(BaseInputTransport):
|
|||||||
image=buffer,
|
image=buffer,
|
||||||
size=size,
|
size=size,
|
||||||
format=format)
|
format=format)
|
||||||
await self._internal_push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|
||||||
self._video_renderers[participant_id]["timestamp"] = curr_time
|
self._video_renderers[participant_id]["timestamp"] = curr_time
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user