165 lines
5.9 KiB
Python
165 lines
5.9 KiB
Python
#
|
|
# Copyright (c) 2024, Daily
|
|
#
|
|
# SPDX-License-Identifier: BSD 2-Clause License
|
|
#
|
|
|
|
import asyncio
|
|
|
|
from dataclasses import dataclass
|
|
from itertools import chain
|
|
from typing import List
|
|
|
|
from pipecat.frames.frames import ControlFrame, EndFrame, Frame, SystemFrame
|
|
from pipecat.pipeline.base_pipeline import BasePipeline
|
|
from pipecat.pipeline.pipeline import Pipeline
|
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
|
|
|
from loguru import logger
|
|
|
|
|
|
@dataclass
|
|
class SyncFrame(ControlFrame):
|
|
"""This frame is used to know when the internal pipelines have finished."""
|
|
|
|
pass
|
|
|
|
|
|
class Source(FrameProcessor):
|
|
def __init__(self, upstream_queue: asyncio.Queue):
|
|
super().__init__()
|
|
self._up_queue = upstream_queue
|
|
|
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
await super().process_frame(frame, direction)
|
|
|
|
match direction:
|
|
case FrameDirection.UPSTREAM:
|
|
await self._up_queue.put(frame)
|
|
case FrameDirection.DOWNSTREAM:
|
|
await self.push_frame(frame, direction)
|
|
|
|
|
|
class Sink(FrameProcessor):
|
|
def __init__(self, downstream_queue: asyncio.Queue):
|
|
super().__init__()
|
|
self._down_queue = downstream_queue
|
|
|
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
await super().process_frame(frame, direction)
|
|
|
|
match direction:
|
|
case FrameDirection.UPSTREAM:
|
|
await self.push_frame(frame, direction)
|
|
case FrameDirection.DOWNSTREAM:
|
|
await self._down_queue.put(frame)
|
|
|
|
|
|
class SyncParallelPipeline(BasePipeline):
|
|
def __init__(self, *args):
|
|
super().__init__()
|
|
|
|
if len(args) == 0:
|
|
raise Exception(f"SyncParallelPipeline needs at least one argument")
|
|
|
|
self._sinks = []
|
|
self._sources = []
|
|
self._pipelines = []
|
|
|
|
self._up_queue = asyncio.Queue()
|
|
self._down_queue = asyncio.Queue()
|
|
|
|
logger.debug(f"Creating {self} pipelines")
|
|
for processors in args:
|
|
if not isinstance(processors, list):
|
|
raise TypeError(f"SyncParallelPipeline argument {processors} is not a list")
|
|
|
|
# We add a source at the beginning of the pipeline and a sink at the end.
|
|
up_queue = asyncio.Queue()
|
|
down_queue = asyncio.Queue()
|
|
source = Source(up_queue)
|
|
sink = Sink(down_queue)
|
|
processors: List[FrameProcessor] = [source] + processors + [sink]
|
|
|
|
# Keep track of sources and sinks. We also keep the output queue of
|
|
# the source and the sinks so we can use it later.
|
|
self._sources.append({"processor": source, "queue": down_queue})
|
|
self._sinks.append({"processor": sink, "queue": up_queue})
|
|
|
|
# Create pipeline
|
|
pipeline = Pipeline(processors)
|
|
self._pipelines.append(pipeline)
|
|
logger.debug(f"Finished creating {self} pipelines")
|
|
|
|
#
|
|
# BasePipeline
|
|
#
|
|
|
|
def processors_with_metrics(self) -> List[FrameProcessor]:
|
|
return list(chain.from_iterable(p.processors_with_metrics() for p in self._pipelines))
|
|
|
|
#
|
|
# Frame processor
|
|
#
|
|
|
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
await super().process_frame(frame, direction)
|
|
|
|
# The last processor of each pipeline needs to be synchronous otherwise
|
|
# this element won't work. Since, we know it should be synchronous we
|
|
# push a SyncFrame. Since frames are ordered we know this frame will be
|
|
# pushed after the synchronous processor has pushed its data allowing us
|
|
# to synchrnonize all the internal pipelines by waiting for the
|
|
# SyncFrame in all of them.
|
|
async def wait_for_sync(
|
|
obj, main_queue: asyncio.Queue, frame: Frame, direction: FrameDirection
|
|
):
|
|
processor = obj["processor"]
|
|
queue = obj["queue"]
|
|
|
|
await processor.process_frame(frame, direction)
|
|
|
|
if isinstance(frame, (SystemFrame, EndFrame)):
|
|
new_frame = await queue.get()
|
|
if isinstance(new_frame, (SystemFrame, EndFrame)):
|
|
await main_queue.put(new_frame)
|
|
else:
|
|
while not isinstance(new_frame, (SystemFrame, EndFrame)):
|
|
await main_queue.put(new_frame)
|
|
queue.task_done()
|
|
new_frame = await queue.get()
|
|
else:
|
|
await processor.process_frame(SyncFrame(), direction)
|
|
new_frame = await queue.get()
|
|
while not isinstance(new_frame, SyncFrame):
|
|
await main_queue.put(new_frame)
|
|
queue.task_done()
|
|
new_frame = await queue.get()
|
|
|
|
if direction == FrameDirection.UPSTREAM:
|
|
# If we get an upstream frame we process it in each sink.
|
|
await asyncio.gather(
|
|
*[wait_for_sync(s, self._up_queue, frame, direction) for s in self._sinks]
|
|
)
|
|
elif direction == FrameDirection.DOWNSTREAM:
|
|
# If we get a downstream frame we process it in each source.
|
|
await asyncio.gather(
|
|
*[wait_for_sync(s, self._down_queue, frame, direction) for s in self._sources]
|
|
)
|
|
|
|
seen_ids = set()
|
|
while not self._up_queue.empty():
|
|
frame = await self._up_queue.get()
|
|
if frame.id not in seen_ids:
|
|
await self.push_frame(frame, FrameDirection.UPSTREAM)
|
|
seen_ids.add(frame.id)
|
|
self._up_queue.task_done()
|
|
|
|
seen_ids = set()
|
|
while not self._down_queue.empty():
|
|
frame = await self._down_queue.get()
|
|
if frame.id not in seen_ids:
|
|
await self.push_frame(frame, FrameDirection.DOWNSTREAM)
|
|
seen_ids.add(frame.id)
|
|
self._down_queue.task_done()
|