dv-pipecat-ai 0.0.74.dev770__py3-none-any.whl → 0.0.82.dev776__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dv-pipecat-ai might be problematic. Click here for more details.
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/METADATA +137 -93
- dv_pipecat_ai-0.0.82.dev776.dist-info/RECORD +340 -0
- pipecat/__init__.py +17 -0
- pipecat/adapters/base_llm_adapter.py +36 -1
- pipecat/adapters/schemas/direct_function.py +296 -0
- pipecat/adapters/schemas/function_schema.py +15 -6
- pipecat/adapters/schemas/tools_schema.py +55 -7
- pipecat/adapters/services/anthropic_adapter.py +22 -3
- pipecat/adapters/services/aws_nova_sonic_adapter.py +23 -3
- pipecat/adapters/services/bedrock_adapter.py +22 -3
- pipecat/adapters/services/gemini_adapter.py +16 -3
- pipecat/adapters/services/open_ai_adapter.py +17 -2
- pipecat/adapters/services/open_ai_realtime_adapter.py +23 -3
- pipecat/audio/filters/base_audio_filter.py +30 -6
- pipecat/audio/filters/koala_filter.py +37 -2
- pipecat/audio/filters/krisp_filter.py +59 -6
- pipecat/audio/filters/noisereduce_filter.py +37 -0
- pipecat/audio/interruptions/base_interruption_strategy.py +25 -5
- pipecat/audio/interruptions/min_words_interruption_strategy.py +21 -4
- pipecat/audio/mixers/base_audio_mixer.py +30 -7
- pipecat/audio/mixers/soundfile_mixer.py +53 -6
- pipecat/audio/resamplers/base_audio_resampler.py +17 -9
- pipecat/audio/resamplers/resampy_resampler.py +26 -1
- pipecat/audio/resamplers/soxr_resampler.py +32 -1
- pipecat/audio/resamplers/soxr_stream_resampler.py +101 -0
- pipecat/audio/utils.py +194 -1
- pipecat/audio/vad/silero.py +60 -3
- pipecat/audio/vad/vad_analyzer.py +114 -30
- pipecat/clocks/base_clock.py +19 -0
- pipecat/clocks/system_clock.py +25 -0
- pipecat/extensions/voicemail/__init__.py +0 -0
- pipecat/extensions/voicemail/voicemail_detector.py +707 -0
- pipecat/frames/frames.py +590 -156
- pipecat/metrics/metrics.py +64 -1
- pipecat/observers/base_observer.py +58 -19
- pipecat/observers/loggers/debug_log_observer.py +56 -64
- pipecat/observers/loggers/llm_log_observer.py +8 -1
- pipecat/observers/loggers/transcription_log_observer.py +19 -7
- pipecat/observers/loggers/user_bot_latency_log_observer.py +32 -5
- pipecat/observers/turn_tracking_observer.py +26 -1
- pipecat/pipeline/base_pipeline.py +5 -7
- pipecat/pipeline/base_task.py +52 -9
- pipecat/pipeline/parallel_pipeline.py +121 -177
- pipecat/pipeline/pipeline.py +129 -20
- pipecat/pipeline/runner.py +50 -1
- pipecat/pipeline/sync_parallel_pipeline.py +132 -32
- pipecat/pipeline/task.py +263 -280
- pipecat/pipeline/task_observer.py +85 -34
- pipecat/pipeline/to_be_updated/merge_pipeline.py +32 -2
- pipecat/processors/aggregators/dtmf_aggregator.py +29 -22
- pipecat/processors/aggregators/gated.py +25 -24
- pipecat/processors/aggregators/gated_openai_llm_context.py +22 -2
- pipecat/processors/aggregators/llm_response.py +398 -89
- pipecat/processors/aggregators/openai_llm_context.py +161 -13
- pipecat/processors/aggregators/sentence.py +25 -14
- pipecat/processors/aggregators/user_response.py +28 -3
- pipecat/processors/aggregators/vision_image_frame.py +24 -14
- pipecat/processors/async_generator.py +28 -0
- pipecat/processors/audio/audio_buffer_processor.py +78 -37
- pipecat/processors/consumer_processor.py +25 -6
- pipecat/processors/filters/frame_filter.py +23 -0
- pipecat/processors/filters/function_filter.py +30 -0
- pipecat/processors/filters/identity_filter.py +17 -2
- pipecat/processors/filters/null_filter.py +24 -1
- pipecat/processors/filters/stt_mute_filter.py +56 -21
- pipecat/processors/filters/wake_check_filter.py +46 -3
- pipecat/processors/filters/wake_notifier_filter.py +21 -3
- pipecat/processors/frame_processor.py +488 -131
- pipecat/processors/frameworks/langchain.py +38 -3
- pipecat/processors/frameworks/rtvi.py +719 -34
- pipecat/processors/gstreamer/pipeline_source.py +41 -0
- pipecat/processors/idle_frame_processor.py +26 -3
- pipecat/processors/logger.py +23 -0
- pipecat/processors/metrics/frame_processor_metrics.py +77 -4
- pipecat/processors/metrics/sentry.py +42 -4
- pipecat/processors/producer_processor.py +34 -14
- pipecat/processors/text_transformer.py +22 -10
- pipecat/processors/transcript_processor.py +48 -29
- pipecat/processors/user_idle_processor.py +31 -21
- pipecat/runner/__init__.py +1 -0
- pipecat/runner/daily.py +132 -0
- pipecat/runner/livekit.py +148 -0
- pipecat/runner/run.py +543 -0
- pipecat/runner/types.py +67 -0
- pipecat/runner/utils.py +515 -0
- pipecat/serializers/base_serializer.py +42 -0
- pipecat/serializers/exotel.py +17 -6
- pipecat/serializers/genesys.py +95 -0
- pipecat/serializers/livekit.py +33 -0
- pipecat/serializers/plivo.py +16 -15
- pipecat/serializers/protobuf.py +37 -1
- pipecat/serializers/telnyx.py +18 -17
- pipecat/serializers/twilio.py +32 -16
- pipecat/services/ai_service.py +5 -3
- pipecat/services/anthropic/llm.py +113 -43
- pipecat/services/assemblyai/models.py +63 -5
- pipecat/services/assemblyai/stt.py +64 -11
- pipecat/services/asyncai/__init__.py +0 -0
- pipecat/services/asyncai/tts.py +501 -0
- pipecat/services/aws/llm.py +185 -111
- pipecat/services/aws/stt.py +217 -23
- pipecat/services/aws/tts.py +118 -52
- pipecat/services/aws/utils.py +101 -5
- pipecat/services/aws_nova_sonic/aws.py +82 -64
- pipecat/services/aws_nova_sonic/context.py +15 -6
- pipecat/services/azure/common.py +10 -2
- pipecat/services/azure/image.py +32 -0
- pipecat/services/azure/llm.py +9 -7
- pipecat/services/azure/stt.py +65 -2
- pipecat/services/azure/tts.py +154 -23
- pipecat/services/cartesia/stt.py +125 -8
- pipecat/services/cartesia/tts.py +102 -38
- pipecat/services/cerebras/llm.py +15 -23
- pipecat/services/deepgram/stt.py +19 -11
- pipecat/services/deepgram/tts.py +36 -0
- pipecat/services/deepseek/llm.py +14 -23
- pipecat/services/elevenlabs/tts.py +330 -64
- pipecat/services/fal/image.py +43 -0
- pipecat/services/fal/stt.py +48 -10
- pipecat/services/fireworks/llm.py +14 -21
- pipecat/services/fish/tts.py +109 -9
- pipecat/services/gemini_multimodal_live/__init__.py +1 -0
- pipecat/services/gemini_multimodal_live/events.py +83 -2
- pipecat/services/gemini_multimodal_live/file_api.py +189 -0
- pipecat/services/gemini_multimodal_live/gemini.py +218 -21
- pipecat/services/gladia/config.py +17 -10
- pipecat/services/gladia/stt.py +82 -36
- pipecat/services/google/frames.py +40 -0
- pipecat/services/google/google.py +2 -0
- pipecat/services/google/image.py +39 -2
- pipecat/services/google/llm.py +176 -58
- pipecat/services/google/llm_openai.py +26 -4
- pipecat/services/google/llm_vertex.py +37 -15
- pipecat/services/google/rtvi.py +41 -0
- pipecat/services/google/stt.py +65 -17
- pipecat/services/google/test-google-chirp.py +45 -0
- pipecat/services/google/tts.py +390 -19
- pipecat/services/grok/llm.py +8 -6
- pipecat/services/groq/llm.py +8 -6
- pipecat/services/groq/stt.py +13 -9
- pipecat/services/groq/tts.py +40 -0
- pipecat/services/hamsa/__init__.py +9 -0
- pipecat/services/hamsa/stt.py +241 -0
- pipecat/services/heygen/__init__.py +5 -0
- pipecat/services/heygen/api.py +281 -0
- pipecat/services/heygen/client.py +620 -0
- pipecat/services/heygen/video.py +338 -0
- pipecat/services/image_service.py +5 -3
- pipecat/services/inworld/__init__.py +1 -0
- pipecat/services/inworld/tts.py +592 -0
- pipecat/services/llm_service.py +127 -45
- pipecat/services/lmnt/tts.py +80 -7
- pipecat/services/mcp_service.py +85 -44
- pipecat/services/mem0/memory.py +42 -13
- pipecat/services/minimax/tts.py +74 -15
- pipecat/services/mistral/__init__.py +0 -0
- pipecat/services/mistral/llm.py +185 -0
- pipecat/services/moondream/vision.py +55 -10
- pipecat/services/neuphonic/tts.py +275 -48
- pipecat/services/nim/llm.py +8 -6
- pipecat/services/ollama/llm.py +27 -7
- pipecat/services/openai/base_llm.py +54 -16
- pipecat/services/openai/image.py +30 -0
- pipecat/services/openai/llm.py +7 -5
- pipecat/services/openai/stt.py +13 -9
- pipecat/services/openai/tts.py +42 -10
- pipecat/services/openai_realtime_beta/azure.py +11 -9
- pipecat/services/openai_realtime_beta/context.py +7 -5
- pipecat/services/openai_realtime_beta/events.py +10 -7
- pipecat/services/openai_realtime_beta/openai.py +37 -18
- pipecat/services/openpipe/llm.py +30 -24
- pipecat/services/openrouter/llm.py +9 -7
- pipecat/services/perplexity/llm.py +15 -19
- pipecat/services/piper/tts.py +26 -12
- pipecat/services/playht/tts.py +227 -65
- pipecat/services/qwen/llm.py +8 -6
- pipecat/services/rime/tts.py +128 -17
- pipecat/services/riva/stt.py +160 -22
- pipecat/services/riva/tts.py +67 -2
- pipecat/services/sambanova/llm.py +19 -17
- pipecat/services/sambanova/stt.py +14 -8
- pipecat/services/sarvam/tts.py +60 -13
- pipecat/services/simli/video.py +82 -21
- pipecat/services/soniox/__init__.py +0 -0
- pipecat/services/soniox/stt.py +398 -0
- pipecat/services/speechmatics/stt.py +29 -17
- pipecat/services/stt_service.py +47 -11
- pipecat/services/tavus/video.py +94 -25
- pipecat/services/together/llm.py +8 -6
- pipecat/services/tts_service.py +77 -53
- pipecat/services/ultravox/stt.py +46 -43
- pipecat/services/vision_service.py +5 -3
- pipecat/services/websocket_service.py +12 -11
- pipecat/services/whisper/base_stt.py +58 -12
- pipecat/services/whisper/stt.py +69 -58
- pipecat/services/xtts/tts.py +59 -2
- pipecat/sync/base_notifier.py +19 -0
- pipecat/sync/event_notifier.py +24 -0
- pipecat/tests/utils.py +73 -5
- pipecat/transcriptions/language.py +24 -0
- pipecat/transports/base_input.py +112 -8
- pipecat/transports/base_output.py +235 -13
- pipecat/transports/base_transport.py +119 -0
- pipecat/transports/local/audio.py +76 -0
- pipecat/transports/local/tk.py +84 -0
- pipecat/transports/network/fastapi_websocket.py +174 -15
- pipecat/transports/network/small_webrtc.py +383 -39
- pipecat/transports/network/webrtc_connection.py +214 -8
- pipecat/transports/network/websocket_client.py +171 -1
- pipecat/transports/network/websocket_server.py +147 -9
- pipecat/transports/services/daily.py +792 -70
- pipecat/transports/services/helpers/daily_rest.py +122 -129
- pipecat/transports/services/livekit.py +339 -4
- pipecat/transports/services/tavus.py +273 -38
- pipecat/utils/asyncio/task_manager.py +92 -186
- pipecat/utils/base_object.py +83 -1
- pipecat/utils/network.py +2 -0
- pipecat/utils/string.py +114 -58
- pipecat/utils/text/base_text_aggregator.py +44 -13
- pipecat/utils/text/base_text_filter.py +46 -0
- pipecat/utils/text/markdown_text_filter.py +70 -14
- pipecat/utils/text/pattern_pair_aggregator.py +18 -14
- pipecat/utils/text/simple_text_aggregator.py +43 -2
- pipecat/utils/text/skip_tags_aggregator.py +21 -13
- pipecat/utils/time.py +36 -0
- pipecat/utils/tracing/class_decorators.py +32 -7
- pipecat/utils/tracing/conversation_context_provider.py +12 -2
- pipecat/utils/tracing/service_attributes.py +80 -64
- pipecat/utils/tracing/service_decorators.py +48 -21
- pipecat/utils/tracing/setup.py +13 -7
- pipecat/utils/tracing/turn_context_provider.py +12 -2
- pipecat/utils/tracing/turn_trace_observer.py +27 -0
- pipecat/utils/utils.py +14 -14
- dv_pipecat_ai-0.0.74.dev770.dist-info/RECORD +0 -319
- pipecat/examples/daily_runner.py +0 -64
- pipecat/examples/run.py +0 -265
- pipecat/utils/asyncio/watchdog_async_iterator.py +0 -72
- pipecat/utils/asyncio/watchdog_event.py +0 -42
- pipecat/utils/asyncio/watchdog_priority_queue.py +0 -48
- pipecat/utils/asyncio/watchdog_queue.py +0 -48
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/WHEEL +0 -0
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/licenses/LICENSE +0 -0
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/top_level.txt +0 -0
- /pipecat/{examples → extensions}/__init__.py +0 -0
pipecat/pipeline/base_task.py
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
5
|
#
|
|
6
6
|
|
|
7
|
+
"""Base pipeline task implementation for managing pipeline execution.
|
|
8
|
+
|
|
9
|
+
This module provides the abstract base class and configuration for pipeline
|
|
10
|
+
tasks that manage the lifecycle and execution of frame processing pipelines.
|
|
11
|
+
"""
|
|
12
|
+
|
|
7
13
|
import asyncio
|
|
8
14
|
from abc import abstractmethod
|
|
9
15
|
from dataclasses import dataclass
|
|
@@ -15,44 +21,81 @@ from pipecat.utils.base_object import BaseObject
|
|
|
15
21
|
|
|
16
22
|
@dataclass
|
|
17
23
|
class PipelineTaskParams:
|
|
18
|
-
"""
|
|
24
|
+
"""Configuration parameters for pipeline task execution.
|
|
25
|
+
|
|
26
|
+
Parameters:
|
|
27
|
+
loop: The asyncio event loop to use for task execution.
|
|
28
|
+
"""
|
|
19
29
|
|
|
20
30
|
loop: asyncio.AbstractEventLoop
|
|
21
31
|
|
|
22
32
|
|
|
23
33
|
class BasePipelineTask(BaseObject):
|
|
34
|
+
"""Abstract base class for pipeline task implementations.
|
|
35
|
+
|
|
36
|
+
Defines the interface for managing pipeline execution lifecycle,
|
|
37
|
+
including starting, stopping, and frame queuing operations.
|
|
38
|
+
"""
|
|
39
|
+
|
|
24
40
|
@abstractmethod
|
|
25
41
|
def has_finished(self) -> bool:
|
|
26
|
-
"""
|
|
27
|
-
have stopped.
|
|
42
|
+
"""Check if the pipeline task has finished execution.
|
|
28
43
|
|
|
44
|
+
Returns:
|
|
45
|
+
True if all processors have stopped and the task is complete.
|
|
29
46
|
"""
|
|
30
47
|
pass
|
|
31
48
|
|
|
32
49
|
@abstractmethod
|
|
33
50
|
async def stop_when_done(self):
|
|
34
|
-
"""
|
|
35
|
-
order to stop the task after everything in it has been processed.
|
|
51
|
+
"""Schedule the pipeline to stop after processing all queued frames.
|
|
36
52
|
|
|
53
|
+
Implementing classes should send an EndFrame or equivalent signal to
|
|
54
|
+
gracefully terminate the pipeline once all current processing is complete.
|
|
37
55
|
"""
|
|
38
56
|
pass
|
|
39
57
|
|
|
40
58
|
@abstractmethod
|
|
41
59
|
async def cancel(self):
|
|
42
|
-
"""
|
|
60
|
+
"""Immediately stop the running pipeline.
|
|
61
|
+
|
|
62
|
+
Implementing classes should cancel all running tasks and stop frame
|
|
63
|
+
processing without waiting for completion.
|
|
64
|
+
"""
|
|
43
65
|
pass
|
|
44
66
|
|
|
45
67
|
@abstractmethod
|
|
46
68
|
async def run(self, params: PipelineTaskParams):
|
|
47
|
-
"""
|
|
69
|
+
"""Start and run the pipeline with the given parameters.
|
|
70
|
+
|
|
71
|
+
Implementing classes should initialize and execute the pipeline using
|
|
72
|
+
the provided configuration parameters.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
params: Configuration parameters for pipeline execution.
|
|
76
|
+
"""
|
|
48
77
|
pass
|
|
49
78
|
|
|
50
79
|
@abstractmethod
|
|
51
80
|
async def queue_frame(self, frame: Frame):
|
|
52
|
-
"""Queue a frame
|
|
81
|
+
"""Queue a single frame for processing by the pipeline.
|
|
82
|
+
|
|
83
|
+
Implementing classes should add the frame to their processing queue
|
|
84
|
+
for downstream handling.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
frame: The frame to be processed.
|
|
88
|
+
"""
|
|
53
89
|
pass
|
|
54
90
|
|
|
55
91
|
@abstractmethod
|
|
56
92
|
async def queue_frames(self, frames: Iterable[Frame] | AsyncIterable[Frame]):
|
|
57
|
-
"""
|
|
93
|
+
"""Queue multiple frames for processing by the pipeline.
|
|
94
|
+
|
|
95
|
+
Implementing classes should process the iterable/async iterable and
|
|
96
|
+
add all frames to their processing queue.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
frames: An iterable or async iterable of frames to be processed.
|
|
100
|
+
"""
|
|
58
101
|
pass
|
|
@@ -4,227 +4,171 @@
|
|
|
4
4
|
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
5
|
#
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
"""Parallel pipeline implementation for concurrent frame processing.
|
|
8
|
+
|
|
9
|
+
This module provides a parallel pipeline that processes frames through multiple
|
|
10
|
+
sub-pipelines concurrently, with coordination for system frames and proper
|
|
11
|
+
handling of pipeline lifecycle events.
|
|
12
|
+
"""
|
|
13
|
+
|
|
8
14
|
from itertools import chain
|
|
9
|
-
from typing import
|
|
15
|
+
from typing import Dict, List
|
|
10
16
|
|
|
11
17
|
from loguru import logger
|
|
12
18
|
|
|
13
|
-
from pipecat.frames.frames import
|
|
14
|
-
CancelFrame,
|
|
15
|
-
EndFrame,
|
|
16
|
-
Frame,
|
|
17
|
-
StartFrame,
|
|
18
|
-
StartInterruptionFrame,
|
|
19
|
-
SystemFrame,
|
|
20
|
-
)
|
|
19
|
+
from pipecat.frames.frames import EndFrame, Frame, StartFrame
|
|
21
20
|
from pipecat.pipeline.base_pipeline import BasePipeline
|
|
22
|
-
from pipecat.pipeline.pipeline import Pipeline
|
|
21
|
+
from pipecat.pipeline.pipeline import Pipeline, PipelineSink, PipelineSource
|
|
23
22
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, FrameProcessorSetup
|
|
24
|
-
from pipecat.utils.asyncio.watchdog_queue import WatchdogQueue
|
|
25
23
|
|
|
26
24
|
|
|
27
|
-
class
|
|
28
|
-
|
|
29
|
-
self,
|
|
30
|
-
upstream_queue: asyncio.Queue,
|
|
31
|
-
push_frame_func: Callable[[Frame, FrameDirection], Awaitable[None]],
|
|
32
|
-
):
|
|
33
|
-
super().__init__()
|
|
34
|
-
self._up_queue = upstream_queue
|
|
35
|
-
self._push_frame_func = push_frame_func
|
|
36
|
-
|
|
37
|
-
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
38
|
-
await super().process_frame(frame, direction)
|
|
39
|
-
|
|
40
|
-
match direction:
|
|
41
|
-
case FrameDirection.UPSTREAM:
|
|
42
|
-
if isinstance(frame, SystemFrame):
|
|
43
|
-
await self._push_frame_func(frame, direction)
|
|
44
|
-
else:
|
|
45
|
-
await self._up_queue.put(frame)
|
|
46
|
-
case FrameDirection.DOWNSTREAM:
|
|
47
|
-
await self.push_frame(frame, direction)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class ParallelPipelineSink(FrameProcessor):
|
|
51
|
-
def __init__(
|
|
52
|
-
self,
|
|
53
|
-
downstream_queue: asyncio.Queue,
|
|
54
|
-
push_frame_func: Callable[[Frame, FrameDirection], Awaitable[None]],
|
|
55
|
-
):
|
|
56
|
-
super().__init__()
|
|
57
|
-
self._down_queue = downstream_queue
|
|
58
|
-
self._push_frame_func = push_frame_func
|
|
25
|
+
class ParallelPipeline(BasePipeline):
|
|
26
|
+
"""Pipeline that processes frames through multiple sub-pipelines concurrently.
|
|
59
27
|
|
|
60
|
-
|
|
61
|
-
|
|
28
|
+
Creates multiple parallel processing branches from the provided processor lists,
|
|
29
|
+
coordinating frame flow and ensuring proper synchronization of lifecycle events
|
|
30
|
+
like EndFrames. Each branch runs independently while system frames are handled
|
|
31
|
+
specially to maintain pipeline coordination.
|
|
32
|
+
"""
|
|
62
33
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
await self.push_frame(frame, direction)
|
|
66
|
-
case FrameDirection.DOWNSTREAM:
|
|
67
|
-
if isinstance(frame, SystemFrame):
|
|
68
|
-
await self._push_frame_func(frame, direction)
|
|
69
|
-
else:
|
|
70
|
-
await self._down_queue.put(frame)
|
|
34
|
+
def __init__(self, *args):
|
|
35
|
+
"""Initialize the parallel pipeline with processor lists.
|
|
71
36
|
|
|
37
|
+
Args:
|
|
38
|
+
*args: Variable number of processor lists, each becoming a parallel branch.
|
|
72
39
|
|
|
73
|
-
|
|
74
|
-
|
|
40
|
+
Raises:
|
|
41
|
+
Exception: If no processor lists are provided.
|
|
42
|
+
TypeError: If any argument is not a list of processors.
|
|
43
|
+
"""
|
|
44
|
+
# We don't set it to direct mode because we use frame pausing and that
|
|
45
|
+
# requires queues.
|
|
75
46
|
super().__init__()
|
|
76
47
|
|
|
77
48
|
if len(args) == 0:
|
|
78
49
|
raise Exception(f"ParallelPipeline needs at least one argument")
|
|
79
50
|
|
|
80
|
-
self._args = args
|
|
81
|
-
self._sources = []
|
|
82
|
-
self._sinks = []
|
|
83
51
|
self._pipelines = []
|
|
84
52
|
|
|
85
53
|
self._seen_ids = set()
|
|
86
|
-
self.
|
|
54
|
+
self._frame_counter: Dict[int, int] = {}
|
|
87
55
|
|
|
88
|
-
|
|
89
|
-
self._down_task = None
|
|
56
|
+
logger.debug(f"Creating {self} pipelines")
|
|
90
57
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
58
|
+
for processors in args:
|
|
59
|
+
if not isinstance(processors, list):
|
|
60
|
+
raise TypeError(f"ParallelPipeline argument {processors} is not a list")
|
|
94
61
|
|
|
95
|
-
|
|
96
|
-
|
|
62
|
+
num_pipelines = len(self._pipelines)
|
|
63
|
+
|
|
64
|
+
# We add a source before the pipeline and a sink after so we control
|
|
65
|
+
# the frames that are pushed upstream and downstream.
|
|
66
|
+
source = PipelineSource(
|
|
67
|
+
self._parallel_push_frame, name=f"{self}::Source{num_pipelines}"
|
|
68
|
+
)
|
|
69
|
+
sink = PipelineSink(self._pipeline_sink_push_frame, name=f"{self}::Sink{num_pipelines}")
|
|
70
|
+
|
|
71
|
+
# Create pipeline
|
|
72
|
+
pipeline = Pipeline(processors, source=source, sink=sink)
|
|
73
|
+
self._pipelines.append(pipeline)
|
|
74
|
+
|
|
75
|
+
logger.debug(f"Finished creating {self} pipelines")
|
|
97
76
|
|
|
98
77
|
#
|
|
99
78
|
# Frame processor
|
|
100
79
|
#
|
|
101
80
|
|
|
102
|
-
|
|
103
|
-
|
|
81
|
+
@property
|
|
82
|
+
def processors(self):
|
|
83
|
+
"""Return the list of sub-processors contained within this processor.
|
|
104
84
|
|
|
105
|
-
|
|
106
|
-
|
|
85
|
+
Only compound processors (e.g. pipelines and parallel pipelines) have
|
|
86
|
+
sub-processors. Non-compound processors will return an empty list.
|
|
107
87
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
88
|
+
Returns:
|
|
89
|
+
The list of sub-processors if this is a compound processor.
|
|
90
|
+
"""
|
|
91
|
+
return self._pipelines
|
|
112
92
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
self._sources.append(source)
|
|
117
|
-
self._sinks.append(sink)
|
|
93
|
+
@property
|
|
94
|
+
def entry_processors(self) -> List["FrameProcessor"]:
|
|
95
|
+
"""Return the list of entry processors for this processor.
|
|
118
96
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
self._pipelines.append(pipeline)
|
|
97
|
+
Entry processors are the first processors in a compound processor
|
|
98
|
+
(e.g. pipelines, parallel pipelines). Note that pipelines can also be an
|
|
99
|
+
entry processor as pipelines are processors themselves. Non-compound
|
|
100
|
+
processors will simply return an empty list.
|
|
124
101
|
|
|
125
|
-
|
|
102
|
+
Returns:
|
|
103
|
+
The list of entry processors.
|
|
104
|
+
"""
|
|
105
|
+
return self._pipelines
|
|
106
|
+
|
|
107
|
+
def processors_with_metrics(self) -> List[FrameProcessor]:
|
|
108
|
+
"""Collect processors that can generate metrics from all parallel branches.
|
|
126
109
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
110
|
+
Returns:
|
|
111
|
+
List of frame processors that support metrics collection from all branches.
|
|
112
|
+
"""
|
|
113
|
+
return list(chain.from_iterable(p.processors_with_metrics() for p in self._pipelines))
|
|
114
|
+
|
|
115
|
+
async def setup(self, setup: FrameProcessorSetup):
|
|
116
|
+
"""Set up the parallel pipeline and all its branches.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
setup: Configuration for frame processor setup.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
TypeError: If any processor list argument is not actually a list.
|
|
123
|
+
"""
|
|
124
|
+
await super().setup(setup)
|
|
125
|
+
for p in self._pipelines:
|
|
126
|
+
await p.setup(setup)
|
|
130
127
|
|
|
131
128
|
async def cleanup(self):
|
|
129
|
+
"""Clean up the parallel pipeline and all its branches."""
|
|
132
130
|
await super().cleanup()
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
await asyncio.gather(*[s.cleanup() for s in self._sinks])
|
|
131
|
+
for p in self._pipelines:
|
|
132
|
+
await p.cleanup()
|
|
136
133
|
|
|
137
134
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
135
|
+
"""Process frames through all parallel branches with lifecycle coordination.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
frame: The frame to process.
|
|
139
|
+
direction: The direction of frame flow.
|
|
140
|
+
"""
|
|
138
141
|
await super().process_frame(frame, direction)
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
self.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
await asyncio.gather(*[s.queue_frame(frame, direction) for s in self._sinks])
|
|
150
|
-
elif direction == FrameDirection.DOWNSTREAM:
|
|
151
|
-
# If we get a downstream frame we process it in each source.
|
|
152
|
-
await asyncio.gather(*[s.queue_frame(frame, direction) for s in self._sources])
|
|
153
|
-
|
|
154
|
-
# Handle interruptions after everything has been cancelled.
|
|
155
|
-
if isinstance(frame, StartInterruptionFrame):
|
|
156
|
-
await self._handle_interruption()
|
|
157
|
-
# Wait for tasks to finish.
|
|
158
|
-
elif isinstance(frame, EndFrame):
|
|
159
|
-
await self._stop()
|
|
160
|
-
|
|
161
|
-
async def _start(self, frame: StartFrame):
|
|
162
|
-
await self._create_tasks()
|
|
163
|
-
|
|
164
|
-
async def _stop(self):
|
|
165
|
-
if self._up_task:
|
|
166
|
-
# The up task doesn't receive an EndFrame, so we just cancel it.
|
|
167
|
-
await self.cancel_task(self._up_task)
|
|
168
|
-
self._up_task = None
|
|
169
|
-
|
|
170
|
-
if self._down_task:
|
|
171
|
-
# The down tasks waits for the last EndFrame sent by the internal
|
|
172
|
-
# pipelines.
|
|
173
|
-
await self._down_task
|
|
174
|
-
self._down_task = None
|
|
175
|
-
|
|
176
|
-
async def _cancel(self):
|
|
177
|
-
if self._up_task:
|
|
178
|
-
await self.cancel_task(self._up_task)
|
|
179
|
-
self._up_task = None
|
|
180
|
-
if self._down_task:
|
|
181
|
-
await self.cancel_task(self._down_task)
|
|
182
|
-
self._down_task = None
|
|
183
|
-
|
|
184
|
-
async def _create_tasks(self):
|
|
185
|
-
if not self._up_task:
|
|
186
|
-
self._up_task = self.create_task(self._process_up_queue())
|
|
187
|
-
if not self._down_task:
|
|
188
|
-
self._down_task = self.create_task(self._process_down_queue())
|
|
189
|
-
|
|
190
|
-
async def _drain_queues(self):
|
|
191
|
-
while not self._up_queue.empty:
|
|
192
|
-
await self._up_queue.get()
|
|
193
|
-
while not self._down_queue.empty:
|
|
194
|
-
await self._down_queue.get()
|
|
195
|
-
|
|
196
|
-
async def _handle_interruption(self):
|
|
197
|
-
await self._cancel()
|
|
198
|
-
await self._drain_queues()
|
|
199
|
-
await self._create_tasks()
|
|
143
|
+
# Parallel pipeline synchronized frames.
|
|
144
|
+
if isinstance(frame, (StartFrame, EndFrame)):
|
|
145
|
+
self._frame_counter[frame.id] = len(self._pipelines)
|
|
146
|
+
await self.pause_processing_system_frames()
|
|
147
|
+
await self.pause_processing_frames()
|
|
148
|
+
|
|
149
|
+
# Process frames in each of the sub-pipelines.
|
|
150
|
+
for p in self._pipelines:
|
|
151
|
+
await p.queue_frame(frame, direction)
|
|
200
152
|
|
|
201
153
|
async def _parallel_push_frame(self, frame: Frame, direction: FrameDirection):
|
|
154
|
+
"""Push frames while avoiding duplicates using frame ID tracking."""
|
|
202
155
|
if frame.id not in self._seen_ids:
|
|
203
156
|
self._seen_ids.add(frame.id)
|
|
204
157
|
await self.push_frame(frame, direction)
|
|
205
158
|
|
|
206
|
-
async def
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
self.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
frame
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
endframe_counter = self._endframe_counter[frame.id]
|
|
223
|
-
|
|
224
|
-
# If we don't have a counter or we reached 0, push the frame.
|
|
225
|
-
if endframe_counter == 0:
|
|
226
|
-
await self._parallel_push_frame(frame, FrameDirection.DOWNSTREAM)
|
|
227
|
-
|
|
228
|
-
running = not (endframe_counter == 0 and isinstance(frame, EndFrame))
|
|
229
|
-
|
|
230
|
-
self._down_queue.task_done()
|
|
159
|
+
async def _pipeline_sink_push_frame(self, frame: Frame, direction: FrameDirection):
|
|
160
|
+
# Parallel pipeline synchronized frames.
|
|
161
|
+
if isinstance(frame, (StartFrame, EndFrame)):
|
|
162
|
+
# Decrement counter.
|
|
163
|
+
frame_counter = self._frame_counter.get(frame.id, 0)
|
|
164
|
+
if frame_counter > 0:
|
|
165
|
+
self._frame_counter[frame.id] -= 1
|
|
166
|
+
frame_counter = self._frame_counter[frame.id]
|
|
167
|
+
|
|
168
|
+
# Only push the frame when all pipelines have processed it.
|
|
169
|
+
if frame_counter == 0:
|
|
170
|
+
await self._parallel_push_frame(frame, direction)
|
|
171
|
+
await self.resume_processing_system_frames()
|
|
172
|
+
await self.resume_processing_frames()
|
|
173
|
+
else:
|
|
174
|
+
await self._parallel_push_frame(frame, direction)
|
pipecat/pipeline/pipeline.py
CHANGED
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
5
|
#
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
"""Pipeline implementation for connecting and managing frame processors.
|
|
8
|
+
|
|
9
|
+
This module provides the main Pipeline class that connects frame processors
|
|
10
|
+
in sequence and manages frame flow between them, along with helper classes
|
|
11
|
+
for pipeline source and sink operations.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Callable, Coroutine, List, Optional
|
|
8
15
|
|
|
9
16
|
from pipecat.frames.frames import Frame
|
|
10
17
|
from pipecat.pipeline.base_pipeline import BasePipeline
|
|
@@ -12,11 +19,30 @@ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, F
|
|
|
12
19
|
|
|
13
20
|
|
|
14
21
|
class PipelineSource(FrameProcessor):
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
"""Source processor that forwards frames to an upstream handler.
|
|
23
|
+
|
|
24
|
+
This processor acts as the entry point for a pipeline, forwarding
|
|
25
|
+
downstream frames to the next processor and upstream frames to a
|
|
26
|
+
provided upstream handler function.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, upstream_push_frame: Callable[[Frame, FrameDirection], Coroutine], **kwargs):
|
|
30
|
+
"""Initialize the pipeline source.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
upstream_push_frame: Coroutine function to handle upstream frames.
|
|
34
|
+
**kwargs: Additional arguments passed to parent class.
|
|
35
|
+
"""
|
|
36
|
+
super().__init__(enable_direct_mode=True, **kwargs)
|
|
17
37
|
self._upstream_push_frame = upstream_push_frame
|
|
18
38
|
|
|
19
39
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
40
|
+
"""Process frames and route them based on direction.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
frame: The frame to process.
|
|
44
|
+
direction: The direction of frame flow.
|
|
45
|
+
"""
|
|
20
46
|
await super().process_frame(frame, direction)
|
|
21
47
|
|
|
22
48
|
match direction:
|
|
@@ -27,11 +53,32 @@ class PipelineSource(FrameProcessor):
|
|
|
27
53
|
|
|
28
54
|
|
|
29
55
|
class PipelineSink(FrameProcessor):
|
|
30
|
-
|
|
31
|
-
|
|
56
|
+
"""Sink processor that forwards frames to a downstream handler.
|
|
57
|
+
|
|
58
|
+
This processor acts as the exit point for a pipeline, forwarding
|
|
59
|
+
upstream frames to the previous processor and downstream frames to a
|
|
60
|
+
provided downstream handler function.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self, downstream_push_frame: Callable[[Frame, FrameDirection], Coroutine], **kwargs
|
|
65
|
+
):
|
|
66
|
+
"""Initialize the pipeline sink.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
downstream_push_frame: Coroutine function to handle downstream frames.
|
|
70
|
+
**kwargs: Additional arguments passed to parent class.
|
|
71
|
+
"""
|
|
72
|
+
super().__init__(enable_direct_mode=True, **kwargs)
|
|
32
73
|
self._downstream_push_frame = downstream_push_frame
|
|
33
74
|
|
|
34
75
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
76
|
+
"""Process frames and route them based on direction.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
frame: The frame to process.
|
|
80
|
+
direction: The direction of frame flow.
|
|
81
|
+
"""
|
|
35
82
|
await super().process_frame(frame, direction)
|
|
36
83
|
|
|
37
84
|
match direction:
|
|
@@ -42,43 +89,104 @@ class PipelineSink(FrameProcessor):
|
|
|
42
89
|
|
|
43
90
|
|
|
44
91
|
class Pipeline(BasePipeline):
|
|
45
|
-
|
|
46
|
-
|
|
92
|
+
"""Main pipeline implementation that connects frame processors in sequence.
|
|
93
|
+
|
|
94
|
+
Creates a linear chain of frame processors with automatic source and sink
|
|
95
|
+
processors for external frame handling. Manages processor lifecycle and
|
|
96
|
+
provides metrics collection from contained processors.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
processors: List[FrameProcessor],
|
|
102
|
+
*,
|
|
103
|
+
source: Optional[FrameProcessor] = None,
|
|
104
|
+
sink: Optional[FrameProcessor] = None,
|
|
105
|
+
):
|
|
106
|
+
"""Initialize the pipeline with a list of processors.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
processors: List of frame processors to connect in sequence.
|
|
110
|
+
source: An optional pipeline source processor.
|
|
111
|
+
sink: An optional pipeline sink processor.
|
|
112
|
+
"""
|
|
113
|
+
super().__init__(enable_direct_mode=True)
|
|
47
114
|
|
|
48
115
|
# Add a source and a sink queue so we can forward frames upstream and
|
|
49
116
|
# downstream outside of the pipeline.
|
|
50
|
-
self._source = PipelineSource(self.push_frame)
|
|
51
|
-
self._sink = PipelineSink(self.push_frame)
|
|
117
|
+
self._source = source or PipelineSource(self.push_frame, name=f"{self}::Source")
|
|
118
|
+
self._sink = sink or PipelineSink(self.push_frame, name=f"{self}::Sink")
|
|
52
119
|
self._processors: List[FrameProcessor] = [self._source] + processors + [self._sink]
|
|
53
120
|
|
|
54
121
|
self._link_processors()
|
|
55
122
|
|
|
56
123
|
#
|
|
57
|
-
#
|
|
124
|
+
# Frame processor
|
|
58
125
|
#
|
|
59
126
|
|
|
127
|
+
@property
|
|
128
|
+
def processors(self):
|
|
129
|
+
"""Return the list of sub-processors contained within this processor.
|
|
130
|
+
|
|
131
|
+
Only compound processors (e.g. pipelines and parallel pipelines) have
|
|
132
|
+
sub-processors. Non-compound processors will return an empty list.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
The list of sub-processors if this is a compound processor.
|
|
136
|
+
"""
|
|
137
|
+
return self._processors
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def entry_processors(self) -> List["FrameProcessor"]:
|
|
141
|
+
"""Return the list of entry processors for this processor.
|
|
142
|
+
|
|
143
|
+
Entry processors are the first processors in a compound processor
|
|
144
|
+
(e.g. pipelines, parallel pipelines). Note that pipelines can also be an
|
|
145
|
+
entry processor as pipelines are processors themselves. Non-compound
|
|
146
|
+
processors will simply return an empty list.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The list of entry processors.
|
|
150
|
+
"""
|
|
151
|
+
return [self._source]
|
|
152
|
+
|
|
60
153
|
def processors_with_metrics(self):
|
|
154
|
+
"""Return processors that can generate metrics.
|
|
155
|
+
|
|
156
|
+
Recursively collects all processors that support metrics generation,
|
|
157
|
+
including those from nested pipelines.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
List of frame processors that can generate metrics.
|
|
161
|
+
"""
|
|
61
162
|
services = []
|
|
62
|
-
for p in self.
|
|
63
|
-
if
|
|
64
|
-
services.extend(p.processors_with_metrics())
|
|
65
|
-
elif p.can_generate_metrics():
|
|
163
|
+
for p in self.processors:
|
|
164
|
+
if p.can_generate_metrics():
|
|
66
165
|
services.append(p)
|
|
166
|
+
services.extend(p.processors_with_metrics())
|
|
67
167
|
return services
|
|
68
168
|
|
|
69
|
-
#
|
|
70
|
-
# Frame processor
|
|
71
|
-
#
|
|
72
|
-
|
|
73
169
|
async def setup(self, setup: FrameProcessorSetup):
|
|
170
|
+
"""Set up the pipeline and all contained processors.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
setup: Configuration for frame processor setup.
|
|
174
|
+
"""
|
|
74
175
|
await super().setup(setup)
|
|
75
176
|
await self._setup_processors(setup)
|
|
76
177
|
|
|
77
178
|
async def cleanup(self):
|
|
179
|
+
"""Clean up the pipeline and all contained processors."""
|
|
78
180
|
await super().cleanup()
|
|
79
181
|
await self._cleanup_processors()
|
|
80
182
|
|
|
81
183
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
184
|
+
"""Process frames by routing them through the pipeline.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
frame: The frame to process.
|
|
188
|
+
direction: The direction of frame flow.
|
|
189
|
+
"""
|
|
82
190
|
await super().process_frame(frame, direction)
|
|
83
191
|
|
|
84
192
|
if direction == FrameDirection.DOWNSTREAM:
|
|
@@ -87,17 +195,18 @@ class Pipeline(BasePipeline):
|
|
|
87
195
|
await self._sink.queue_frame(frame, FrameDirection.UPSTREAM)
|
|
88
196
|
|
|
89
197
|
async def _setup_processors(self, setup: FrameProcessorSetup):
|
|
198
|
+
"""Set up all processors in the pipeline."""
|
|
90
199
|
for p in self._processors:
|
|
91
200
|
await p.setup(setup)
|
|
92
201
|
|
|
93
202
|
async def _cleanup_processors(self):
|
|
203
|
+
"""Clean up all processors in the pipeline."""
|
|
94
204
|
for p in self._processors:
|
|
95
205
|
await p.cleanup()
|
|
96
206
|
|
|
97
207
|
def _link_processors(self):
|
|
208
|
+
"""Link all processors in sequence and set their parent."""
|
|
98
209
|
prev = self._processors[0]
|
|
99
210
|
for curr in self._processors[1:]:
|
|
100
|
-
prev.set_parent(self)
|
|
101
211
|
prev.link(curr)
|
|
102
212
|
prev = curr
|
|
103
|
-
prev.set_parent(self)
|