dv-pipecat-ai 0.0.74.dev770__py3-none-any.whl → 0.0.82.dev776__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dv-pipecat-ai might be problematic. Click here for more details.
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/METADATA +137 -93
- dv_pipecat_ai-0.0.82.dev776.dist-info/RECORD +340 -0
- pipecat/__init__.py +17 -0
- pipecat/adapters/base_llm_adapter.py +36 -1
- pipecat/adapters/schemas/direct_function.py +296 -0
- pipecat/adapters/schemas/function_schema.py +15 -6
- pipecat/adapters/schemas/tools_schema.py +55 -7
- pipecat/adapters/services/anthropic_adapter.py +22 -3
- pipecat/adapters/services/aws_nova_sonic_adapter.py +23 -3
- pipecat/adapters/services/bedrock_adapter.py +22 -3
- pipecat/adapters/services/gemini_adapter.py +16 -3
- pipecat/adapters/services/open_ai_adapter.py +17 -2
- pipecat/adapters/services/open_ai_realtime_adapter.py +23 -3
- pipecat/audio/filters/base_audio_filter.py +30 -6
- pipecat/audio/filters/koala_filter.py +37 -2
- pipecat/audio/filters/krisp_filter.py +59 -6
- pipecat/audio/filters/noisereduce_filter.py +37 -0
- pipecat/audio/interruptions/base_interruption_strategy.py +25 -5
- pipecat/audio/interruptions/min_words_interruption_strategy.py +21 -4
- pipecat/audio/mixers/base_audio_mixer.py +30 -7
- pipecat/audio/mixers/soundfile_mixer.py +53 -6
- pipecat/audio/resamplers/base_audio_resampler.py +17 -9
- pipecat/audio/resamplers/resampy_resampler.py +26 -1
- pipecat/audio/resamplers/soxr_resampler.py +32 -1
- pipecat/audio/resamplers/soxr_stream_resampler.py +101 -0
- pipecat/audio/utils.py +194 -1
- pipecat/audio/vad/silero.py +60 -3
- pipecat/audio/vad/vad_analyzer.py +114 -30
- pipecat/clocks/base_clock.py +19 -0
- pipecat/clocks/system_clock.py +25 -0
- pipecat/extensions/voicemail/__init__.py +0 -0
- pipecat/extensions/voicemail/voicemail_detector.py +707 -0
- pipecat/frames/frames.py +590 -156
- pipecat/metrics/metrics.py +64 -1
- pipecat/observers/base_observer.py +58 -19
- pipecat/observers/loggers/debug_log_observer.py +56 -64
- pipecat/observers/loggers/llm_log_observer.py +8 -1
- pipecat/observers/loggers/transcription_log_observer.py +19 -7
- pipecat/observers/loggers/user_bot_latency_log_observer.py +32 -5
- pipecat/observers/turn_tracking_observer.py +26 -1
- pipecat/pipeline/base_pipeline.py +5 -7
- pipecat/pipeline/base_task.py +52 -9
- pipecat/pipeline/parallel_pipeline.py +121 -177
- pipecat/pipeline/pipeline.py +129 -20
- pipecat/pipeline/runner.py +50 -1
- pipecat/pipeline/sync_parallel_pipeline.py +132 -32
- pipecat/pipeline/task.py +263 -280
- pipecat/pipeline/task_observer.py +85 -34
- pipecat/pipeline/to_be_updated/merge_pipeline.py +32 -2
- pipecat/processors/aggregators/dtmf_aggregator.py +29 -22
- pipecat/processors/aggregators/gated.py +25 -24
- pipecat/processors/aggregators/gated_openai_llm_context.py +22 -2
- pipecat/processors/aggregators/llm_response.py +398 -89
- pipecat/processors/aggregators/openai_llm_context.py +161 -13
- pipecat/processors/aggregators/sentence.py +25 -14
- pipecat/processors/aggregators/user_response.py +28 -3
- pipecat/processors/aggregators/vision_image_frame.py +24 -14
- pipecat/processors/async_generator.py +28 -0
- pipecat/processors/audio/audio_buffer_processor.py +78 -37
- pipecat/processors/consumer_processor.py +25 -6
- pipecat/processors/filters/frame_filter.py +23 -0
- pipecat/processors/filters/function_filter.py +30 -0
- pipecat/processors/filters/identity_filter.py +17 -2
- pipecat/processors/filters/null_filter.py +24 -1
- pipecat/processors/filters/stt_mute_filter.py +56 -21
- pipecat/processors/filters/wake_check_filter.py +46 -3
- pipecat/processors/filters/wake_notifier_filter.py +21 -3
- pipecat/processors/frame_processor.py +488 -131
- pipecat/processors/frameworks/langchain.py +38 -3
- pipecat/processors/frameworks/rtvi.py +719 -34
- pipecat/processors/gstreamer/pipeline_source.py +41 -0
- pipecat/processors/idle_frame_processor.py +26 -3
- pipecat/processors/logger.py +23 -0
- pipecat/processors/metrics/frame_processor_metrics.py +77 -4
- pipecat/processors/metrics/sentry.py +42 -4
- pipecat/processors/producer_processor.py +34 -14
- pipecat/processors/text_transformer.py +22 -10
- pipecat/processors/transcript_processor.py +48 -29
- pipecat/processors/user_idle_processor.py +31 -21
- pipecat/runner/__init__.py +1 -0
- pipecat/runner/daily.py +132 -0
- pipecat/runner/livekit.py +148 -0
- pipecat/runner/run.py +543 -0
- pipecat/runner/types.py +67 -0
- pipecat/runner/utils.py +515 -0
- pipecat/serializers/base_serializer.py +42 -0
- pipecat/serializers/exotel.py +17 -6
- pipecat/serializers/genesys.py +95 -0
- pipecat/serializers/livekit.py +33 -0
- pipecat/serializers/plivo.py +16 -15
- pipecat/serializers/protobuf.py +37 -1
- pipecat/serializers/telnyx.py +18 -17
- pipecat/serializers/twilio.py +32 -16
- pipecat/services/ai_service.py +5 -3
- pipecat/services/anthropic/llm.py +113 -43
- pipecat/services/assemblyai/models.py +63 -5
- pipecat/services/assemblyai/stt.py +64 -11
- pipecat/services/asyncai/__init__.py +0 -0
- pipecat/services/asyncai/tts.py +501 -0
- pipecat/services/aws/llm.py +185 -111
- pipecat/services/aws/stt.py +217 -23
- pipecat/services/aws/tts.py +118 -52
- pipecat/services/aws/utils.py +101 -5
- pipecat/services/aws_nova_sonic/aws.py +82 -64
- pipecat/services/aws_nova_sonic/context.py +15 -6
- pipecat/services/azure/common.py +10 -2
- pipecat/services/azure/image.py +32 -0
- pipecat/services/azure/llm.py +9 -7
- pipecat/services/azure/stt.py +65 -2
- pipecat/services/azure/tts.py +154 -23
- pipecat/services/cartesia/stt.py +125 -8
- pipecat/services/cartesia/tts.py +102 -38
- pipecat/services/cerebras/llm.py +15 -23
- pipecat/services/deepgram/stt.py +19 -11
- pipecat/services/deepgram/tts.py +36 -0
- pipecat/services/deepseek/llm.py +14 -23
- pipecat/services/elevenlabs/tts.py +330 -64
- pipecat/services/fal/image.py +43 -0
- pipecat/services/fal/stt.py +48 -10
- pipecat/services/fireworks/llm.py +14 -21
- pipecat/services/fish/tts.py +109 -9
- pipecat/services/gemini_multimodal_live/__init__.py +1 -0
- pipecat/services/gemini_multimodal_live/events.py +83 -2
- pipecat/services/gemini_multimodal_live/file_api.py +189 -0
- pipecat/services/gemini_multimodal_live/gemini.py +218 -21
- pipecat/services/gladia/config.py +17 -10
- pipecat/services/gladia/stt.py +82 -36
- pipecat/services/google/frames.py +40 -0
- pipecat/services/google/google.py +2 -0
- pipecat/services/google/image.py +39 -2
- pipecat/services/google/llm.py +176 -58
- pipecat/services/google/llm_openai.py +26 -4
- pipecat/services/google/llm_vertex.py +37 -15
- pipecat/services/google/rtvi.py +41 -0
- pipecat/services/google/stt.py +65 -17
- pipecat/services/google/test-google-chirp.py +45 -0
- pipecat/services/google/tts.py +390 -19
- pipecat/services/grok/llm.py +8 -6
- pipecat/services/groq/llm.py +8 -6
- pipecat/services/groq/stt.py +13 -9
- pipecat/services/groq/tts.py +40 -0
- pipecat/services/hamsa/__init__.py +9 -0
- pipecat/services/hamsa/stt.py +241 -0
- pipecat/services/heygen/__init__.py +5 -0
- pipecat/services/heygen/api.py +281 -0
- pipecat/services/heygen/client.py +620 -0
- pipecat/services/heygen/video.py +338 -0
- pipecat/services/image_service.py +5 -3
- pipecat/services/inworld/__init__.py +1 -0
- pipecat/services/inworld/tts.py +592 -0
- pipecat/services/llm_service.py +127 -45
- pipecat/services/lmnt/tts.py +80 -7
- pipecat/services/mcp_service.py +85 -44
- pipecat/services/mem0/memory.py +42 -13
- pipecat/services/minimax/tts.py +74 -15
- pipecat/services/mistral/__init__.py +0 -0
- pipecat/services/mistral/llm.py +185 -0
- pipecat/services/moondream/vision.py +55 -10
- pipecat/services/neuphonic/tts.py +275 -48
- pipecat/services/nim/llm.py +8 -6
- pipecat/services/ollama/llm.py +27 -7
- pipecat/services/openai/base_llm.py +54 -16
- pipecat/services/openai/image.py +30 -0
- pipecat/services/openai/llm.py +7 -5
- pipecat/services/openai/stt.py +13 -9
- pipecat/services/openai/tts.py +42 -10
- pipecat/services/openai_realtime_beta/azure.py +11 -9
- pipecat/services/openai_realtime_beta/context.py +7 -5
- pipecat/services/openai_realtime_beta/events.py +10 -7
- pipecat/services/openai_realtime_beta/openai.py +37 -18
- pipecat/services/openpipe/llm.py +30 -24
- pipecat/services/openrouter/llm.py +9 -7
- pipecat/services/perplexity/llm.py +15 -19
- pipecat/services/piper/tts.py +26 -12
- pipecat/services/playht/tts.py +227 -65
- pipecat/services/qwen/llm.py +8 -6
- pipecat/services/rime/tts.py +128 -17
- pipecat/services/riva/stt.py +160 -22
- pipecat/services/riva/tts.py +67 -2
- pipecat/services/sambanova/llm.py +19 -17
- pipecat/services/sambanova/stt.py +14 -8
- pipecat/services/sarvam/tts.py +60 -13
- pipecat/services/simli/video.py +82 -21
- pipecat/services/soniox/__init__.py +0 -0
- pipecat/services/soniox/stt.py +398 -0
- pipecat/services/speechmatics/stt.py +29 -17
- pipecat/services/stt_service.py +47 -11
- pipecat/services/tavus/video.py +94 -25
- pipecat/services/together/llm.py +8 -6
- pipecat/services/tts_service.py +77 -53
- pipecat/services/ultravox/stt.py +46 -43
- pipecat/services/vision_service.py +5 -3
- pipecat/services/websocket_service.py +12 -11
- pipecat/services/whisper/base_stt.py +58 -12
- pipecat/services/whisper/stt.py +69 -58
- pipecat/services/xtts/tts.py +59 -2
- pipecat/sync/base_notifier.py +19 -0
- pipecat/sync/event_notifier.py +24 -0
- pipecat/tests/utils.py +73 -5
- pipecat/transcriptions/language.py +24 -0
- pipecat/transports/base_input.py +112 -8
- pipecat/transports/base_output.py +235 -13
- pipecat/transports/base_transport.py +119 -0
- pipecat/transports/local/audio.py +76 -0
- pipecat/transports/local/tk.py +84 -0
- pipecat/transports/network/fastapi_websocket.py +174 -15
- pipecat/transports/network/small_webrtc.py +383 -39
- pipecat/transports/network/webrtc_connection.py +214 -8
- pipecat/transports/network/websocket_client.py +171 -1
- pipecat/transports/network/websocket_server.py +147 -9
- pipecat/transports/services/daily.py +792 -70
- pipecat/transports/services/helpers/daily_rest.py +122 -129
- pipecat/transports/services/livekit.py +339 -4
- pipecat/transports/services/tavus.py +273 -38
- pipecat/utils/asyncio/task_manager.py +92 -186
- pipecat/utils/base_object.py +83 -1
- pipecat/utils/network.py +2 -0
- pipecat/utils/string.py +114 -58
- pipecat/utils/text/base_text_aggregator.py +44 -13
- pipecat/utils/text/base_text_filter.py +46 -0
- pipecat/utils/text/markdown_text_filter.py +70 -14
- pipecat/utils/text/pattern_pair_aggregator.py +18 -14
- pipecat/utils/text/simple_text_aggregator.py +43 -2
- pipecat/utils/text/skip_tags_aggregator.py +21 -13
- pipecat/utils/time.py +36 -0
- pipecat/utils/tracing/class_decorators.py +32 -7
- pipecat/utils/tracing/conversation_context_provider.py +12 -2
- pipecat/utils/tracing/service_attributes.py +80 -64
- pipecat/utils/tracing/service_decorators.py +48 -21
- pipecat/utils/tracing/setup.py +13 -7
- pipecat/utils/tracing/turn_context_provider.py +12 -2
- pipecat/utils/tracing/turn_trace_observer.py +27 -0
- pipecat/utils/utils.py +14 -14
- dv_pipecat_ai-0.0.74.dev770.dist-info/RECORD +0 -319
- pipecat/examples/daily_runner.py +0 -64
- pipecat/examples/run.py +0 -265
- pipecat/utils/asyncio/watchdog_async_iterator.py +0 -72
- pipecat/utils/asyncio/watchdog_event.py +0 -42
- pipecat/utils/asyncio/watchdog_priority_queue.py +0 -48
- pipecat/utils/asyncio/watchdog_queue.py +0 -48
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/WHEEL +0 -0
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/licenses/LICENSE +0 -0
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/top_level.txt +0 -0
- /pipecat/{examples → extensions}/__init__.py +0 -0
|
@@ -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
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
280
|
-
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
"""
|
|
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)):
|