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,22 +4,34 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """Task observer for managing pipeline frame observers.
8
+
9
+ This module provides a proxy observer system that manages multiple observers
10
+ for pipeline frame events, ensuring that observer processing doesn't block
11
+ the main pipeline execution.
12
+ """
13
+
7
14
  import asyncio
8
15
  import inspect
9
- from typing import Dict, List, Optional
16
+ from typing import Any, Dict, List, Optional
10
17
 
11
18
  from attr import dataclass
12
19
 
13
- from pipecat.observers.base_observer import BaseObserver, FramePushed
20
+ from pipecat.observers.base_observer import BaseObserver, FrameProcessed, FramePushed
14
21
  from pipecat.utils.asyncio.task_manager import BaseTaskManager
15
- from pipecat.utils.asyncio.watchdog_queue import WatchdogQueue
16
22
 
17
23
 
18
24
  @dataclass
19
25
  class Proxy:
20
- """This is the data we receive from the main observer and that we put into
21
- a queue for later processing.
26
+ """Proxy data for managing observer tasks and queues.
27
+
28
+ This represents is the data received from the main observer that
29
+ is queued for later processing.
22
30
 
31
+ Parameters:
32
+ queue: Queue for frame data awaiting observer processing.
33
+ task: Asyncio task running the observer's frame processing loop.
34
+ observer: The actual observer instance being proxied.
23
35
  """
24
36
 
25
37
  queue: asyncio.Queue
@@ -28,7 +40,9 @@ class Proxy:
28
40
 
29
41
 
30
42
  class TaskObserver(BaseObserver):
31
- """This is a pipeline frame observer that is meant to be used as a proxy to
43
+ """Proxy observer that manages multiple observers without blocking the pipeline.
44
+
45
+ This is a pipeline frame observer that is meant to be used as a proxy to
32
46
  the user provided observers. That is, this is the observer that should be
33
47
  passed to the frame processors. Then, every time a frame is pushed this
34
48
  observer will call all the observers registered to the pipeline task.
@@ -37,7 +51,6 @@ class TaskObserver(BaseObserver):
37
51
  pipeline by creating a queue and a task for each user observer. When a frame
38
52
  is received, it will be put in a queue for efficiency and later processed by
39
53
  each task.
40
-
41
54
  """
42
55
 
43
56
  def __init__(
@@ -47,6 +60,13 @@ class TaskObserver(BaseObserver):
47
60
  task_manager: BaseTaskManager,
48
61
  **kwargs,
49
62
  ):
63
+ """Initialize the TaskObserver.
64
+
65
+ Args:
66
+ observers: List of observers to manage. Defaults to empty list.
67
+ task_manager: Task manager for creating and managing observer tasks.
68
+ **kwargs: Additional arguments passed to the base observer.
69
+ """
50
70
  super().__init__(**kwargs)
51
71
  self._observers = observers or []
52
72
  self._task_manager = task_manager
@@ -55,18 +75,28 @@ class TaskObserver(BaseObserver):
55
75
  )
56
76
 
57
77
  def add_observer(self, observer: BaseObserver):
78
+ """Add a new observer to the managed list.
79
+
80
+ Args:
81
+ observer: The observer to add.
82
+ """
58
83
  # Add the observer to the list.
59
84
  self._observers.append(observer)
60
85
 
61
86
  # If we already started, create a new proxy for the observer.
62
87
  # Otherwise, it will be created in start().
63
- if self._started():
88
+ if self._proxies:
64
89
  proxy = self._create_proxy(observer)
65
90
  self._proxies[observer] = proxy
66
91
 
67
92
  async def remove_observer(self, observer: BaseObserver):
93
+ """Remove an observer and clean up its resources.
94
+
95
+ Args:
96
+ observer: The observer to remove.
97
+ """
68
98
  # If the observer has a proxy, remove it.
69
- if observer in self._proxies:
99
+ if self._proxies and observer in self._proxies:
70
100
  proxy = self._proxies[observer]
71
101
  # Remove the proxy so it doesn't get called anymore.
72
102
  del self._proxies[observer]
@@ -78,26 +108,36 @@ class TaskObserver(BaseObserver):
78
108
  self._observers.remove(observer)
79
109
 
80
110
  async def start(self):
81
- """Starts all proxy observer tasks."""
111
+ """Start all proxy observer tasks."""
82
112
  self._proxies = self._create_proxies(self._observers)
