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
pipecat/runner/utils.py
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2024–2025, Daily
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""Transport utility functions and FastAPI route setup helpers.
|
|
8
|
+
|
|
9
|
+
This module provides common functionality for setting up transport-specific
|
|
10
|
+
FastAPI routes and handling WebRTC/WebSocket connections. It includes SDP
|
|
11
|
+
manipulation utilities for WebRTC compatibility and transport detection helpers.
|
|
12
|
+
|
|
13
|
+
Key features:
|
|
14
|
+
|
|
15
|
+
- WebRTC route setup with connection management
|
|
16
|
+
- WebSocket route setup for telephony providers
|
|
17
|
+
- SDP munging for ESP32 and other WebRTC compatibility
|
|
18
|
+
- Transport client ID detection across different transport types
|
|
19
|
+
- Video capture utilities for Daily transports
|
|
20
|
+
|
|
21
|
+
The utilities are designed to be transport-agnostic where possible, with
|
|
22
|
+
specific handlers for each transport type's unique requirements.
|
|
23
|
+
|
|
24
|
+
Example::
|
|
25
|
+
|
|
26
|
+
from pipecat.runner.utils import parse_telephony_websocket
|
|
27
|
+
|
|
28
|
+
async def telephony_websocket_handler(websocket: WebSocket):
|
|
29
|
+
transport_type, call_data = await parse_telephony_websocket(websocket)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import re
|
|
35
|
+
from typing import Any, Callable, Dict, Optional
|
|
36
|
+
|
|
37
|
+
from fastapi import WebSocket
|
|
38
|
+
from loguru import logger
|
|
39
|
+
|
|
40
|
+
from pipecat.runner.types import (
|
|
41
|
+
DailyRunnerArguments,
|
|
42
|
+
SmallWebRTCRunnerArguments,
|
|
43
|
+
WebSocketRunnerArguments,
|
|
44
|
+
)
|
|
45
|
+
from pipecat.transports.base_transport import BaseTransport
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _detect_transport_type_from_message(message_data: dict) -> str:
|
|
49
|
+
"""Attempt to auto-detect transport type from WebSocket message structure."""
|
|
50
|
+
logger.trace("=== Auto-Detection Analysis ===")
|
|
51
|
+
|
|
52
|
+
# Twilio detection
|
|
53
|
+
if (
|
|
54
|
+
message_data.get("event") == "start"
|
|
55
|
+
and "start" in message_data
|
|
56
|
+
and "streamSid" in message_data.get("start", {})
|
|
57
|
+
and "callSid" in message_data.get("start", {})
|
|
58
|
+
):
|
|
59
|
+
logger.trace("Auto-detected: TWILIO")
|
|
60
|
+
return "twilio"
|
|
61
|
+
|
|
62
|
+
# Telnyx detection
|
|
63
|
+
if (
|
|
64
|
+
"stream_id" in message_data
|
|
65
|
+
and "start" in message_data
|
|
66
|
+
and "call_control_id" in message_data.get("start", {})
|
|
67
|
+
):
|
|
68
|
+
logger.trace("Auto-detected: TELNYX")
|
|
69
|
+
return "telnyx"
|
|
70
|
+
|
|
71
|
+
# Plivo detection
|
|
72
|
+
if (
|
|
73
|
+
"start" in message_data
|
|
74
|
+
and "streamId" in message_data.get("start", {})
|
|
75
|
+
and "callId" in message_data.get("start", {})
|
|
76
|
+
):
|
|
77
|
+
logger.trace("Auto-detected: PLIVO")
|
|
78
|
+
return "plivo"
|
|
79
|
+
|
|
80
|
+
# Exotel detection
|
|
81
|
+
if (
|
|
82
|
+
message_data.get("event") == "start"
|
|
83
|
+
and "start" in message_data
|
|
84
|
+
and "stream_sid" in message_data.get("start", {})
|
|
85
|
+
and "call_sid" in message_data.get("start", {})
|
|
86
|
+
and "account_sid" in message_data.get("start", {})
|
|
87
|
+
):
|
|
88
|
+
logger.trace("Auto-detected: EXOTEL")
|
|
89
|
+
return "exotel"
|
|
90
|
+
|
|
91
|
+
logger.trace("Auto-detection failed - unknown format")
|
|
92
|
+
return "unknown"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def parse_telephony_websocket(websocket: WebSocket):
|
|
96
|
+
"""Parse telephony WebSocket messages and return transport type and call data.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
tuple: (transport_type: str, call_data: dict)
|
|
100
|
+
|
|
101
|
+
call_data contains provider-specific fields:
|
|
102
|
+
- Twilio: {"stream_id": str, "call_id": str}
|
|
103
|
+
- Telnyx: {"stream_id": str, "call_control_id": str, "outbound_encoding": str}
|
|
104
|
+
- Plivo: {"stream_id": str, "call_id": str}
|
|
105
|
+
- Exotel: {"stream_id": str, "call_id": str, "account_sid": str}
|
|
106
|
+
|
|
107
|
+
Example usage::
|
|
108
|
+
|
|
109
|
+
transport_type, call_data = await parse_telephony_websocket(websocket)
|
|
110
|
+
if transport_type == "telnyx":
|
|
111
|
+
outbound_encoding = call_data["outbound_encoding"]
|
|
112
|
+
"""
|
|
113
|
+
# Read first two messages
|
|
114
|
+
start_data = websocket.iter_text()
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
# First message
|
|
118
|
+
first_message_raw = await start_data.__anext__()
|
|
119
|
+
logger.trace(f"First message: {first_message_raw}")
|
|
120
|
+
try:
|
|
121
|
+
first_message = json.loads(first_message_raw)
|
|
122
|
+
except json.JSONDecodeError:
|
|
123
|
+
first_message = {}
|
|
124
|
+
|
|
125
|
+
# Second message
|
|
126
|
+
second_message_raw = await start_data.__anext__()
|
|
127
|
+
logger.trace(f"Second message: {second_message_raw}")
|
|
128
|
+
try:
|
|
129
|
+
second_message = json.loads(second_message_raw)
|
|
130
|
+
except json.JSONDecodeError:
|
|
131
|
+
second_message = {}
|
|
132
|
+
|
|
133
|
+
# Try auto-detection on both messages
|
|
134
|
+
detected_type_first = _detect_transport_type_from_message(first_message)
|
|
135
|
+
detected_type_second = _detect_transport_type_from_message(second_message)
|
|
136
|
+
|
|
137
|
+
# Use the successful detection
|
|
138
|
+
if detected_type_first != "unknown":
|
|
139
|
+
transport_type = detected_type_first
|
|
140
|
+
call_data_raw = first_message
|
|
141
|
+
logger.debug(f"Detected transport: {transport_type} (from first message)")
|
|
142
|
+
elif detected_type_second != "unknown":
|
|
143
|
+
transport_type = detected_type_second
|
|
144
|
+
call_data_raw = second_message
|
|
145
|
+
logger.debug(f"Detected transport: {transport_type} (from second message)")
|
|
146
|
+
else:
|
|
147
|
+
transport_type = "unknown"
|
|
148
|
+
call_data_raw = second_message
|
|
149
|
+
logger.warning("Could not auto-detect transport type")
|
|
150
|
+
|
|
151
|
+
# Extract provider-specific data
|
|
152
|
+
if transport_type == "twilio":
|
|
153
|
+
start_data = call_data_raw.get("start", {})
|
|
154
|
+
call_data = {
|
|
155
|
+
"stream_id": start_data.get("streamSid"),
|
|
156
|
+
"call_id": start_data.get("callSid"),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
elif transport_type == "telnyx":
|
|
160
|
+
call_data = {
|
|
161
|
+
"stream_id": call_data_raw.get("stream_id"),
|
|
162
|
+
"call_control_id": call_data_raw.get("start", {}).get("call_control_id"),
|
|
163
|
+
"outbound_encoding": call_data_raw.get("start", {})
|
|
164
|
+
.get("media_format", {})
|
|
165
|
+
.get("encoding"),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
elif transport_type == "plivo":
|
|
169
|
+
start_data = call_data_raw.get("start", {})
|
|
170
|
+
call_data = {
|
|
171
|
+
"stream_id": start_data.get("streamId"),
|
|
172
|
+
"call_id": start_data.get("callId"),
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
elif transport_type == "exotel":
|
|
176
|
+
start_data = call_data_raw.get("start", {})
|
|
177
|
+
call_data = {
|
|
178
|
+
"stream_id": start_data.get("stream_sid"),
|
|
179
|
+
"call_id": start_data.get("call_sid"),
|
|
180
|
+
"account_sid": start_data.get("account_sid"),
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
else:
|
|
184
|
+
call_data = {}
|
|
185
|
+
|
|
186
|
+
logger.debug(f"Parsed - Type: {transport_type}, Data: {call_data}")
|
|
187
|
+
return transport_type, call_data
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.error(f"Error parsing telephony WebSocket: {e}")
|
|
191
|
+
raise
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_transport_client_id(transport: BaseTransport, client: Any) -> str:
|
|
195
|
+
"""Get client identifier from transport-specific client object.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
transport: The transport instance.
|
|
199
|
+
client: Transport-specific client object.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Client identifier string, empty if transport not supported.
|
|
203
|
+
"""
|
|
204
|
+
# Import conditionally to avoid dependency issues
|
|
205
|
+
try:
|
|
206
|
+
from pipecat.transports.network.small_webrtc import SmallWebRTCTransport
|
|
207
|
+
|
|
208
|
+
if isinstance(transport, SmallWebRTCTransport):
|
|
209
|
+
return client.pc_id
|
|
210
|
+
except ImportError:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
from pipecat.transports.services.daily import DailyTransport
|
|
215
|
+
|
|
216
|
+
if isinstance(transport, DailyTransport):
|
|
217
|
+
return client["id"]
|
|
218
|
+
except ImportError:
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
logger.warning(f"Unable to get client id from unsupported transport {type(transport)}")
|
|
222
|
+
return ""
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def maybe_capture_participant_camera(
|
|
226
|
+
transport: BaseTransport, client: Any, framerate: int = 0
|
|
227
|
+
):
|
|
228
|
+
"""Capture participant camera video if transport supports it.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
transport: The transport instance.
|
|
232
|
+
client: Transport-specific client object.
|
|
233
|
+
framerate: Video capture framerate. Defaults to 0 (auto).
|
|
234
|
+
"""
|
|
235
|
+
try:
|
|
236
|
+
from pipecat.transports.services.daily import DailyTransport
|
|
237
|
+
|
|
238
|
+
if isinstance(transport, DailyTransport):
|
|
239
|
+
await transport.capture_participant_video(
|
|
240
|
+
client["id"], framerate=framerate, video_source="camera"
|
|
241
|
+
)
|
|
242
|
+
except ImportError:
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
async def maybe_capture_participant_screen(
|
|
247
|
+
transport: BaseTransport, client: Any, framerate: int = 0
|
|
248
|
+
):
|
|
249
|
+
"""Capture participant screen video if transport supports it.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
transport: The transport instance.
|
|
253
|
+
client: Transport-specific client object.
|
|
254
|
+
framerate: Video capture framerate. Defaults to 0 (auto).
|
|
255
|
+
"""
|
|
256
|
+
try:
|
|
257
|
+
from pipecat.transports.services.daily import DailyTransport
|
|
258
|
+
|
|
259
|
+
if isinstance(transport, DailyTransport):
|
|
260
|
+
await transport.capture_participant_video(
|
|
261
|
+
client["id"], framerate=framerate, video_source="screenVideo"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
except ImportError:
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _smallwebrtc_sdp_cleanup_ice_candidates(text: str, pattern: str) -> str:
|
|
269
|
+
"""Clean up ICE candidates in SDP text for SmallWebRTC.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
text: SDP text to clean up.
|
|
273
|
+
pattern: Pattern to match for candidate filtering.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Cleaned SDP text with filtered ICE candidates.
|
|
277
|
+
"""
|
|
278
|
+
result = []
|
|
279
|
+
lines = text.splitlines()
|
|
280
|
+
for line in lines:
|
|
281
|
+
if re.search("a=candidate", line):
|
|
282
|
+
if re.search(pattern, line) and not re.search("raddr", line):
|
|
283
|
+
result.append(line)
|
|
284
|
+
else:
|
|
285
|
+
result.append(line)
|
|
286
|
+
return "\r\n".join(result)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _smallwebrtc_sdp_cleanup_fingerprints(text: str) -> str:
|
|
290
|
+
"""Remove unsupported fingerprint algorithms from SDP text.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
text: SDP text to clean up.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
SDP text with sha-384 and sha-512 fingerprints removed.
|
|
297
|
+
"""
|
|
298
|
+
result = []
|
|
299
|
+
lines = text.splitlines()
|
|
300
|
+
for line in lines:
|
|
301
|
+
if not re.search("sha-384", line) and not re.search("sha-512", line):
|
|
302
|
+
result.append(line)
|
|
303
|
+
return "\r\n".join(result)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def smallwebrtc_sdp_munging(sdp: str, host: str) -> str:
|
|
307
|
+
"""Apply SDP modifications for SmallWebRTC compatibility.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
sdp: Original SDP string.
|
|
311
|
+
host: Host address for ICE candidate filtering.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Modified SDP string with fingerprint and ICE candidate cleanup.
|
|
315
|
+
"""
|
|
316
|
+
sdp = _smallwebrtc_sdp_cleanup_fingerprints(sdp)
|
|
317
|
+
sdp = _smallwebrtc_sdp_cleanup_ice_candidates(sdp, host)
|
|
318
|
+
return sdp
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _get_transport_params(transport_key: str, transport_params: Dict[str, Callable]) -> Any:
|
|
322
|
+
"""Get transport parameters from factory function.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
transport_key: The transport key to look up
|
|
326
|
+
transport_params: Dict mapping transport names to parameter factory functions
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Transport parameters from the factory function
|
|
330
|
+
|
|
331
|
+
Raises:
|
|
332
|
+
ValueError: If transport key is missing from transport_params
|
|
333
|
+
"""
|
|
334
|
+
if transport_key not in transport_params:
|
|
335
|
+
raise ValueError(
|
|
336
|
+
f"Missing transport params for '{transport_key}'. "
|
|
337
|
+
f"Please add '{transport_key}' key to your transport_params dict."
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
params = transport_params[transport_key]()
|
|
341
|
+
logger.debug(f"Using transport params for {transport_key}")
|
|
342
|
+
return params
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
async def _create_telephony_transport(
|
|
346
|
+
websocket: WebSocket,
|
|
347
|
+
params: Optional[Any] = None,
|
|
348
|
+
transport_type: str = None,
|
|
349
|
+
call_data: dict = None,
|
|
350
|
+
) -> BaseTransport:
|
|
351
|
+
"""Create a telephony transport with pre-parsed WebSocket data.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
websocket: FastAPI WebSocket connection from telephony provider
|
|
355
|
+
params: FastAPIWebsocketParams (required)
|
|
356
|
+
transport_type: Pre-detected provider type ("twilio", "telnyx", "plivo")
|
|
357
|
+
call_data: Pre-parsed call data dict with provider-specific fields
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Configured FastAPIWebsocketTransport ready for telephony use.
|
|
361
|
+
"""
|
|
362
|
+
from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketTransport
|
|
363
|
+
|
|
364
|
+
if params is None:
|
|
365
|
+
raise ValueError(
|
|
366
|
+
"FastAPIWebsocketParams must be provided. "
|
|
367
|
+
"The serializer and add_wav_header will be set automatically."
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Always set add_wav_header to False for telephony
|
|
371
|
+
params.add_wav_header = False
|
|
372
|
+
|
|
373
|
+
logger.info(f"Using pre-detected telephony provider: {transport_type}")
|
|
374
|
+
|
|
375
|
+
if transport_type == "twilio":
|
|
376
|
+
from pipecat.serializers.twilio import TwilioFrameSerializer
|
|
377
|
+
|
|
378
|
+
params.serializer = TwilioFrameSerializer(
|
|
379
|
+
stream_sid=call_data["stream_id"],
|
|
380
|
+
call_sid=call_data["call_id"],
|
|
381
|
+
account_sid=os.getenv("TWILIO_ACCOUNT_SID", ""),
|
|
382
|
+
auth_token=os.getenv("TWILIO_AUTH_TOKEN", ""),
|
|
383
|
+
)
|
|
384
|
+
elif transport_type == "telnyx":
|
|
385
|
+
from pipecat.serializers.telnyx import TelnyxFrameSerializer
|
|
386
|
+
|
|
387
|
+
params.serializer = TelnyxFrameSerializer(
|
|
388
|
+
stream_id=call_data["stream_id"],
|
|
389
|
+
call_control_id=call_data["call_control_id"],
|
|
390
|
+
outbound_encoding=call_data["outbound_encoding"],
|
|
391
|
+
inbound_encoding="PCMU", # Standard default
|
|
392
|
+
api_key=os.getenv("TELNYX_API_KEY", ""),
|
|
393
|
+
)
|
|
394
|
+
elif transport_type == "plivo":
|
|
395
|
+
from pipecat.serializers.plivo import PlivoFrameSerializer
|
|
396
|
+
|
|
397
|
+
params.serializer = PlivoFrameSerializer(
|
|
398
|
+
stream_id=call_data["stream_id"],
|
|
399
|
+
call_id=call_data["call_id"],
|
|
400
|
+
auth_id=os.getenv("PLIVO_AUTH_ID", ""),
|
|
401
|
+
auth_token=os.getenv("PLIVO_AUTH_TOKEN", ""),
|
|
402
|
+
)
|
|
403
|
+
elif transport_type == "exotel":
|
|
404
|
+
from pipecat.serializers.exotel import ExotelFrameSerializer
|
|
405
|
+
|
|
406
|
+
params.serializer = ExotelFrameSerializer(
|
|
407
|
+
stream_sid=call_data["stream_id"],
|
|
408
|
+
call_sid=call_data["call_id"],
|
|
409
|
+
)
|
|
410
|
+
else:
|
|
411
|
+
raise ValueError(
|
|
412
|
+
f"Unsupported telephony provider: {transport_type}. "
|
|
413
|
+
f"Supported providers: twilio, telnyx, plivo, exotel"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return FastAPIWebsocketTransport(websocket=websocket, params=params)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
async def create_transport(
|
|
420
|
+
runner_args: Any, transport_params: Dict[str, Callable]
|
|
421
|
+
) -> BaseTransport:
|
|
422
|
+
"""Create a transport from runner arguments using factory functions.
|
|
423
|
+
|
|
424
|
+
This function uses the clean transport_params factory pattern where users
|
|
425
|
+
define a dictionary mapping transport names to parameter factory functions.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
runner_args: Arguments from the runner.
|
|
429
|
+
transport_params: Dict mapping transport names to parameter factory functions.
|
|
430
|
+
Keys should be: "daily", "webrtc", "twilio", "telnyx", "plivo", "exotel"
|
|
431
|
+
Values should be functions that return transport parameters when called.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Configured transport instance.
|
|
435
|
+
|
|
436
|
+
Raises:
|
|
437
|
+
ValueError: If transport key is missing from transport_params or runner_args type is unsupported.
|
|
438
|
+
ImportError: If required dependencies are not installed.
|
|
439
|
+
|
|
440
|
+
Example::
|
|
441
|
+
|
|
442
|
+
transport_params = {
|
|
443
|
+
"daily": lambda: DailyParams(
|
|
444
|
+
audio_in_enabled=True,
|
|
445
|
+
audio_out_enabled=True,
|
|
446
|
+
vad_analyzer=SileroVADAnalyzer(),
|
|
447
|
+
),
|
|
448
|
+
"webrtc": lambda: TransportParams(
|
|
449
|
+
audio_in_enabled=True,
|
|
450
|
+
audio_out_enabled=True,
|
|
451
|
+
vad_analyzer=SileroVADAnalyzer(),
|
|
452
|
+
),
|
|
453
|
+
"twilio": lambda: FastAPIWebsocketParams(
|
|
454
|
+
audio_in_enabled=True,
|
|
455
|
+
audio_out_enabled=True,
|
|
456
|
+
vad_analyzer=SileroVADAnalyzer(),
|
|
457
|
+
# add_wav_header and serializer will be set automatically
|
|
458
|
+
),
|
|
459
|
+
"telnyx": lambda: FastAPIWebsocketParams(
|
|
460
|
+
audio_in_enabled=True,
|
|
461
|
+
audio_out_enabled=True,
|
|
462
|
+
vad_analyzer=SileroVADAnalyzer(),
|
|
463
|
+
# add_wav_header and serializer will be set automatically
|
|
464
|
+
),
|
|
465
|
+
"plivo": lambda: FastAPIWebsocketParams(
|
|
466
|
+
audio_in_enabled=True,
|
|
467
|
+
audio_out_enabled=True,
|
|
468
|
+
vad_analyzer=SileroVADAnalyzer(),
|
|
469
|
+
# add_wav_header and serializer will be set automatically
|
|
470
|
+
),
|
|
471
|
+
"exotel": lambda: FastAPIWebsocketParams(
|
|
472
|
+
audio_in_enabled=True,
|
|
473
|
+
audio_out_enabled=True,
|
|
474
|
+
vad_analyzer=SileroVADAnalyzer(),
|
|
475
|
+
# add_wav_header and serializer will be set automatically
|
|
476
|
+
),
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
transport = await create_transport(runner_args, transport_params)
|
|
480
|
+
"""
|
|
481
|
+
# Create transport based on runner args type
|
|
482
|
+
if isinstance(runner_args, DailyRunnerArguments):
|
|
483
|
+
params = _get_transport_params("daily", transport_params)
|
|
484
|
+
|
|
485
|
+
from pipecat.transports.services.daily import DailyTransport
|
|
486
|
+
|
|
487
|
+
return DailyTransport(
|
|
488
|
+
runner_args.room_url,
|
|
489
|
+
runner_args.token,
|
|
490
|
+
"Pipecat Bot",
|
|
491
|
+
params=params,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
elif isinstance(runner_args, SmallWebRTCRunnerArguments):
|
|
495
|
+
params = _get_transport_params("webrtc", transport_params)
|
|
496
|
+
|
|
497
|
+
from pipecat.transports.network.small_webrtc import SmallWebRTCTransport
|
|
498
|
+
|
|
499
|
+
return SmallWebRTCTransport(
|
|
500
|
+
params=params,
|
|
501
|
+
webrtc_connection=runner_args.webrtc_connection,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
elif isinstance(runner_args, WebSocketRunnerArguments):
|
|
505
|
+
# Parse once to determine the provider and get data
|
|
506
|
+
transport_type, call_data = await parse_telephony_websocket(runner_args.websocket)
|
|
507
|
+
params = _get_transport_params(transport_type, transport_params)
|
|
508
|
+
|
|
509
|
+
# Create telephony transport with pre-parsed data
|
|
510
|
+
return await _create_telephony_transport(
|
|
511
|
+
runner_args.websocket, params, transport_type, call_data
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
else:
|
|
515
|
+
raise ValueError(f"Unsupported runner arguments type: {type(runner_args)}")
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
5
|
#
|
|
6
6
|
|
|
7
|
+
"""Frame serialization interfaces for Pipecat."""
|
|
8
|
+
|
|
7
9
|
from abc import ABC, abstractmethod
|
|
8
10
|
from enum import Enum
|
|
9
11
|
|
|
@@ -11,23 +13,63 @@ from pipecat.frames.frames import Frame, StartFrame
|
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class FrameSerializerType(Enum):
|
|
16
|
+
"""Enumeration of supported frame serialization formats.
|
|
17
|
+
|
|
18
|
+
Parameters:
|
|
19
|
+
BINARY: Binary serialization format for compact representation.
|
|
20
|
+
TEXT: Text-based serialization format for human-readable output.
|
|
21
|
+
"""
|
|
22
|
+
|
|
14
23
|
BINARY = "binary"
|
|
15
24
|
TEXT = "text"
|
|
16
25
|
|
|
17
26
|
|
|
18
27
|
class FrameSerializer(ABC):
|
|
28
|
+
"""Abstract base class for frame serialization implementations.
|
|
29
|
+
|
|
30
|
+
Defines the interface for converting frames to/from serialized formats
|
|
31
|
+
for transmission or storage. Subclasses must implement serialization
|
|
32
|
+
type detection and the core serialize/deserialize methods.
|
|
33
|
+
"""
|
|
34
|
+
|
|
19
35
|
@property
|
|
20
36
|
@abstractmethod
|
|
21
37
|
def type(self) -> FrameSerializerType:
|
|
38
|
+
"""Get the serialization type supported by this serializer.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The FrameSerializerType indicating binary or text format.
|
|
42
|
+
"""
|
|
22
43
|
pass
|
|
23
44
|
|
|
24
45
|
async def setup(self, frame: StartFrame):
|
|
46
|
+
"""Initialize the serializer with startup configuration.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
frame: StartFrame containing initialization parameters.
|
|
50
|
+
"""
|
|
25
51
|
pass
|
|
26
52
|
|
|
27
53
|
@abstractmethod
|
|
28
54
|
async def serialize(self, frame: Frame) -> str | bytes | None:
|
|
55
|
+
"""Convert a frame to its serialized representation.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
frame: The frame to serialize.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Serialized frame data as string, bytes, or None if serialization fails.
|
|
62
|
+
"""
|
|
29
63
|
pass
|
|
30
64
|
|
|
31
65
|
@abstractmethod
|
|
32
66
|
async def deserialize(self, data: str | bytes) -> Frame | None:
|
|
67
|
+
"""Convert serialized data back to a frame object.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
data: Serialized frame data as string or bytes.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Reconstructed Frame object, or None if deserialization fails.
|
|
74
|
+
"""
|
|
33
75
|
pass
|
pipecat/serializers/exotel.py
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
5
|
#
|
|
6
6
|
|
|
7
|
+
"""Exotel Media Streams serializer for Pipecat."""
|
|
8
|
+
|
|
7
9
|
import base64
|
|
8
10
|
import json
|
|
9
11
|
from typing import Optional
|
|
@@ -11,7 +13,7 @@ from typing import Optional
|
|
|
11
13
|
from loguru import logger
|
|
12
14
|
from pydantic import BaseModel
|
|
13
15
|
|
|
14
|
-
from pipecat.audio.utils import
|
|
16
|
+
from pipecat.audio.utils import create_stream_resampler
|
|
15
17
|
from pipecat.frames.frames import (
|
|
16
18
|
AudioRawFrame,
|
|
17
19
|
Frame,
|
|
@@ -33,13 +35,14 @@ class ExotelFrameSerializer(FrameSerializer):
|
|
|
33
35
|
media streams protocol. It supports audio conversion, DTMF events, and automatic
|
|
34
36
|
call termination.
|
|
35
37
|
|
|
36
|
-
Ref
|
|
38
|
+
Note: Ref docs for events:
|
|
39
|
+
https://support.exotel.com/support/solutions/articles/3000108630-working-with-the-stream-and-voicebot-applet
|
|
37
40
|
"""
|
|
38
41
|
|
|
39
42
|
class InputParams(BaseModel):
|
|
40
43
|
"""Configuration parameters for ExotelFrameSerializer.
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
Parameters:
|
|
43
46
|
exotel_sample_rate: Sample rate used by Exotel, defaults to 8000 Hz.
|
|
44
47
|
sample_rate: Optional override for pipeline input sample rate.
|
|
45
48
|
"""
|
|
@@ -64,7 +67,8 @@ class ExotelFrameSerializer(FrameSerializer):
|
|
|
64
67
|
self._exotel_sample_rate = self._params.exotel_sample_rate
|
|
65
68
|
self._sample_rate = 0 # Pipeline input rate
|
|
66
69
|
|
|
67
|
-
self.
|
|
70
|
+
self._input_resampler = create_stream_resampler()
|
|
71
|
+
self._output_resampler = create_stream_resampler()
|
|
68
72
|
|
|
69
73
|
@property
|
|
70
74
|
def type(self) -> FrameSerializerType:
|
|
@@ -101,9 +105,13 @@ class ExotelFrameSerializer(FrameSerializer):
|
|
|
101
105
|
data = frame.audio
|
|
102
106
|
|
|
103
107
|
# Output: Exotel outputs PCM audio, but we need to resample to match requested sample_rate
|
|
104
|
-
serialized_data = await self.
|
|
108
|
+
serialized_data = await self._output_resampler.resample(
|
|
105
109
|
data, frame.sample_rate, self._exotel_sample_rate
|
|
106
110
|
)
|
|
111
|
+
if serialized_data is None or len(serialized_data) == 0:
|
|
112
|
+
# Ignoring in case we don't have audio
|
|
113
|
+
return None
|
|
114
|
+
|
|
107
115
|
payload = base64.b64encode(serialized_data).decode("ascii")
|
|
108
116
|
|
|
109
117
|
answer = {
|
|
@@ -135,11 +143,14 @@ class ExotelFrameSerializer(FrameSerializer):
|
|
|
135
143
|
payload_base64 = message["media"]["payload"]
|
|
136
144
|
payload = base64.b64decode(payload_base64)
|
|
137
145
|
|
|
138
|
-
deserialized_data = await self.
|
|
146
|
+
deserialized_data = await self._input_resampler.resample(
|
|
139
147
|
payload,
|
|
140
148
|
self._exotel_sample_rate,
|
|
141
149
|
self._sample_rate,
|
|
142
150
|
)
|
|
151
|
+
if deserialized_data is None or len(deserialized_data) == 0:
|
|
152
|
+
# Ignoring in case we don't have audio
|
|
153
|
+
return None
|
|
143
154
|
|
|
144
155
|
# Input: Exotel takes PCM data, so just resample to match sample_rate
|
|
145
156
|
audio_frame = InputAudioRawFrame(
|