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.

Files changed (244) hide show
  1. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/METADATA +137 -93
  2. dv_pipecat_ai-0.0.82.dev776.dist-info/RECORD +340 -0
  3. pipecat/__init__.py +17 -0
  4. pipecat/adapters/base_llm_adapter.py +36 -1
  5. pipecat/adapters/schemas/direct_function.py +296 -0
  6. pipecat/adapters/schemas/function_schema.py +15 -6
  7. pipecat/adapters/schemas/tools_schema.py +55 -7
  8. pipecat/adapters/services/anthropic_adapter.py +22 -3
  9. pipecat/adapters/services/aws_nova_sonic_adapter.py +23 -3
  10. pipecat/adapters/services/bedrock_adapter.py +22 -3
  11. pipecat/adapters/services/gemini_adapter.py +16 -3
  12. pipecat/adapters/services/open_ai_adapter.py +17 -2
  13. pipecat/adapters/services/open_ai_realtime_adapter.py +23 -3
  14. pipecat/audio/filters/base_audio_filter.py +30 -6
  15. pipecat/audio/filters/koala_filter.py +37 -2
  16. pipecat/audio/filters/krisp_filter.py +59 -6
  17. pipecat/audio/filters/noisereduce_filter.py +37 -0
  18. pipecat/audio/interruptions/base_interruption_strategy.py +25 -5
  19. pipecat/audio/interruptions/min_words_interruption_strategy.py +21 -4
  20. pipecat/audio/mixers/base_audio_mixer.py +30 -7
  21. pipecat/audio/mixers/soundfile_mixer.py +53 -6
  22. pipecat/audio/resamplers/base_audio_resampler.py +17 -9
  23. pipecat/audio/resamplers/resampy_resampler.py +26 -1
  24. pipecat/audio/resamplers/soxr_resampler.py +32 -1
  25. pipecat/audio/resamplers/soxr_stream_resampler.py +101 -0
  26. pipecat/audio/utils.py +194 -1
  27. pipecat/audio/vad/silero.py +60 -3
  28. pipecat/audio/vad/vad_analyzer.py +114 -30
  29. pipecat/clocks/base_clock.py +19 -0
  30. pipecat/clocks/system_clock.py +25 -0
  31. pipecat/extensions/voicemail/__init__.py +0 -0
  32. pipecat/extensions/voicemail/voicemail_detector.py +707 -0
  33. pipecat/frames/frames.py +590 -156
  34. pipecat/metrics/metrics.py +64 -1
  35. pipecat/observers/base_observer.py +58 -19
  36. pipecat/observers/loggers/debug_log_observer.py +56 -64
  37. pipecat/observers/loggers/llm_log_observer.py +8 -1
  38. pipecat/observers/loggers/transcription_log_observer.py +19 -7
  39. pipecat/observers/loggers/user_bot_latency_log_observer.py +32 -5
  40. pipecat/observers/turn_tracking_observer.py +26 -1
  41. pipecat/pipeline/base_pipeline.py +5 -7
  42. pipecat/pipeline/base_task.py +52 -9
  43. pipecat/pipeline/parallel_pipeline.py +121 -177
  44. pipecat/pipeline/pipeline.py +129 -20
  45. pipecat/pipeline/runner.py +50 -1
  46. pipecat/pipeline/sync_parallel_pipeline.py +132 -32
  47. pipecat/pipeline/task.py +263 -280
  48. pipecat/pipeline/task_observer.py +85 -34
  49. pipecat/pipeline/to_be_updated/merge_pipeline.py +32 -2
  50. pipecat/processors/aggregators/dtmf_aggregator.py +29 -22
  51. pipecat/processors/aggregators/gated.py +25 -24
  52. pipecat/processors/aggregators/gated_openai_llm_context.py +22 -2
  53. pipecat/processors/aggregators/llm_response.py +398 -89
  54. pipecat/processors/aggregators/openai_llm_context.py +161 -13
  55. pipecat/processors/aggregators/sentence.py +25 -14
  56. pipecat/processors/aggregators/user_response.py +28 -3
  57. pipecat/processors/aggregators/vision_image_frame.py +24 -14
  58. pipecat/processors/async_generator.py +28 -0
  59. pipecat/processors/audio/audio_buffer_processor.py +78 -37
  60. pipecat/processors/consumer_processor.py +25 -6
  61. pipecat/processors/filters/frame_filter.py +23 -0
  62. pipecat/processors/filters/function_filter.py +30 -0
  63. pipecat/processors/filters/identity_filter.py +17 -2
  64. pipecat/processors/filters/null_filter.py +24 -1
  65. pipecat/processors/filters/stt_mute_filter.py +56 -21
  66. pipecat/processors/filters/wake_check_filter.py +46 -3
  67. pipecat/processors/filters/wake_notifier_filter.py +21 -3
  68. pipecat/processors/frame_processor.py +488 -131
  69. pipecat/processors/frameworks/langchain.py +38 -3
  70. pipecat/processors/frameworks/rtvi.py +719 -34
  71. pipecat/processors/gstreamer/pipeline_source.py +41 -0
  72. pipecat/processors/idle_frame_processor.py +26 -3
  73. pipecat/processors/logger.py +23 -0
  74. pipecat/processors/metrics/frame_processor_metrics.py +77 -4
  75. pipecat/processors/metrics/sentry.py +42 -4
  76. pipecat/processors/producer_processor.py +34 -14
  77. pipecat/processors/text_transformer.py +22 -10
  78. pipecat/processors/transcript_processor.py +48 -29
  79. pipecat/processors/user_idle_processor.py +31 -21
  80. pipecat/runner/__init__.py +1 -0
  81. pipecat/runner/daily.py +132 -0
  82. pipecat/runner/livekit.py +148 -0
  83. pipecat/runner/run.py +543 -0
  84. pipecat/runner/types.py +67 -0
  85. pipecat/runner/utils.py +515 -0
  86. pipecat/serializers/base_serializer.py +42 -0
  87. pipecat/serializers/exotel.py +17 -6
  88. pipecat/serializers/genesys.py +95 -0
  89. pipecat/serializers/livekit.py +33 -0
  90. pipecat/serializers/plivo.py +16 -15
  91. pipecat/serializers/protobuf.py +37 -1
  92. pipecat/serializers/telnyx.py +18 -17
  93. pipecat/serializers/twilio.py +32 -16
  94. pipecat/services/ai_service.py +5 -3
  95. pipecat/services/anthropic/llm.py +113 -43
  96. pipecat/services/assemblyai/models.py +63 -5
  97. pipecat/services/assemblyai/stt.py +64 -11
  98. pipecat/services/asyncai/__init__.py +0 -0
  99. pipecat/services/asyncai/tts.py +501 -0
  100. pipecat/services/aws/llm.py +185 -111
  101. pipecat/services/aws/stt.py +217 -23
  102. pipecat/services/aws/tts.py +118 -52
  103. pipecat/services/aws/utils.py +101 -5
  104. pipecat/services/aws_nova_sonic/aws.py +82 -64
  105. pipecat/services/aws_nova_sonic/context.py +15 -6
  106. pipecat/services/azure/common.py +10 -2
  107. pipecat/services/azure/image.py +32 -0
  108. pipecat/services/azure/llm.py +9 -7
  109. pipecat/services/azure/stt.py +65 -2
  110. pipecat/services/azure/tts.py +154 -23
  111. pipecat/services/cartesia/stt.py +125 -8
  112. pipecat/services/cartesia/tts.py +102 -38
  113. pipecat/services/cerebras/llm.py +15 -23
  114. pipecat/services/deepgram/stt.py +19 -11
  115. pipecat/services/deepgram/tts.py +36 -0
  116. pipecat/services/deepseek/llm.py +14 -23
  117. pipecat/services/elevenlabs/tts.py +330 -64
  118. pipecat/services/fal/image.py +43 -0
  119. pipecat/services/fal/stt.py +48 -10
  120. pipecat/services/fireworks/llm.py +14 -21
  121. pipecat/services/fish/tts.py +109 -9
  122. pipecat/services/gemini_multimodal_live/__init__.py +1 -0
  123. pipecat/services/gemini_multimodal_live/events.py +83 -2
  124. pipecat/services/gemini_multimodal_live/file_api.py +189 -0
  125. pipecat/services/gemini_multimodal_live/gemini.py +218 -21
  126. pipecat/services/gladia/config.py +17 -10
  127. pipecat/services/gladia/stt.py +82 -36
  128. pipecat/services/google/frames.py +40 -0
  129. pipecat/services/google/google.py +2 -0
  130. pipecat/services/google/image.py +39 -2
  131. pipecat/services/google/llm.py +176 -58
  132. pipecat/services/google/llm_openai.py +26 -4
  133. pipecat/services/google/llm_vertex.py +37 -15
  134. pipecat/services/google/rtvi.py +41 -0
  135. pipecat/services/google/stt.py +65 -17
  136. pipecat/services/google/test-google-chirp.py +45 -0
  137. pipecat/services/google/tts.py +390 -19
  138. pipecat/services/grok/llm.py +8 -6
  139. pipecat/services/groq/llm.py +8 -6
  140. pipecat/services/groq/stt.py +13 -9
  141. pipecat/services/groq/tts.py +40 -0
  142. pipecat/services/hamsa/__init__.py +9 -0
  143. pipecat/services/hamsa/stt.py +241 -0
  144. pipecat/services/heygen/__init__.py +5 -0
  145. pipecat/services/heygen/api.py +281 -0
  146. pipecat/services/heygen/client.py +620 -0
  147. pipecat/services/heygen/video.py +338 -0
  148. pipecat/services/image_service.py +5 -3
  149. pipecat/services/inworld/__init__.py +1 -0
  150. pipecat/services/inworld/tts.py +592 -0
  151. pipecat/services/llm_service.py +127 -45
  152. pipecat/services/lmnt/tts.py +80 -7
  153. pipecat/services/mcp_service.py +85 -44
  154. pipecat/services/mem0/memory.py +42 -13
  155. pipecat/services/minimax/tts.py +74 -15
  156. pipecat/services/mistral/__init__.py +0 -0
  157. pipecat/services/mistral/llm.py +185 -0
  158. pipecat/services/moondream/vision.py +55 -10
  159. pipecat/services/neuphonic/tts.py +275 -48
  160. pipecat/services/nim/llm.py +8 -6
  161. pipecat/services/ollama/llm.py +27 -7
  162. pipecat/services/openai/base_llm.py +54 -16
  163. pipecat/services/openai/image.py +30 -0
  164. pipecat/services/openai/llm.py +7 -5
  165. pipecat/services/openai/stt.py +13 -9
  166. pipecat/services/openai/tts.py +42 -10
  167. pipecat/services/openai_realtime_beta/azure.py +11 -9
  168. pipecat/services/openai_realtime_beta/context.py +7 -5
  169. pipecat/services/openai_realtime_beta/events.py +10 -7
  170. pipecat/services/openai_realtime_beta/openai.py +37 -18
  171. pipecat/services/openpipe/llm.py +30 -24
  172. pipecat/services/openrouter/llm.py +9 -7
  173. pipecat/services/perplexity/llm.py +15 -19
  174. pipecat/services/piper/tts.py +26 -12
  175. pipecat/services/playht/tts.py +227 -65
  176. pipecat/services/qwen/llm.py +8 -6
  177. pipecat/services/rime/tts.py +128 -17
  178. pipecat/services/riva/stt.py +160 -22
  179. pipecat/services/riva/tts.py +67 -2
  180. pipecat/services/sambanova/llm.py +19 -17
  181. pipecat/services/sambanova/stt.py +14 -8
  182. pipecat/services/sarvam/tts.py +60 -13
  183. pipecat/services/simli/video.py +82 -21
  184. pipecat/services/soniox/__init__.py +0 -0
  185. pipecat/services/soniox/stt.py +398 -0
  186. pipecat/services/speechmatics/stt.py +29 -17
  187. pipecat/services/stt_service.py +47 -11
  188. pipecat/services/tavus/video.py +94 -25
  189. pipecat/services/together/llm.py +8 -6
  190. pipecat/services/tts_service.py +77 -53
  191. pipecat/services/ultravox/stt.py +46 -43
  192. pipecat/services/vision_service.py +5 -3
  193. pipecat/services/websocket_service.py +12 -11
  194. pipecat/services/whisper/base_stt.py +58 -12
  195. pipecat/services/whisper/stt.py +69 -58
  196. pipecat/services/xtts/tts.py +59 -2
  197. pipecat/sync/base_notifier.py +19 -0
  198. pipecat/sync/event_notifier.py +24 -0
  199. pipecat/tests/utils.py +73 -5
  200. pipecat/transcriptions/language.py +24 -0
  201. pipecat/transports/base_input.py +112 -8
  202. pipecat/transports/base_output.py +235 -13
  203. pipecat/transports/base_transport.py +119 -0
  204. pipecat/transports/local/audio.py +76 -0
  205. pipecat/transports/local/tk.py +84 -0
  206. pipecat/transports/network/fastapi_websocket.py +174 -15
  207. pipecat/transports/network/small_webrtc.py +383 -39
  208. pipecat/transports/network/webrtc_connection.py +214 -8
  209. pipecat/transports/network/websocket_client.py +171 -1
  210. pipecat/transports/network/websocket_server.py +147 -9
  211. pipecat/transports/services/daily.py +792 -70
  212. pipecat/transports/services/helpers/daily_rest.py +122 -129
  213. pipecat/transports/services/livekit.py +339 -4
  214. pipecat/transports/services/tavus.py +273 -38
  215. pipecat/utils/asyncio/task_manager.py +92 -186
  216. pipecat/utils/base_object.py +83 -1
  217. pipecat/utils/network.py +2 -0
  218. pipecat/utils/string.py +114 -58
  219. pipecat/utils/text/base_text_aggregator.py +44 -13
  220. pipecat/utils/text/base_text_filter.py +46 -0
  221. pipecat/utils/text/markdown_text_filter.py +70 -14
  222. pipecat/utils/text/pattern_pair_aggregator.py +18 -14
  223. pipecat/utils/text/simple_text_aggregator.py +43 -2
  224. pipecat/utils/text/skip_tags_aggregator.py +21 -13
  225. pipecat/utils/time.py +36 -0
  226. pipecat/utils/tracing/class_decorators.py +32 -7
  227. pipecat/utils/tracing/conversation_context_provider.py +12 -2
  228. pipecat/utils/tracing/service_attributes.py +80 -64
  229. pipecat/utils/tracing/service_decorators.py +48 -21
  230. pipecat/utils/tracing/setup.py +13 -7
  231. pipecat/utils/tracing/turn_context_provider.py +12 -2
  232. pipecat/utils/tracing/turn_trace_observer.py +27 -0
  233. pipecat/utils/utils.py +14 -14
  234. dv_pipecat_ai-0.0.74.dev770.dist-info/RECORD +0 -319
  235. pipecat/examples/daily_runner.py +0 -64
  236. pipecat/examples/run.py +0 -265
  237. pipecat/utils/asyncio/watchdog_async_iterator.py +0 -72
  238. pipecat/utils/asyncio/watchdog_event.py +0 -42
  239. pipecat/utils/asyncio/watchdog_priority_queue.py +0 -48
  240. pipecat/utils/asyncio/watchdog_queue.py +0 -48
  241. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/WHEEL +0 -0
  242. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/licenses/LICENSE +0 -0
  243. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/top_level.txt +0 -0
  244. /pipecat/{examples → extensions}/__init__.py +0 -0