83
113
 
84
114
  async def stop(self):
85
- """Stops all proxy observer tasks."""
115
+ """Stop all proxy observer tasks."""
86
116
  if not self._proxies:
87
117
  return
88
118
 
89
119
  for proxy in self._proxies.values():
90
120
  await self._task_manager.cancel_task(proxy.task)
91
121
 
122
+ async def on_process_frame(self, data: FramePushed):
123
+ """Queue frame data for all managed observers.
124
+
125
+ Args:
126
+ data: The frame push event data to distribute to observers.
127
+ """
128
+ await self._send_to_proxy(data)
129
+
92
130
  async def on_push_frame(self, data: FramePushed):
93
- for proxy in self._proxies.values():
94
- await proxy.queue.put(data)
131
+ """Queue frame data for all managed observers.
95
132
 
96
- def _started(self) -> bool:
97
- return self._proxies is not None
133
+ Args:
134
+ data: The frame push event data to distribute to observers.
135
+ """
136
+ await self._send_to_proxy(data)
98
137
 
99
138
  def _create_proxy(self, observer: BaseObserver) -> Proxy:
100
- queue = WatchdogQueue(self._task_manager)
139
+ """Create a proxy for a single observer."""
140
+ queue = asyncio.Queue()
101
141
  task = self._task_manager.create_task(
102
142
  self._proxy_task_handler(queue, observer),
103
143
  f"TaskObserver::{observer}::_proxy_task_handler",
@@ -106,33 +146,44 @@ class TaskObserver(BaseObserver):
106
146
  return proxy
107
147
 
108
148
  def _create_proxies(self, observers: List[BaseObserver]) -> Dict[BaseObserver, Proxy]:
149
+ """Create proxies for all observers."""
109
150
  proxies = {}
110
151
  for observer in observers:
111
152
  proxy = self._create_proxy(observer)
112
153
  proxies[observer] = proxy
113
154
  return proxies
114
155
 
156
+ async def _send_to_proxy(self, data: Any):
157
+ for proxy in self._proxies.values():
158
+ await proxy.queue.put(data)
159
+
115
160
  async def _proxy_task_handler(self, queue: asyncio.Queue, observer: BaseObserver):
116
- warning_reported = False
161
+ """Handle frame processing for a single observer."""
162
+ on_push_frame_deprecated = False
163
+ signature = inspect.signature(observer.on_push_frame)
164
+ if len(signature.parameters) > 1:
165
+ import warnings
166
+
167
+ with warnings.catch_warnings():
168
+ warnings.simplefilter("always")
169
+ warnings.warn(
170
+ "Observer `on_push_frame(source, destination, frame, direction, timestamp)` is deprecated, us `on_push_frame(data: FramePushed)` instead.",
171
+ DeprecationWarning,
172
+ )
173
+
174
+ on_push_frame_deprecated = True
175
+
117
176
  while True:
118
177
  data = await queue.get()
119
178
 
120
- signature = inspect.signature(observer.on_push_frame)
121
- if len(signature.parameters) > 1:
122
- if not warning_reported:
123
- import warnings
124
-
125
- with warnings.catch_warnings():
126
- warnings.simplefilter("always")
127
- warnings.warn(
128
- "Observer `on_push_frame(source, destination, frame, direction, timestamp)` is deprecated, us `on_push_frame(data: FramePushed)` instead.",
129
- DeprecationWarning,
130
- )
131
- warning_reported = True
132
- await observer.on_push_frame(
133
- data.src, data.dst, data.frame, data.direction, data.timestamp
134
- )
135
- else:
136
- await observer.on_push_frame(data)
179
+ if isinstance(data, FramePushed):
180
+ if on_push_frame_deprecated:
181
+ await observer.on_push_frame(
182
+ data.src, data.dst, data.frame, data.direction, data.timestamp
183
+ )
184
+ else:
185
+ await observer.on_push_frame(data)
186
+ elif isinstance(data, FrameProcessed):
187
+ await observer.on_process_frame(data)
137
188
 
138
189
  queue.task_done()
@@ -1,3 +1,16 @@
1
+ #
2
+ # Copyright (c) 2024–2025, Daily
3
+ #
4
+ # SPDX-License-Identifier: BSD 2-Clause License
5
+ #
6
+
7
+ """Sequential pipeline merging for Pipecat.
8
+
9
+ This module provides a pipeline implementation that sequentially merges
10
+ the output from multiple pipelines, processing them one after another
11
+ in a specified order.
12
+ """
13
+
1
14
  from typing import List
2
15
 
3
16
  from pipecat.frames.frames import EndFrame, EndPipeFrame
@@ -5,14 +18,31 @@ from pipecat.pipeline.pipeline import Pipeline
5
18
 
6
19
 
7
20
  class SequentialMergePipeline(Pipeline):
8
- """This class merges the sink queues from a list of pipelines. Frames from
9
- each pipeline's sink are merged in the order of pipelines in the list."""
21
+ """Pipeline that sequentially merges output from multiple pipelines.
22
+
23
+ This pipeline merges the sink queues from a list of pipelines by processing
24
+ frames from each pipeline's sink sequentially in the order specified. Each
25
+ pipeline runs to completion before the next one begins processing.
26
+ """
10
27
 
11
28
  def __init__(self, pipelines: List[Pipeline]):
29
+ """Initialize the sequential merge pipeline.
30
+
31
+ Args:
32
+ pipelines: List of pipelines to merge sequentially. Pipelines will
33
+ be processed in the order they appear in this list.
34
+ """
12
35
  super().__init__([])
13
36
  self.pipelines = pipelines
14
37
 
15
38
  async def run_pipeline(self):
39
+ """Run all pipelines sequentially and merge their output.
40
+
41
+ Processes each pipeline in order, consuming all frames from each
42
+ pipeline's sink until an EndFrame or EndPipeFrame is encountered,
43
+ then moves to the next pipeline. After all pipelines complete,
44
+ sends a final EndFrame to signal completion.
45
+ """
16
46
  for idx, pipeline in enumerate(self.pipelines):
17
47
  while True:
18
48
  frame = await pipeline.sink.get()
@@ -4,6 +4,13 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """DTMF aggregation processor for converting keypad input to transcription.
8
+
9
+ This module provides a frame processor that aggregates DTMF (Dual-Tone Multi-Frequency)
10
+ keypad inputs into meaningful sequences and converts them to transcription frames
11
+ for downstream processing by LLM context aggregators.
12
+ """
13
+
7
14
  import asyncio
8
15
  from typing import Optional
9
16
 
@@ -17,7 +24,7 @@ from pipecat.frames.frames import (
17
24
  StartFrame,
18
25
  TranscriptionFrame,
19
26
  )
20
- from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
27
+ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, FrameProcessorSetup
21
28
  from pipecat.utils.time import time_now_iso8601
22
29
 
23
30
 
@@ -26,16 +33,12 @@ class DTMFAggregator(FrameProcessor):
26
33
 
27
34
  The aggregator accumulates digits from InputDTMFFrame instances and flushes
28
35
  when:
36
+
29
37
  - Timeout occurs (configurable idle period)
30
38
  - Termination digit is received (default: '#')
31
39
  - EndFrame or CancelFrame is received
32
40
 
33
41
  Emits TranscriptionFrame for compatibility with existing LLM context aggregators.
34
-
35
- Args:
36
- timeout: Idle timeout in seconds before flushing
37
- termination_digit: Digit that triggers immediate flush
38
- prefix: Prefix added to DTMF sequence in transcription
39
42
  """
40
43
 
41
44
  def __init__(
@@ -45,6 +48,14 @@ class DTMFAggregator(FrameProcessor):
45
48
  prefix: str = "DTMF: ",
46
49
  **kwargs,
47
50
  ):
51
+ """Initialize the DTMF aggregator.
52
+
53
+ Args:
54
+ timeout: Idle timeout in seconds before flushing
55
+ termination_digit: Digit that triggers immediate flush
56
+ prefix: Prefix added to DTMF sequence in transcription
57
+ **kwargs: Additional arguments passed to FrameProcessor
58
+ """
48
59
  super().__init__(**kwargs)
49
60
  self._aggregation = ""
50
61
  self._idle_timeout = timeout
@@ -54,7 +65,18 @@ class DTMFAggregator(FrameProcessor):
54
65
  self._digit_event = asyncio.Event()
55
66
  self._aggregation_task: Optional[asyncio.Task] = None
56
67
 
68
+ async def cleanup(self) -> None:
69
+ """Clean up resources."""
70
+ await super().cleanup()
71
+ await self._stop_aggregation_task()
72
+
57
73
  async def process_frame(self, frame: Frame, direction: FrameDirection) -> None:
74
+ """Process incoming frames and handle DTMF aggregation.
75
+
76
+ Args:
77
+ frame: The frame to process.
78
+ direction: The direction of frame flow in the pipeline.
79
+ """
58
80
  await super().process_frame(frame, direction)
59
81
 
60
82
  if isinstance(frame, StartFrame):
@@ -83,7 +105,7 @@ class DTMFAggregator(FrameProcessor):
83
105
 
84
106
  # For first digit, schedule interruption in separate task
85
107
  if is_first_digit:
86
- asyncio.create_task(self._send_interruption_task())
108
+ await self.push_frame(BotInterruptionFrame(), FrameDirection.UPSTREAM)
87
109
 
88
110
  # Check for immediate flush conditions
89
111
  if frame.button == self._termination_digit:
@@ -92,15 +114,6 @@ class DTMFAggregator(FrameProcessor):
92
114
  # Signal digit received for timeout handling
93
115
  self._digit_event.set()
94
116
 
95
- async def _send_interruption_task(self):
96
- """Send interruption frame safely in a separate task."""
97
- try:
98
- # Send the interruption frame
99
- await self.push_frame(BotInterruptionFrame(), FrameDirection.UPSTREAM)
100
- except Exception as e:
101
- # Log error but don't propagate
102
- print(f"Error sending interruption: {e}")
103
-
104
117
  def _create_aggregation_task(self) -> None:
105
118
  """Creates the aggregation task if it hasn't been created yet."""
106
119
  if not self._aggregation_task:
@@ -119,7 +132,6 @@ class DTMFAggregator(FrameProcessor):
119
132
  await asyncio.wait_for(self._digit_event.wait(), timeout=self._idle_timeout)
120
133
  self._digit_event.clear()
121
134
  except asyncio.TimeoutError:
122
- self.reset_watchdog()
123
135
  if self._aggregation:
124
136
  await self._flush_aggregation()
125
137
 
@@ -137,8 +149,3 @@ class DTMFAggregator(FrameProcessor):
137
149
  await self.push_frame(transcription_frame)
138
150
 
139
151
  self._aggregation = ""
140
-
141
- async def cleanup(self) -> None:
142
- """Clean up resources."""
143
- await super().cleanup()
144
- await self._stop_aggregation_task()
@@ -4,6 +4,13 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """Gated frame aggregator for conditional frame accumulation.
8
+
9
+ This module provides a gated aggregator that accumulates frames based on
10
+ custom gate open/close functions, allowing for conditional frame buffering
11
+ and release in frame processing pipelines.
12
+ """
13
+
7
14
  from typing import List, Tuple
8
15
 
9
16
  from loguru import logger
@@ -14,31 +21,11 @@ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
14
21
 
15
22
  class GatedAggregator(FrameProcessor):
16
23
  """Accumulate frames, with custom functions to start and stop accumulation.
24
+
17
25
  Yields gate-opening frame before any accumulated frames, then ensuing frames
18
- until and not including the gate-closed frame.
19
-
20
- Doctest: FIXME to work with asyncio
21
- >>> from pipecat.frames.frames import ImageRawFrame
22
-
23
- >>> async def print_frames(aggregator, frame):
24
- ... async for frame in aggregator.process_frame(frame):
25
- ... if isinstance(frame, TextFrame):
26
- ... print(frame.text)
27
- ... else:
28
- ... print(frame.__class__.__name__)
29
-
30
- >>> aggregator = GatedAggregator(
31
- ... gate_close_fn=lambda x: isinstance(x, LLMResponseStartFrame),
32
- ... gate_open_fn=lambda x: isinstance(x, ImageRawFrame),
33
- ... start_open=False)
34
- >>> asyncio.run(print_frames(aggregator, TextFrame("Hello")))
35
- >>> asyncio.run(print_frames(aggregator, TextFrame("Hello again.")))
36
- >>> asyncio.run(print_frames(aggregator, ImageRawFrame(image=bytes([]), size=(0, 0))))
37
- ImageRawFrame
38
- Hello
39
- Hello again.
40
- >>> asyncio.run(print_frames(aggregator, TextFrame("Goodbye.")))
41
- Goodbye.
26
+ until and not including the gate-closed frame. The aggregator maintains an
27
+ internal gate state that controls whether frames are passed through immediately
28
+ or accumulated for later release.
42
29
  """
43
30
 
44
31
  def __init__(
@@ -48,6 +35,14 @@ class GatedAggregator(FrameProcessor):
48
35
  start_open,
49
36
  direction: FrameDirection = FrameDirection.DOWNSTREAM,
50
37
  ):
38
+ """Initialize the gated aggregator.
39
+
40
+ Args:
41
+ gate_open_fn: Function that returns True when a frame should open the gate.
42
+ gate_close_fn: Function that returns True when a frame should close the gate.
43
+ start_open: Whether the gate should start in the open state.
44
+ direction: The frame direction this aggregator operates on.
45
+ """
51
46
  super().__init__()
52
47
  self._gate_open_fn = gate_open_fn
53
48
  self._gate_close_fn = gate_close_fn
@@ -56,6 +51,12 @@ class GatedAggregator(FrameProcessor):
56
51
  self._accumulator: List[Tuple[Frame, FrameDirection]] = []
57
52
 
58
53
  async def process_frame(self, frame: Frame, direction: FrameDirection):
54
+ """Process incoming frames with gated accumulation logic.
55
+
56
+ Args:
57
+ frame: The frame to process.
58
+ direction: The direction of the frame flow.
59
+ """
59
60
  await super().process_frame(frame, direction)
60
61
 
61
62
  # We must not block system frames.
@@ -4,6 +4,8 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """Gated OpenAI LLM context aggregator for controlled message flow."""
8
+
7
9
  from pipecat.frames.frames import CancelFrame, EndFrame, Frame, StartFrame
8
10
  from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContextFrame
9
11
  from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
@@ -11,12 +13,21 @@ from pipecat.sync.base_notifier import BaseNotifier
11
13
 
12
14
 
13
15
  class GatedOpenAILLMContextAggregator(FrameProcessor):
14
- """This aggregator keeps the last received OpenAI LLM context frame and it
15
- doesn't let it through until the notifier is notified.
16
+ """Aggregator that gates OpenAI LLM context frames until notified.
16
17
 
18
+ This aggregator captures OpenAI LLM context frames and holds them until
19
+ a notifier signals that they can be released. This is useful for controlling
20
+ the flow of context frames based on external conditions or timing.
17
21
  """
18
22
 
19
23
  def __init__(self, *, notifier: BaseNotifier, start_open: bool = False, **kwargs):
24
+ """Initialize the gated context aggregator.
25
+
26
+ Args:
27
+ notifier: The notifier that controls when frames are released.
28
+ start_open: If True, the first context frame passes through immediately.
29
+ **kwargs: Additional arguments passed to the parent FrameProcessor.
30
+ """
20
31
  super().__init__(**kwargs)
21
32
  self._notifier = notifier
22
33
  self._start_open = start_open
@@ -24,6 +35,12 @@ class GatedOpenAILLMContextAggregator(FrameProcessor):
24
35
  self._gate_task = None
25
36
 
26
37
  async def process_frame(self, frame: Frame, direction: FrameDirection):
38
+ """Process incoming frames, gating OpenAI LLM context frames.
39
+
40
+ Args:
41
+ frame: The frame to process.
42
+ direction: The direction of frame flow in the pipeline.
43
+ """
27
44
  await super().process_frame(frame, direction)
28
45
 
29
46
  if isinstance(frame, StartFrame):
@@ -42,15 +59,18 @@ class GatedOpenAILLMContextAggregator(FrameProcessor):
42
59
  await self.push_frame(frame, direction)
43
60
 
44
61
  async def _start(self):
62
+ """Start the gate task handler."""
45
63
  if not self._gate_task:
46
64
  self._gate_task = self.create_task(self._gate_task_handler())
47
65
 
48
66
  async def _stop(self):
67
+ """Stop the gate task handler."""
49
68
  if self._gate_task:
50
69
  await self.cancel_task(self._gate_task)
51
70
  self._gate_task = None
52
71
 
53
72
  async def _gate_task_handler(self):
73
+ """Handle the gating logic by waiting for notifications and releasing frames."""
54
74
  while True:
55
75
  await self._notifier.wait()
56
76
  if self._last_context_frame: