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,12 +4,18 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """Audio buffer processor for managing and synchronizing audio streams.
8
+
9
+ This module provides an AudioBufferProcessor that handles buffering and synchronization
10
+ of audio from both user input and bot output sources, with support for various audio
11
+ configurations and event-driven processing.
12
+ """
13
+
7
14
  import time
8
15
  from typing import Optional
9
16
 
10
- from pipecat.audio.utils import create_default_resampler, interleave_stereo_audio, mix_audio
17
+ from pipecat.audio.utils import create_stream_resampler, interleave_stereo_audio, mix_audio
11
18
  from pipecat.frames.frames import (
12
- AudioRawFrame,
13
19
  BotStartedSpeakingFrame,
14
20
  BotStoppedSpeakingFrame,
15
21
  CancelFrame,
@@ -32,23 +38,19 @@ class AudioBufferProcessor(FrameProcessor):
32
38
  including sample rate conversion and mono/stereo output.
33
39
 
34
40
  Events:
35
- on_audio_data: Triggered when buffer_size is reached, providing merged audio
36
- on_track_audio_data: Triggered when buffer_size is reached, providing separate tracks
37
- on_user_turn_audio_data: Triggered when user turn has ended, providing that user turn's audio
38
- on_bot_turn_audio_data: Triggered when bot turn has ended, providing that bot turn's audio
39
41
 
40
- Args:
41
- sample_rate (Optional[int]): Desired output sample rate. If None, uses source rate
42
- num_channels (int): Number of channels (1 for mono, 2 for stereo). Defaults to 1
43
- buffer_size (int): Size of buffer before triggering events. 0 for no buffering
44
- enable_turn_audio (bool): Whether turn audio event handlers should be triggered
42
+ - on_audio_data: Triggered when buffer_size is reached, providing merged audio
43
+ - on_track_audio_data: Triggered when buffer_size is reached, providing separate tracks
44
+ - on_user_turn_audio_data: Triggered when user turn has ended, providing that user turn's audio
45
+ - on_bot_turn_audio_data: Triggered when bot turn has ended, providing that bot turn's audio
45
46
 
46
47
  Audio handling:
47
- - Mono output (num_channels=1): User and bot audio are mixed
48
- - Stereo output (num_channels=2): User audio on left, bot audio on right
49
- - Automatic resampling of incoming audio to match desired sample_rate
50
- - Silence insertion for non-continuous audio streams
51
- - Buffer synchronization between user and bot audio
48
+
49
+ - Mono output (num_channels=1): User and bot audio are mixed
50
+ - Stereo output (num_channels=2): User audio on left, bot audio on right
51
+ - Automatic resampling of incoming audio to match desired sample_rate
52
+ - Silence insertion for non-continuous audio streams
53
+ - Buffer synchronization between user and bot audio
52
54
  """
53
55
 
54
56
  def __init__(
@@ -61,6 +63,21 @@ class AudioBufferProcessor(FrameProcessor):
61
63
  enable_turn_audio: bool = False,
62
64
  **kwargs,
63
65
  ):
66
+ """Initialize the audio buffer processor.
67
+
68
+ Args:
69
+ sample_rate: Desired output sample rate. If None, uses source rate.
70
+ num_channels: Number of channels (1 for mono, 2 for stereo). Defaults to 1.
71
+ buffer_size: Size of buffer before triggering events. 0 for no buffering.
72
+ user_continuous_stream: Controls whether user audio is treated as a continuous
73
+ stream for buffering purposes.
74
+
75
+ .. deprecated:: 0.0.72
76
+ This parameter no longer has any effect and will be removed in a future version.
77
+
78
+ enable_turn_audio: Whether turn audio event handlers should be triggered.
79
+ **kwargs: Additional arguments passed to parent class.
80
+ """
64
81
  super().__init__(**kwargs)
65
82
  self._init_sample_rate = sample_rate
66
83
  self._sample_rate = 0
@@ -93,7 +110,8 @@ class AudioBufferProcessor(FrameProcessor):
93
110
 
94
111
  self._recording = False
95
112
 
96
- self._resampler = create_default_resampler()
113
+ self._input_resampler = create_stream_resampler()
114
+ self._output_resampler = create_stream_resampler()
97
115
 
98
116
  self._register_event_handler("on_audio_data")
99
117
  self._register_event_handler("on_track_audio_data")
@@ -105,7 +123,7 @@ class AudioBufferProcessor(FrameProcessor):
105
123
  """Current sample rate of the audio processor.
106
124
 
107
125
  Returns:
108
- int: The sample rate in Hz
126
+ The sample rate in Hz.
109
127
  """
110
128
  return self._sample_rate
111
129
 
@@ -114,7 +132,7 @@ class AudioBufferProcessor(FrameProcessor):
114
132
  """Number of channels in the audio output.
115
133
 
116
134
  Returns:
117
- int: Number of channels (1 for mono, 2 for stereo)
135
+ Number of channels (1 for mono, 2 for stereo).
118
136
  """
119
137
  return self._num_channels
120
138
 
@@ -122,7 +140,7 @@ class AudioBufferProcessor(FrameProcessor):
122
140
  """Check if both user and bot audio buffers contain data.
123
141
 
124
142
  Returns:
125
- bool: True if both buffers contain audio data
143
+ True if both buffers contain audio data.
126
144
  """
127
145
  return self._buffer_has_audio(self._user_audio_buffer) and self._buffer_has_audio(
128
146
  self._bot_audio_buffer
@@ -135,7 +153,7 @@ class AudioBufferProcessor(FrameProcessor):
135
153
  on the left channel and bot audio on the right channel.
136
154
 
137
155
  Returns:
138
- bytes: Mixed audio data
156
+ Mixed audio data as bytes.
139
157
  """
140
158
  if self._num_channels == 1:
141
159
  return mix_audio(bytes(self._user_audio_buffer), bytes(self._bot_audio_buffer))
@@ -160,10 +178,16 @@ class AudioBufferProcessor(FrameProcessor):
160
178
  Calls audio handlers with any remaining buffered audio before stopping.
161
179
  """
162
180
  await self._call_on_audio_data_handler()
181
+ self._reset_recording()
163
182
  self._recording = False
164
183
 
165
184
  async def process_frame(self, frame: Frame, direction: FrameDirection):
166
- """Process incoming audio frames and manage audio buffers."""
185
+ """Process incoming audio frames and manage audio buffers.
186
+
187
+ Args:
188
+ frame: The frame to process.
189
+ direction: The direction of frame flow in the pipeline.
190
+ """
167
191
  await super().process_frame(frame, direction)
168
192
 
169
193
  # Update output sample rate if necessary.
@@ -172,8 +196,6 @@ class AudioBufferProcessor(FrameProcessor):
172
196
 
173
197
  if self._recording:
174
198
  await self._process_recording(frame)
175
- if self._enable_turn_audio:
176
- await self._process_turn_recording(frame)
177
199
 
178
200
  if isinstance(frame, (CancelFrame, EndFrame)):
179
201
  await self.stop_recording()
@@ -181,16 +203,19 @@ class AudioBufferProcessor(FrameProcessor):
181
203
  await self.push_frame(frame, direction)
182
204
 
183
205
  def _update_sample_rate(self, frame: StartFrame):
206
+ """Update the sample rate from the start frame."""
184
207
  self._sample_rate = self._init_sample_rate or frame.audio_out_sample_rate
185
208
  self._audio_buffer_size_1s = self._sample_rate * 2
186
209
 
187
210
  async def _process_recording(self, frame: Frame):
211
+ """Process audio frames for recording."""
212
+ resampled = None
188
213
  if isinstance(frame, InputAudioRawFrame):
189
214
  # Add silence if we need to.
190
215
  silence = self._compute_silence(self._last_user_frame_at)
191
216
  self._user_audio_buffer.extend(silence)
192
217
  # Add user audio.
193
- resampled = await self._resample_audio(frame)
218
+ resampled = await self._resample_input_audio(frame)
194
219
  self._user_audio_buffer.extend(resampled)
195
220
  # Save time of frame so we can compute silence.
196
221
  self._last_user_frame_at = time.time()
@@ -199,15 +224,21 @@ class AudioBufferProcessor(FrameProcessor):
199
224
  silence = self._compute_silence(self._last_bot_frame_at)
200
225
  self._bot_audio_buffer.extend(silence)
201
226
  # Add bot audio.
202
- resampled = await self._resample_audio(frame)
227
+ resampled = await self._resample_output_audio(frame)
203
228
  self._bot_audio_buffer.extend(resampled)
204
229
  # Save time of frame so we can compute silence.
205
230
  self._last_bot_frame_at = time.time()
206
231
 
207
232
  if self._buffer_size > 0 and len(self._user_audio_buffer) > self._buffer_size:
208
233
  await self._call_on_audio_data_handler()
234
+ self._reset_recording()
235
+
236
+ # Process turn recording with preprocessed data.
237
+ if self._enable_turn_audio:
238
+ await self._process_turn_recording(frame, resampled)
209
239
 
210
- async def _process_turn_recording(self, frame: Frame):
240
+ async def _process_turn_recording(self, frame: Frame, resampled_audio: Optional[bytes] = None):
241
+ """Process frames for turn-based audio recording."""
211
242
  if isinstance(frame, UserStartedSpeakingFrame):
212
243
  self._user_speaking = True
213
244
  elif isinstance(frame, UserStoppedSpeakingFrame):
@@ -225,9 +256,8 @@ class AudioBufferProcessor(FrameProcessor):
225
256
  self._bot_speaking = False
226
257
  self._bot_turn_audio_buffer = bytearray()
227
258
 
228
- if isinstance(frame, InputAudioRawFrame):
229
- resampled = await self._resample_audio(frame)
230
- self._user_turn_audio_buffer += resampled
259
+ if isinstance(frame, InputAudioRawFrame) and resampled_audio:
260
+ self._user_turn_audio_buffer.extend(resampled_audio)
231
261
  # In the case of the user, we need to keep a short buffer of audio
232
262
  # since VAD notification of when the user starts speaking comes
233
263
  # later.
@@ -237,11 +267,11 @@ class AudioBufferProcessor(FrameProcessor):
237
267
  ):
238
268
  discarded = len(self._user_turn_audio_buffer) - self._audio_buffer_size_1s
239
269
  self._user_turn_audio_buffer = self._user_turn_audio_buffer[discarded:]
240
- elif self._bot_speaking and isinstance(frame, OutputAudioRawFrame):
241
- resampled = await self._resample_audio(frame)
242
- self._bot_turn_audio_buffer += resampled
270
+ elif self._bot_speaking and isinstance(frame, OutputAudioRawFrame) and resampled_audio:
271
+ self._bot_turn_audio_buffer.extend(resampled_audio)
243
272
 
244
273
  async def _call_on_audio_data_handler(self):
274
+ """Call the audio data event handlers with buffered audio."""
245
275
  if not self.has_audio() or not self._recording:
246
276
  return
247
277
 
@@ -260,26 +290,37 @@ class AudioBufferProcessor(FrameProcessor):
260
290
  self._num_channels,
261
291
  )
262
292
 
263
- self._reset_audio_buffers()
264
-
265
293
  def _buffer_has_audio(self, buffer: bytearray) -> bool:
294
+ """Check if a buffer contains audio data."""
266
295
  return buffer is not None and len(buffer) > 0
267
296
 
268
297
  def _reset_recording(self):
298
+ """Reset recording state and buffers."""
269
299
  self._reset_audio_buffers()
270
300
  self._last_user_frame_at = time.time()
271
301
  self._last_bot_frame_at = time.time()
272
302
 
273
303
  def _reset_audio_buffers(self):
304
+ """Reset all audio buffers to empty state."""
274
305
  self._user_audio_buffer = bytearray()
275
306
  self._bot_audio_buffer = bytearray()
276
307
  self._user_turn_audio_buffer = bytearray()
277
308
  self._bot_turn_audio_buffer = bytearray()
278
309
 
279
- async def _resample_audio(self, frame: AudioRawFrame) -> bytes:
280
- return await self._resampler.resample(frame.audio, frame.sample_rate, self._sample_rate)
310
+ async def _resample_input_audio(self, frame: InputAudioRawFrame) -> bytes:
311
+ """Resample audio frame to the target sample rate."""
312
+ return await self._input_resampler.resample(
313
+ frame.audio, frame.sample_rate, self._sample_rate
314
+ )
315
+
316
+ async def _resample_output_audio(self, frame: OutputAudioRawFrame) -> bytes:
317
+ """Resample audio frame to the target sample rate."""
318
+ return await self._output_resampler.resample(
319
+ frame.audio, frame.sample_rate, self._sample_rate
320
+ )
281
321
 
282
322
  def _compute_silence(self, from_time: float) -> bytes:
323
+ """Compute silence to insert based on time gap."""
283
324
  quiet_time = time.time() - from_time
284
325
  # We should get audio frames very frequently. We introduce silence only
285
326
  # if there's a big enough gap of 1s.
@@ -4,21 +4,22 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """Consumer processor for consuming frames from ProducerProcessor queues."""
8
+
7
9
  import asyncio
8
10
  from typing import Awaitable, Callable, Optional
9
11
 
10
12
  from pipecat.frames.frames import CancelFrame, EndFrame, Frame, StartFrame
11
13
  from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
12
14
  from pipecat.processors.producer_processor import ProducerProcessor, identity_transformer
13
- from pipecat.utils.asyncio.watchdog_queue import WatchdogQueue
14
15
 
15
16
 
16
17
  class ConsumerProcessor(FrameProcessor):
17
- """This class passes-through frames and also consumes frames from a
18
- producer's queue. When a frame from a producer queue is received it will be
19
- pushed to the specified direction. The frames can be transformed into a
20
- different type of frame before being pushed.
18
+ """Frame processor that consumes frames from a ProducerProcessor's queue.
21
19
 
20
+ This processor passes through frames normally while also consuming frames
21
+ from a ProducerProcessor's queue. When frames are received from the producer
22
+ queue, they are optionally transformed and pushed in the specified direction.
22
23
  """
23
24
 
24
25
  def __init__(
@@ -29,6 +30,14 @@ class ConsumerProcessor(FrameProcessor):
29
30
  direction: FrameDirection = FrameDirection.DOWNSTREAM,
30
31
  **kwargs,
31
32
  ):
33
+ """Initialize the consumer processor.
34
+
35
+ Args:
36
+ producer: The producer processor to consume frames from.
37
+ transformer: Function to transform frames before pushing. Defaults to identity_transformer.
38
+ direction: Direction to push consumed frames. Defaults to DOWNSTREAM.
39
+ **kwargs: Additional arguments passed to parent class.
40
+ """
32
41
  super().__init__(**kwargs)
33
42
  self._transformer = transformer
34
43
  self._direction = direction
@@ -36,6 +45,12 @@ class ConsumerProcessor(FrameProcessor):
36
45
  self._consumer_task: Optional[asyncio.Task] = None
37
46
 
38
47
  async def process_frame(self, frame: Frame, direction: FrameDirection):
48
+ """Process incoming frames and handle lifecycle events.
49
+
50
+ Args:
51
+ frame: The frame to process.
52
+ direction: The direction the frame is traveling.
53
+ """
39
54
  await super().process_frame(frame, direction)
40
55
 
41
56
  if isinstance(frame, StartFrame):
@@ -48,19 +63,23 @@ class ConsumerProcessor(FrameProcessor):
48
63
  await self.push_frame(frame, direction)
49
64
 
50
65
  async def _start(self, _: StartFrame):
66
+ """Start the consumer task and register with the producer."""
51
67
  if not self._consumer_task:
52
- self._queue: WatchdogQueue = self._producer.add_consumer()
68
+ self._queue = self._producer.add_consumer()
53
69
  self._consumer_task = self.create_task(self._consumer_task_handler())
54
70
 
55
71
  async def _stop(self, _: EndFrame):
72
+ """Stop the consumer task."""
56
73
  if self._consumer_task:
57
74
  await self.cancel_task(self._consumer_task)
58
75
 
59
76
  async def _cancel(self, _: CancelFrame):
77
+ """Cancel the consumer task."""
60
78
  if self._consumer_task:
61
79
  await self.cancel_task(self._consumer_task)
62
80
 
63
81
  async def _consumer_task_handler(self):
82
+ """Handle consuming frames from the producer queue."""
64
83
  while True:
65
84
  frame = await self._queue.get()
66
85
  new_frame = await self._transformer(frame)
@@ -4,6 +4,8 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """Frame filtering processor for the Pipecat framework."""
8
+
7
9
  from typing import Tuple, Type
8
10
 
9
11
  from pipecat.frames.frames import EndFrame, Frame, SystemFrame
@@ -11,7 +13,21 @@ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
11
13
 
12
14
 
13
15
  class FrameFilter(FrameProcessor):
16
+ """A frame processor that filters frames based on their types.
17
+
18
+ This processor acts as a selective gate in the pipeline, allowing only
19
+ frames of specified types to pass through. System and end frames are
20
+ automatically allowed to pass through to maintain pipeline integrity.
21
+ """
22
+
14
23
  def __init__(self, types: Tuple[Type[Frame], ...]):
24
+ """Initialize the frame filter.
25
+
26
+ Args:
27
+ types: Tuple of frame types that should be allowed to pass through
28
+ the filter. All other frame types (except SystemFrame and
29
+ EndFrame) will be blocked.
30
+ """
15
31
  super().__init__()
16
32
  self._types = types
17
33
 
@@ -20,12 +36,19 @@ class FrameFilter(FrameProcessor):
20
36
  #
21
37
 
22
38
  def _should_passthrough_frame(self, frame):
39
+ """Determine if a frame should pass through the filter."""
23
40
  if isinstance(frame, self._types):
24
41
  return True
25
42
 
26
43
  return isinstance(frame, (EndFrame, SystemFrame))
27
44
 
28
45
  async def process_frame(self, frame: Frame, direction: FrameDirection):
46
+ """Process an incoming frame and conditionally pass it through.
47
+
48
+ Args:
49
+ frame: The frame to process.
50
+ direction: The direction of frame flow in the pipeline.
51
+ """
29
52
  await super().process_frame(frame, direction)
30
53
 
31
54
  if self._should_passthrough_frame(frame):
@@ -4,6 +4,12 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """Function-based frame filtering for Pipecat pipelines.
8
+
9
+ This module provides a processor that filters frames based on a custom function,
10
+ allowing for flexible frame filtering logic in processing pipelines.
11
+ """
12
+
7
13
  from typing import Awaitable, Callable
8
14
 
9
15
  from pipecat.frames.frames import EndFrame, Frame, SystemFrame
@@ -11,11 +17,26 @@ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
11
17
 
12
18
 
13
19
  class FunctionFilter(FrameProcessor):
20
+ """A frame processor that filters frames using a custom function.
21
+
22
+ This processor allows frames to pass through based on the result of a
23
+ user-provided filter function. System and end frames always pass through
24
+ regardless of the filter result.
25
+ """
26
+
14
27
  def __init__(
15
28
  self,
16
29
  filter: Callable[[Frame], Awaitable[bool]],
17
30
  direction: FrameDirection = FrameDirection.DOWNSTREAM,
18
31
  ):
32
+ """Initialize the function filter.
33
+
34
+ Args:
35
+ filter: An async function that takes a Frame and returns True if the
36
+ frame should pass through, False otherwise.
37
+ direction: The direction to apply filtering. Only frames moving in
38
+ this direction will be filtered. Defaults to DOWNSTREAM.
39
+ """
19
40
  super().__init__()
20
41
  self._filter = filter
21
42
  self._direction = direction
@@ -27,9 +48,18 @@ class FunctionFilter(FrameProcessor):
27
48
  # Ignore system frames, end frames and frames that are not following the
28
49
  # direction of this gate
29
50
  def _should_passthrough_frame(self, frame, direction):
51
+ """Check if a frame should pass through without filtering."""
52
+ # Ignore system frames, end frames and frames that are not following the
53
+ # direction of this gate
30
54
  return isinstance(frame, (SystemFrame, EndFrame)) or direction != self._direction
31
55
 
32
56
  async def process_frame(self, frame: Frame, direction: FrameDirection):
57
+ """Process a frame through the filter.
58
+
59
+ Args:
60
+ frame: The frame to process.
61
+ direction: The direction the frame is moving in the pipeline.
62
+ """
33
63
  await super().process_frame(frame, direction)
34
64
 
35
65
  passthrough = self._should_passthrough_frame(frame, direction)
@@ -4,6 +4,12 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """Identity filter for transparent frame passthrough.
8
+
9
+ This module provides a simple passthrough filter that forwards all frames
10
+ without modification, useful for testing and pipeline composition.
11
+ """
12
+
7
13
  from pipecat.frames.frames import Frame
8
14
  from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
9
15
 
@@ -14,10 +20,14 @@ class IdentityFilter(FrameProcessor):
14
20
  This filter acts as a transparent passthrough, allowing all frames to flow
15
21
  through unchanged. It can be useful when testing `ParallelPipeline` to
16
22
  create pipelines that pass through frames (no frames should be repeated).
17
-
18
23
  """
19
24
 
20
25
  def __init__(self, **kwargs):
26
+ """Initialize the identity filter.
27
+
28
+ Args:
29
+ **kwargs: Additional arguments passed to the parent FrameProcessor.
30
+ """
21
31
  super().__init__(**kwargs)
22
32
 
23
33
  #
@@ -25,6 +35,11 @@ class IdentityFilter(FrameProcessor):
25
35
  #
26
36
 
27
37
  async def process_frame(self, frame: Frame, direction: FrameDirection):
28
- """Process an incoming frame by passing it through unchanged."""
38
+ """Process an incoming frame by passing it through unchanged.
39
+
40
+ Args:
41
+ frame: The frame to process and forward.
42
+ direction: The direction of frame flow in the pipeline.
43
+ """
29
44
  await super().process_frame(frame, direction)
30
45
  await self.push_frame(frame, direction)
@@ -4,14 +4,31 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """Null filter processor for blocking frame transmission.
8
+
9
+ This module provides a frame processor that blocks all frames except
10
+ system and end frames, useful for testing or temporarily stopping
11
+ frame flow in a pipeline.
12
+ """
13
+
7
14
  from pipecat.frames.frames import EndFrame, Frame, SystemFrame
8
15
  from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
9
16
 
10
17
 
11
18
  class NullFilter(FrameProcessor):
12
- """This filter doesn't allow passing any frames up or downstream."""
19
+ """A filter that blocks all frames except system and end frames.
20
+
21
+ This processor acts as a null filter, preventing frames from passing
22
+ through the pipeline while still allowing essential system and end
23
+ frames to maintain proper pipeline operation.
24
+ """
13
25
 
14
26
  def __init__(self, **kwargs):
27
+ """Initialize the null filter.
28
+
29
+ Args:
30
+ **kwargs: Additional arguments passed to parent FrameProcessor.
31
+ """
15
32
  super().__init__(**kwargs)
16
33
 
17
34
  #
@@ -19,6 +36,12 @@ class NullFilter(FrameProcessor):
19
36
  #
20
37
 
21
38
  async def process_frame(self, frame: Frame, direction: FrameDirection):
39
+ """Process incoming frames, only allowing system and end frames through.
40
+
41
+ Args:
42
+ frame: The frame to process.
43
+ direction: The direction of frame flow in the pipeline.
44
+ """
22
45
  await super().process_frame(frame, direction)
23
46
 
24
47
  if isinstance(frame, (SystemFrame, EndFrame)):