@@ -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
- """Specific configuration for the pipeline task."""
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
- """Indicates whether the tasks has finished. That is, all processors
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
- """This is a helper function that sends an EndFrame to the pipeline in
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
- """Stops the running pipeline immediately."""
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
- """Starts running the given pipeline."""
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 to be pushed down the pipeline."""
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
- """Queues multiple frames to be pushed down the pipeline."""
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
- import asyncio
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 Awaitable, Callable, Dict, List
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 ParallelPipelineSource(FrameProcessor):
28
- def __init__(
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
- async def process_frame(self, frame: Frame, direction: FrameDirection):
61
- await super().process_frame(frame, direction)
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
- match direction:
64
- case FrameDirection.UPSTREAM:
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
- class ParallelPipeline(BasePipeline):
74
- def __init__(self, *args):
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._endframe_counter: Dict[int, int] = {}
54
+ self._frame_counter: Dict[int, int] = {}
87
55
 
88
- self._up_task = None
89
- self._down_task = None
56
+ logger.debug(f"Creating {self} pipelines")
90
57
 
91
- #
92
- # BasePipeline
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
- def processors_with_metrics(self) -> List[FrameProcessor]:
96
- return list(chain.from_iterable(p.processors_with_metrics() for p in self._pipelines))
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
- async def setup(self, setup: FrameProcessorSetup):
103
- await super().setup(setup)
81
+ @property
82
+ def processors(self):
83
+ """Return the list of sub-processors contained within this processor.
104
84
 
