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,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
|
-
"""
|
|
21
|
-
|
|
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
|
-
"""
|
|
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.
|
|
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
|
-
"""
|
|
111
|
+
"""Start all proxy observer tasks."""
|
|
82
112
|
self._proxies = self._create_proxies(self._observers)
|
|
83
113
|
|
|
84
114
|
async def stop(self):
|
|
85
|
-
"""
|
|
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
|
|
94
|
-
await proxy.queue.put(data)
|
|
131
|
+
"""Queue frame data for all managed observers.
|
|
95
132
|
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
"""
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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:
|