dv-pipecat-ai 0.0.74.dev770__py3-none-any.whl → 0.0.82.dev776__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dv-pipecat-ai might be problematic. Click here for more details.

Files changed (244) hide show
  1. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/METADATA +137 -93
  2. dv_pipecat_ai-0.0.82.dev776.dist-info/RECORD +340 -0
  3. pipecat/__init__.py +17 -0
  4. pipecat/adapters/base_llm_adapter.py +36 -1
  5. pipecat/adapters/schemas/direct_function.py +296 -0
  6. pipecat/adapters/schemas/function_schema.py +15 -6
  7. pipecat/adapters/schemas/tools_schema.py +55 -7
  8. pipecat/adapters/services/anthropic_adapter.py +22 -3
  9. pipecat/adapters/services/aws_nova_sonic_adapter.py +23 -3
  10. pipecat/adapters/services/bedrock_adapter.py +22 -3
  11. pipecat/adapters/services/gemini_adapter.py +16 -3
  12. pipecat/adapters/services/open_ai_adapter.py +17 -2
  13. pipecat/adapters/services/open_ai_realtime_adapter.py +23 -3
  14. pipecat/audio/filters/base_audio_filter.py +30 -6
  15. pipecat/audio/filters/koala_filter.py +37 -2
  16. pipecat/audio/filters/krisp_filter.py +59 -6
  17. pipecat/audio/filters/noisereduce_filter.py +37 -0
  18. pipecat/audio/interruptions/base_interruption_strategy.py +25 -5
  19. pipecat/audio/interruptions/min_words_interruption_strategy.py +21 -4
  20. pipecat/audio/mixers/base_audio_mixer.py +30 -7
  21. pipecat/audio/mixers/soundfile_mixer.py +53 -6
  22. pipecat/audio/resamplers/base_audio_resampler.py +17 -9
  23. pipecat/audio/resamplers/resampy_resampler.py +26 -1
  24. pipecat/audio/resamplers/soxr_resampler.py +32 -1
  25. pipecat/audio/resamplers/soxr_stream_resampler.py +101 -0
  26. pipecat/audio/utils.py +194 -1
  27. pipecat/audio/vad/silero.py +60 -3
  28. pipecat/audio/vad/vad_analyzer.py +114 -30
  29. pipecat/clocks/base_clock.py +19 -0
  30. pipecat/clocks/system_clock.py +25 -0
  31. pipecat/extensions/voicemail/__init__.py +0 -0
  32. pipecat/extensions/voicemail/voicemail_detector.py +707 -0
  33. pipecat/frames/frames.py +590 -156
  34. pipecat/metrics/metrics.py +64 -1
  35. pipecat/observers/base_observer.py +58 -19
  36. pipecat/observers/loggers/debug_log_observer.py +56 -64
  37. pipecat/observers/loggers/llm_log_observer.py +8 -1
  38. pipecat/observers/loggers/transcription_log_observer.py +19 -7
  39. pipecat/observers/loggers/user_bot_latency_log_observer.py +32 -5
  40. pipecat/observers/turn_tracking_observer.py +26 -1
  41. pipecat/pipeline/base_pipeline.py +5 -7
  42. pipecat/pipeline/base_task.py +52 -9
  43. pipecat/pipeline/parallel_pipeline.py +121 -177
  44. pipecat/pipeline/pipeline.py +129 -20
  45. pipecat/pipeline/runner.py +50 -1
  46. pipecat/pipeline/sync_parallel_pipeline.py +132 -32
  47. pipecat/pipeline/task.py +263 -280
  48. pipecat/pipeline/task_observer.py +85 -34
  49. pipecat/pipeline/to_be_updated/merge_pipeline.py +32 -2
  50. pipecat/processors/aggregators/dtmf_aggregator.py +29 -22
  51. pipecat/processors/aggregators/gated.py +25 -24
  52. pipecat/processors/aggregators/gated_openai_llm_context.py +22 -2
  53. pipecat/processors/aggregators/llm_response.py +398 -89
  54. pipecat/processors/aggregators/openai_llm_context.py +161 -13
  55. pipecat/processors/aggregators/sentence.py +25 -14
  56. pipecat/processors/aggregators/user_response.py +28 -3
  57. pipecat/processors/aggregators/vision_image_frame.py +24 -14
  58. pipecat/processors/async_generator.py +28 -0
  59. pipecat/processors/audio/audio_buffer_processor.py +78 -37
  60. pipecat/processors/consumer_processor.py +25 -6
  61. pipecat/processors/filters/frame_filter.py +23 -0
  62. pipecat/processors/filters/function_filter.py +30 -0
  63. pipecat/processors/filters/identity_filter.py +17 -2
  64. pipecat/processors/filters/null_filter.py +24 -1
  65. pipecat/processors/filters/stt_mute_filter.py +56 -21
  66. pipecat/processors/filters/wake_check_filter.py +46 -3
  67. pipecat/processors/filters/wake_notifier_filter.py +21 -3
  68. pipecat/processors/frame_processor.py +488 -131
  69. pipecat/processors/frameworks/langchain.py +38 -3
  70. pipecat/processors/frameworks/rtvi.py +719 -34
  71. pipecat/processors/gstreamer/pipeline_source.py +41 -0
  72. pipecat/processors/idle_frame_processor.py +26 -3
  73. pipecat/processors/logger.py +23 -0
  74. pipecat/processors/metrics/frame_processor_metrics.py +77 -4
  75. pipecat/processors/metrics/sentry.py +42 -4
  76. pipecat/processors/producer_processor.py +34 -14
  77. pipecat/processors/text_transformer.py +22 -10
  78. pipecat/processors/transcript_processor.py +48 -29
  79. pipecat/processors/user_idle_processor.py +31 -21
  80. pipecat/runner/__init__.py +1 -0
  81. pipecat/runner/daily.py +132 -0
  82. pipecat/runner/livekit.py +148 -0
  83. pipecat/runner/run.py +543 -0
  84. pipecat/runner/types.py +67 -0
  85. pipecat/runner/utils.py +515 -0
  86. pipecat/serializers/base_serializer.py +42 -0
  87. pipecat/serializers/exotel.py +17 -6
  88. pipecat/serializers/genesys.py +95 -0
  89. pipecat/serializers/livekit.py +33 -0
  90. pipecat/serializers/plivo.py +16 -15
  91. pipecat/serializers/protobuf.py +37 -1
  92. pipecat/serializers/telnyx.py +18 -17
  93. pipecat/serializers/twilio.py +32 -16
  94. pipecat/services/ai_service.py +5 -3
  95. pipecat/services/anthropic/llm.py +113 -43
  96. pipecat/services/assemblyai/models.py +63 -5
  97. pipecat/services/assemblyai/stt.py +64 -11
  98. pipecat/services/asyncai/__init__.py +0 -0
  99. pipecat/services/asyncai/tts.py +501 -0
  100. pipecat/services/aws/llm.py +185 -111
  101. pipecat/services/aws/stt.py +217 -23
  102. pipecat/services/aws/tts.py +118 -52
  103. pipecat/services/aws/utils.py +101 -5
  104. pipecat/services/aws_nova_sonic/aws.py +82 -64
  105. pipecat/services/aws_nova_sonic/context.py +15 -6
  106. pipecat/services/azure/common.py +10 -2
  107. pipecat/services/azure/image.py +32 -0
  108. pipecat/services/azure/llm.py +9 -7
  109. pipecat/services/azure/stt.py +65 -2
  110. pipecat/services/azure/tts.py +154 -23
  111. pipecat/services/cartesia/stt.py +125 -8
  112. pipecat/services/cartesia/tts.py +102 -38
  113. pipecat/services/cerebras/llm.py +15 -23
  114. pipecat/services/deepgram/stt.py +19 -11
  115. pipecat/services/deepgram/tts.py +36 -0
  116. pipecat/services/deepseek/llm.py +14 -23
  117. pipecat/services/elevenlabs/tts.py +330 -64
  118. pipecat/services/fal/image.py +43 -0
  119. pipecat/services/fal/stt.py +48 -10
  120. pipecat/services/fireworks/llm.py +14 -21
  121. pipecat/services/fish/tts.py +109 -9
  122. pipecat/services/gemini_multimodal_live/__init__.py +1 -0
  123. pipecat/services/gemini_multimodal_live/events.py +83 -2
  124. pipecat/services/gemini_multimodal_live/file_api.py +189 -0
  125. pipecat/services/gemini_multimodal_live/gemini.py +218 -21
  126. pipecat/services/gladia/config.py +17 -10
  127. pipecat/services/gladia/stt.py +82 -36
  128. pipecat/services/google/frames.py +40 -0
  129. pipecat/services/google/google.py +2 -0
  130. pipecat/services/google/image.py +39 -2
  131. pipecat/services/google/llm.py +176 -58
  132. pipecat/services/google/llm_openai.py +26 -4
  133. pipecat/services/google/llm_vertex.py +37 -15
  134. pipecat/services/google/rtvi.py +41 -0
  135. pipecat/services/google/stt.py +65 -17
  136. pipecat/services/google/test-google-chirp.py +45 -0
  137. pipecat/services/google/tts.py +390 -19
  138. pipecat/services/grok/llm.py +8 -6
  139. pipecat/services/groq/llm.py +8 -6
  140. pipecat/services/groq/stt.py +13 -9
  141. pipecat/services/groq/tts.py +40 -0
  142. pipecat/services/hamsa/__init__.py +9 -0
  143. pipecat/services/hamsa/stt.py +241 -0
  144. pipecat/services/heygen/__init__.py +5 -0
  145. pipecat/services/heygen/api.py +281 -0
  146. pipecat/services/heygen/client.py +620 -0
  147. pipecat/services/heygen/video.py +338 -0
  148. pipecat/services/image_service.py +5 -3
  149. pipecat/services/inworld/__init__.py +1 -0
  150. pipecat/services/inworld/tts.py +592 -0
  151. pipecat/services/llm_service.py +127 -45
  152. pipecat/services/lmnt/tts.py +80 -7
  153. pipecat/services/mcp_service.py +85 -44
  154. pipecat/services/mem0/memory.py +42 -13
  155. pipecat/services/minimax/tts.py +74 -15
  156. pipecat/services/mistral/__init__.py +0 -0
  157. pipecat/services/mistral/llm.py +185 -0
  158. pipecat/services/moondream/vision.py +55 -10
  159. pipecat/services/neuphonic/tts.py +275 -48
  160. pipecat/services/nim/llm.py +8 -6
  161. pipecat/services/ollama/llm.py +27 -7
  162. pipecat/services/openai/base_llm.py +54 -16
  163. pipecat/services/openai/image.py +30 -0
  164. pipecat/services/openai/llm.py +7 -5
  165. pipecat/services/openai/stt.py +13 -9
  166. pipecat/services/openai/tts.py +42 -10
  167. pipecat/services/openai_realtime_beta/azure.py +11 -9
  168. pipecat/services/openai_realtime_beta/context.py +7 -5
  169. pipecat/services/openai_realtime_beta/events.py +10 -7
  170. pipecat/services/openai_realtime_beta/openai.py +37 -18
  171. pipecat/services/openpipe/llm.py +30 -24
  172. pipecat/services/openrouter/llm.py +9 -7
  173. pipecat/services/perplexity/llm.py +15 -19
  174. pipecat/services/piper/tts.py +26 -12
  175. pipecat/services/playht/tts.py +227 -65
  176. pipecat/services/qwen/llm.py +8 -6
  177. pipecat/services/rime/tts.py +128 -17
  178. pipecat/services/riva/stt.py +160 -22
  179. pipecat/services/riva/tts.py +67 -2
  180. pipecat/services/sambanova/llm.py +19 -17
  181. pipecat/services/sambanova/stt.py +14 -8
  182. pipecat/services/sarvam/tts.py +60 -13
  183. pipecat/services/simli/video.py +82 -21
  184. pipecat/services/soniox/__init__.py +0 -0
  185. pipecat/services/soniox/stt.py +398 -0
  186. pipecat/services/speechmatics/stt.py +29 -17
  187. pipecat/services/stt_service.py +47 -11
  188. pipecat/services/tavus/video.py +94 -25
  189. pipecat/services/together/llm.py +8 -6
  190. pipecat/services/tts_service.py +77 -53
  191. pipecat/services/ultravox/stt.py +46 -43
  192. pipecat/services/vision_service.py +5 -3
  193. pipecat/services/websocket_service.py +12 -11
  194. pipecat/services/whisper/base_stt.py +58 -12
  195. pipecat/services/whisper/stt.py +69 -58
  196. pipecat/services/xtts/tts.py +59 -2
  197. pipecat/sync/base_notifier.py +19 -0
  198. pipecat/sync/event_notifier.py +24 -0
  199. pipecat/tests/utils.py +73 -5
  200. pipecat/transcriptions/language.py +24 -0
  201. pipecat/transports/base_input.py +112 -8
  202. pipecat/transports/base_output.py +235 -13
  203. pipecat/transports/base_transport.py +119 -0
  204. pipecat/transports/local/audio.py +76 -0
  205. pipecat/transports/local/tk.py +84 -0
  206. pipecat/transports/network/fastapi_websocket.py +174 -15
  207. pipecat/transports/network/small_webrtc.py +383 -39
  208. pipecat/transports/network/webrtc_connection.py +214 -8
  209. pipecat/transports/network/websocket_client.py +171 -1
  210. pipecat/transports/network/websocket_server.py +147 -9
  211. pipecat/transports/services/daily.py +792 -70
  212. pipecat/transports/services/helpers/daily_rest.py +122 -129
  213. pipecat/transports/services/livekit.py +339 -4
  214. pipecat/transports/services/tavus.py +273 -38
  215. pipecat/utils/asyncio/task_manager.py +92 -186
  216. pipecat/utils/base_object.py +83 -1
  217. pipecat/utils/network.py +2 -0
  218. pipecat/utils/string.py +114 -58
  219. pipecat/utils/text/base_text_aggregator.py +44 -13
  220. pipecat/utils/text/base_text_filter.py +46 -0
  221. pipecat/utils/text/markdown_text_filter.py +70 -14
  222. pipecat/utils/text/pattern_pair_aggregator.py +18 -14
  223. pipecat/utils/text/simple_text_aggregator.py +43 -2
  224. pipecat/utils/text/skip_tags_aggregator.py +21 -13
  225. pipecat/utils/time.py +36 -0
  226. pipecat/utils/tracing/class_decorators.py +32 -7
  227. pipecat/utils/tracing/conversation_context_provider.py +12 -2
  228. pipecat/utils/tracing/service_attributes.py +80 -64
  229. pipecat/utils/tracing/service_decorators.py +48 -21
  230. pipecat/utils/tracing/setup.py +13 -7
  231. pipecat/utils/tracing/turn_context_provider.py +12 -2
  232. pipecat/utils/tracing/turn_trace_observer.py +27 -0
  233. pipecat/utils/utils.py +14 -14
  234. dv_pipecat_ai-0.0.74.dev770.dist-info/RECORD +0 -319
  235. pipecat/examples/daily_runner.py +0 -64
  236. pipecat/examples/run.py +0 -265
  237. pipecat/utils/asyncio/watchdog_async_iterator.py +0 -72
  238. pipecat/utils/asyncio/watchdog_event.py +0 -42
  239. pipecat/utils/asyncio/watchdog_priority_queue.py +0 -48
  240. pipecat/utils/asyncio/watchdog_queue.py +0 -48
  241. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/WHEEL +0 -0
  242. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/licenses/LICENSE +0 -0
  243. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/top_level.txt +0 -0
  244. /pipecat/{examples → extensions}/__init__.py +0 -0
@@ -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
@@ -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 create_default_resampler
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 Doc for events - https://support.exotel.com/support/solutions/articles/3000108630-working-with-the-stream-and-voicebot-applet
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
- Attributes:
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._resampler = create_default_resampler()
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._resampler.resample(
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._resampler.resample(
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(