105
- self._up_queue = WatchdogQueue(setup.task_manager)
106
- self._down_queue = WatchdogQueue(setup.task_manager)
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
- logger.debug(f"Creating {self} pipelines")
109
- for processors in self._args:
110
- if not isinstance(processors, list):
111
- raise TypeError(f"ParallelPipeline argument {processors} is not a list")
88
+ Returns:
89
+ The list of sub-processors if this is a compound processor.
90
+ """
91
+ return self._pipelines
112
92
 
113
- # We will add a source before the pipeline and a sink after.
114
- source = ParallelPipelineSource(self._up_queue, self._parallel_push_frame)
115
- sink = ParallelPipelineSink(self._down_queue, self._parallel_push_frame)
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
- # Create pipeline
120
- pipeline = Pipeline(processors)
121
- source.link(pipeline)
122
- pipeline.link(sink)
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
- logger.debug(f"Finished creating {self} pipelines")
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
- await asyncio.gather(*[s.setup(setup) for s in self._sources])
128
- await asyncio.gather(*[p.setup(setup) for p in self._pipelines])
129
- await asyncio.gather(*[s.setup(setup) for s in self._sinks])
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
- await asyncio.gather(*[s.cleanup() for s in self._sources])
134
- await asyncio.gather(*[p.cleanup() for p in self._pipelines])
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
- if isinstance(frame, StartFrame):
141
- await self._start(frame)
142
- elif isinstance(frame, EndFrame):
143
- self._endframe_counter[frame.id] = len(self._pipelines)
144
- elif isinstance(frame, CancelFrame):
145
- await self._cancel()
146
-
147
- if direction == FrameDirection.UPSTREAM:
148
- # If we get an upstream frame we process it in each sink.
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 _process_up_queue(self):
207
- while True:
208
- frame = await self._up_queue.get()
209
- await self._parallel_push_frame(frame, FrameDirection.UPSTREAM)
210
- self._up_queue.task_done()
211
-
212
- async def _process_down_queue(self):
213
- running = True
214
- while running:
215
- frame = await self._down_queue.get()
216
-
217
- endframe_counter = self._endframe_counter.get(frame.id, 0)
218
-
219
- # If we have a counter, decrement it.
220
- if endframe_counter > 0:
221
- self._endframe_counter[frame.id] -= 1
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)
@@ -4,7 +4,14 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
- from typing import Callable, Coroutine, List
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
- def __init__(self, upstream_push_frame: Callable[[Frame, FrameDirection], Coroutine]):
16
- super().__init__()
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
- def __init__(self, downstream_push_frame: Callable[[Frame, FrameDirection], Coroutine]):
31
- super().__init__()
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
- def __init__(self, processors: List[FrameProcessor]):
46
- super().__init__()
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
- # BasePipeline
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._processors:
63
- if isinstance(p, BasePipeline):
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)