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

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

Potentially problematic release.


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

Files changed (244) hide show
  1. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/METADATA +137 -93
  2. dv_pipecat_ai-0.0.82.dev776.dist-info/RECORD +340 -0
  3. pipecat/__init__.py +17 -0
  4. pipecat/adapters/base_llm_adapter.py +36 -1
  5. pipecat/adapters/schemas/direct_function.py +296 -0
  6. pipecat/adapters/schemas/function_schema.py +15 -6
  7. pipecat/adapters/schemas/tools_schema.py +55 -7
  8. pipecat/adapters/services/anthropic_adapter.py +22 -3
  9. pipecat/adapters/services/aws_nova_sonic_adapter.py +23 -3
  10. pipecat/adapters/services/bedrock_adapter.py +22 -3
  11. pipecat/adapters/services/gemini_adapter.py +16 -3
  12. pipecat/adapters/services/open_ai_adapter.py +17 -2
  13. pipecat/adapters/services/open_ai_realtime_adapter.py +23 -3
  14. pipecat/audio/filters/base_audio_filter.py +30 -6
  15. pipecat/audio/filters/koala_filter.py +37 -2
  16. pipecat/audio/filters/krisp_filter.py +59 -6
  17. pipecat/audio/filters/noisereduce_filter.py +37 -0
  18. pipecat/audio/interruptions/base_interruption_strategy.py +25 -5
  19. pipecat/audio/interruptions/min_words_interruption_strategy.py +21 -4
  20. pipecat/audio/mixers/base_audio_mixer.py +30 -7
  21. pipecat/audio/mixers/soundfile_mixer.py +53 -6
  22. pipecat/audio/resamplers/base_audio_resampler.py +17 -9
  23. pipecat/audio/resamplers/resampy_resampler.py +26 -1
  24. pipecat/audio/resamplers/soxr_resampler.py +32 -1
  25. pipecat/audio/resamplers/soxr_stream_resampler.py +101 -0
  26. pipecat/audio/utils.py +194 -1
  27. pipecat/audio/vad/silero.py +60 -3
  28. pipecat/audio/vad/vad_analyzer.py +114 -30
  29. pipecat/clocks/base_clock.py +19 -0
  30. pipecat/clocks/system_clock.py +25 -0
  31. pipecat/extensions/voicemail/__init__.py +0 -0
  32. pipecat/extensions/voicemail/voicemail_detector.py +707 -0
  33. pipecat/frames/frames.py +590 -156
  34. pipecat/metrics/metrics.py +64 -1
  35. pipecat/observers/base_observer.py +58 -19
  36. pipecat/observers/loggers/debug_log_observer.py +56 -64
  37. pipecat/observers/loggers/llm_log_observer.py +8 -1
  38. pipecat/observers/loggers/transcription_log_observer.py +19 -7
  39. pipecat/observers/loggers/user_bot_latency_log_observer.py +32 -5
  40. pipecat/observers/turn_tracking_observer.py +26 -1
  41. pipecat/pipeline/base_pipeline.py +5 -7
  42. pipecat/pipeline/base_task.py +52 -9
  43. pipecat/pipeline/parallel_pipeline.py +121 -177
  44. pipecat/pipeline/pipeline.py +129 -20
  45. pipecat/pipeline/runner.py +50 -1
  46. pipecat/pipeline/sync_parallel_pipeline.py +132 -32
  47. pipecat/pipeline/task.py +263 -280
  48. pipecat/pipeline/task_observer.py +85 -34
  49. pipecat/pipeline/to_be_updated/merge_pipeline.py +32 -2
  50. pipecat/processors/aggregators/dtmf_aggregator.py +29 -22
  51. pipecat/processors/aggregators/gated.py +25 -24
  52. pipecat/processors/aggregators/gated_openai_llm_context.py +22 -2
  53. pipecat/processors/aggregators/llm_response.py +398 -89
  54. pipecat/processors/aggregators/openai_llm_context.py +161 -13
  55. pipecat/processors/aggregators/sentence.py +25 -14
  56. pipecat/processors/aggregators/user_response.py +28 -3
  57. pipecat/processors/aggregators/vision_image_frame.py +24 -14
  58. pipecat/processors/async_generator.py +28 -0
  59. pipecat/processors/audio/audio_buffer_processor.py +78 -37
  60. pipecat/processors/consumer_processor.py +25 -6
  61. pipecat/processors/filters/frame_filter.py +23 -0
  62. pipecat/processors/filters/function_filter.py +30 -0
  63. pipecat/processors/filters/identity_filter.py +17 -2
  64. pipecat/processors/filters/null_filter.py +24 -1
  65. pipecat/processors/filters/stt_mute_filter.py +56 -21
  66. pipecat/processors/filters/wake_check_filter.py +46 -3
  67. pipecat/processors/filters/wake_notifier_filter.py +21 -3
  68. pipecat/processors/frame_processor.py +488 -131
  69. pipecat/processors/frameworks/langchain.py +38 -3
  70. pipecat/processors/frameworks/rtvi.py +719 -34
  71. pipecat/processors/gstreamer/pipeline_source.py +41 -0
  72. pipecat/processors/idle_frame_processor.py +26 -3
  73. pipecat/processors/logger.py +23 -0
  74. pipecat/processors/metrics/frame_processor_metrics.py +77 -4
  75. pipecat/processors/metrics/sentry.py +42 -4
  76. pipecat/processors/producer_processor.py +34 -14
  77. pipecat/processors/text_transformer.py +22 -10
  78. pipecat/processors/transcript_processor.py +48 -29
  79. pipecat/processors/user_idle_processor.py +31 -21
  80. pipecat/runner/__init__.py +1 -0
  81. pipecat/runner/daily.py +132 -0
  82. pipecat/runner/livekit.py +148 -0
  83. pipecat/runner/run.py +543 -0
  84. pipecat/runner/types.py +67 -0
  85. pipecat/runner/utils.py +515 -0
  86. pipecat/serializers/base_serializer.py +42 -0
  87. pipecat/serializers/exotel.py +17 -6
  88. pipecat/serializers/genesys.py +95 -0
  89. pipecat/serializers/livekit.py +33 -0
  90. pipecat/serializers/plivo.py +16 -15
  91. pipecat/serializers/protobuf.py +37 -1
  92. pipecat/serializers/telnyx.py +18 -17
  93. pipecat/serializers/twilio.py +32 -16
  94. pipecat/services/ai_service.py +5 -3
  95. pipecat/services/anthropic/llm.py +113 -43
  96. pipecat/services/assemblyai/models.py +63 -5
  97. pipecat/services/assemblyai/stt.py +64 -11
  98. pipecat/services/asyncai/__init__.py +0 -0
  99. pipecat/services/asyncai/tts.py +501 -0
  100. pipecat/services/aws/llm.py +185 -111
  101. pipecat/services/aws/stt.py +217 -23
  102. pipecat/services/aws/tts.py +118 -52
  103. pipecat/services/aws/utils.py +101 -5
  104. pipecat/services/aws_nova_sonic/aws.py +82 -64
  105. pipecat/services/aws_nova_sonic/context.py +15 -6
  106. pipecat/services/azure/common.py +10 -2
  107. pipecat/services/azure/image.py +32 -0
  108. pipecat/services/azure/llm.py +9 -7
  109. pipecat/services/azure/stt.py +65 -2
  110. pipecat/services/azure/tts.py +154 -23
  111. pipecat/services/cartesia/stt.py +125 -8
  112. pipecat/services/cartesia/tts.py +102 -38
  113. pipecat/services/cerebras/llm.py +15 -23
  114. pipecat/services/deepgram/stt.py +19 -11
  115. pipecat/services/deepgram/tts.py +36 -0
  116. pipecat/services/deepseek/llm.py +14 -23
  117. pipecat/services/elevenlabs/tts.py +330 -64
  118. pipecat/services/fal/image.py +43 -0
  119. pipecat/services/fal/stt.py +48 -10
  120. pipecat/services/fireworks/llm.py +14 -21
  121. pipecat/services/fish/tts.py +109 -9
  122. pipecat/services/gemini_multimodal_live/__init__.py +1 -0
  123. pipecat/services/gemini_multimodal_live/events.py +83 -2
  124. pipecat/services/gemini_multimodal_live/file_api.py +189 -0
  125. pipecat/services/gemini_multimodal_live/gemini.py +218 -21
  126. pipecat/services/gladia/config.py +17 -10
  127. pipecat/services/gladia/stt.py +82 -36
  128. pipecat/services/google/frames.py +40 -0
  129. pipecat/services/google/google.py +2 -0
  130. pipecat/services/google/image.py +39 -2
  131. pipecat/services/google/llm.py +176 -58
  132. pipecat/services/google/llm_openai.py +26 -4
  133. pipecat/services/google/llm_vertex.py +37 -15
  134. pipecat/services/google/rtvi.py +41 -0
  135. pipecat/services/google/stt.py +65 -17
  136. pipecat/services/google/test-google-chirp.py +45 -0
  137. pipecat/services/google/tts.py +390 -19
  138. pipecat/services/grok/llm.py +8 -6
  139. pipecat/services/groq/llm.py +8 -6
  140. pipecat/services/groq/stt.py +13 -9
  141. pipecat/services/groq/tts.py +40 -0
  142. pipecat/services/hamsa/__init__.py +9 -0
  143. pipecat/services/hamsa/stt.py +241 -0
  144. pipecat/services/heygen/__init__.py +5 -0
  145. pipecat/services/heygen/api.py +281 -0
  146. pipecat/services/heygen/client.py +620 -0
  147. pipecat/services/heygen/video.py +338 -0
  148. pipecat/services/image_service.py +5 -3
  149. pipecat/services/inworld/__init__.py +1 -0
  150. pipecat/services/inworld/tts.py +592 -0
  151. pipecat/services/llm_service.py +127 -45
  152. pipecat/services/lmnt/tts.py +80 -7
  153. pipecat/services/mcp_service.py +85 -44
  154. pipecat/services/mem0/memory.py +42 -13
  155. pipecat/services/minimax/tts.py +74 -15
  156. pipecat/services/mistral/__init__.py +0 -0
  157. pipecat/services/mistral/llm.py +185 -0
  158. pipecat/services/moondream/vision.py +55 -10
  159. pipecat/services/neuphonic/tts.py +275 -48
  160. pipecat/services/nim/llm.py +8 -6
  161. pipecat/services/ollama/llm.py +27 -7
  162. pipecat/services/openai/base_llm.py +54 -16
  163. pipecat/services/openai/image.py +30 -0
  164. pipecat/services/openai/llm.py +7 -5
  165. pipecat/services/openai/stt.py +13 -9
  166. pipecat/services/openai/tts.py +42 -10
  167. pipecat/services/openai_realtime_beta/azure.py +11 -9
  168. pipecat/services/openai_realtime_beta/context.py +7 -5
  169. pipecat/services/openai_realtime_beta/events.py +10 -7
  170. pipecat/services/openai_realtime_beta/openai.py +37 -18
  171. pipecat/services/openpipe/llm.py +30 -24
  172. pipecat/services/openrouter/llm.py +9 -7
  173. pipecat/services/perplexity/llm.py +15 -19
  174. pipecat/services/piper/tts.py +26 -12
  175. pipecat/services/playht/tts.py +227 -65
  176. pipecat/services/qwen/llm.py +8 -6
  177. pipecat/services/rime/tts.py +128 -17
  178. pipecat/services/riva/stt.py +160 -22
  179. pipecat/services/riva/tts.py +67 -2
  180. pipecat/services/sambanova/llm.py +19 -17
  181. pipecat/services/sambanova/stt.py +14 -8
  182. pipecat/services/sarvam/tts.py +60 -13
  183. pipecat/services/simli/video.py +82 -21
  184. pipecat/services/soniox/__init__.py +0 -0
  185. pipecat/services/soniox/stt.py +398 -0
  186. pipecat/services/speechmatics/stt.py +29 -17
  187. pipecat/services/stt_service.py +47 -11
  188. pipecat/services/tavus/video.py +94 -25
  189. pipecat/services/together/llm.py +8 -6
  190. pipecat/services/tts_service.py +77 -53
  191. pipecat/services/ultravox/stt.py +46 -43
  192. pipecat/services/vision_service.py +5 -3
  193. pipecat/services/websocket_service.py +12 -11
  194. pipecat/services/whisper/base_stt.py +58 -12
  195. pipecat/services/whisper/stt.py +69 -58
  196. pipecat/services/xtts/tts.py +59 -2
  197. pipecat/sync/base_notifier.py +19 -0
  198. pipecat/sync/event_notifier.py +24 -0
  199. pipecat/tests/utils.py +73 -5
  200. pipecat/transcriptions/language.py +24 -0
  201. pipecat/transports/base_input.py +112 -8
  202. pipecat/transports/base_output.py +235 -13
  203. pipecat/transports/base_transport.py +119 -0
  204. pipecat/transports/local/audio.py +76 -0
  205. pipecat/transports/local/tk.py +84 -0
  206. pipecat/transports/network/fastapi_websocket.py +174 -15
  207. pipecat/transports/network/small_webrtc.py +383 -39
  208. pipecat/transports/network/webrtc_connection.py +214 -8
  209. pipecat/transports/network/websocket_client.py +171 -1
  210. pipecat/transports/network/websocket_server.py +147 -9
  211. pipecat/transports/services/daily.py +792 -70
  212. pipecat/transports/services/helpers/daily_rest.py +122 -129
  213. pipecat/transports/services/livekit.py +339 -4
  214. pipecat/transports/services/tavus.py +273 -38
  215. pipecat/utils/asyncio/task_manager.py +92 -186
  216. pipecat/utils/base_object.py +83 -1
  217. pipecat/utils/network.py +2 -0
  218. pipecat/utils/string.py +114 -58
  219. pipecat/utils/text/base_text_aggregator.py +44 -13
  220. pipecat/utils/text/base_text_filter.py +46 -0
  221. pipecat/utils/text/markdown_text_filter.py +70 -14
  222. pipecat/utils/text/pattern_pair_aggregator.py +18 -14
  223. pipecat/utils/text/simple_text_aggregator.py +43 -2
  224. pipecat/utils/text/skip_tags_aggregator.py +21 -13
  225. pipecat/utils/time.py +36 -0
  226. pipecat/utils/tracing/class_decorators.py +32 -7
  227. pipecat/utils/tracing/conversation_context_provider.py +12 -2
  228. pipecat/utils/tracing/service_attributes.py +80 -64
  229. pipecat/utils/tracing/service_decorators.py +48 -21
  230. pipecat/utils/tracing/setup.py +13 -7
  231. pipecat/utils/tracing/turn_context_provider.py +12 -2
  232. pipecat/utils/tracing/turn_trace_observer.py +27 -0
  233. pipecat/utils/utils.py +14 -14
  234. dv_pipecat_ai-0.0.74.dev770.dist-info/RECORD +0 -319
  235. pipecat/examples/daily_runner.py +0 -64
  236. pipecat/examples/run.py +0 -265
  237. pipecat/utils/asyncio/watchdog_async_iterator.py +0 -72
  238. pipecat/utils/asyncio/watchdog_event.py +0 -42
  239. pipecat/utils/asyncio/watchdog_priority_queue.py +0 -48
  240. pipecat/utils/asyncio/watchdog_queue.py +0 -48
  241. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/WHEEL +0 -0
  242. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/licenses/LICENSE +0 -0
  243. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/top_level.txt +0 -0
  244. /pipecat/{examples → extensions}/__init__.py +0 -0
@@ -4,8 +4,16 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """Daily transport implementation for Pipecat.
8
+
9
+ This module provides comprehensive Daily video conferencing integration including
10
+ audio/video streaming, transcription, recording, dial-in/out functionality, and
11
+ real-time communication features.
12
+ """
13
+
7
14
  import asyncio
8
15
  import time
16
+ from concurrent.futures import CancelledError as FuturesCancelledError
9
17
  from concurrent.futures import ThreadPoolExecutor
10
18
  from dataclasses import dataclass
11
19
  from typing import Any, Awaitable, Callable, Dict, Mapping, Optional
@@ -20,6 +28,7 @@ from pipecat.frames.frames import (
20
28
  EndFrame,
21
29
  ErrorFrame,
22
30
  Frame,
31
+ InputAudioRawFrame,
23
32
  InterimTranscriptionFrame,
24
33
  OutputAudioRawFrame,
25
34
  OutputDTMFFrame,
@@ -40,7 +49,6 @@ from pipecat.transports.base_input import BaseInputTransport
40
49
  from pipecat.transports.base_output import BaseOutputTransport
41
50
  from pipecat.transports.base_transport import BaseTransport, TransportParams
42
51
  from pipecat.utils.asyncio.task_manager import BaseTaskManager
43
- from pipecat.utils.asyncio.watchdog_queue import WatchdogQueue
44
52
 
45
53
  try:
46
54
  from daily import (
@@ -52,6 +60,10 @@ try:
52
60
  EventHandler,
53
61
  VideoFrame,
54
62
  VirtualCameraDevice,
63
+ VirtualSpeakerDevice,
64
+ )
65
+ from daily import (
66
+ LogLevel as DailyLogLevel,
55
67
  )
56
68
  except ModuleNotFoundError as e:
57
69
  logger.error(f"Exception: {e}")
@@ -67,7 +79,7 @@ VAD_RESET_PERIOD_MS = 2000
67
79
  class DailyTransportMessageFrame(TransportMessageFrame):
68
80
  """Frame for transport messages in Daily calls.
69
81
 
70
- Attributes:
82
+ Parameters:
71
83
  participant_id: Optional ID of the participant this message is for/from.
72
84
  """
73
85
 
@@ -78,7 +90,7 @@ class DailyTransportMessageFrame(TransportMessageFrame):
78
90
  class DailyTransportMessageUrgentFrame(TransportMessageUrgentFrame):
79
91
  """Frame for urgent transport messages in Daily calls.
80
92
 
81
- Attributes:
93
+ Parameters:
82
94
  participant_id: Optional ID of the participant this message is for/from.
83
95
  """
84
96
 
@@ -89,13 +101,15 @@ class WebRTCVADAnalyzer(VADAnalyzer):
89
101
  """Voice Activity Detection analyzer using WebRTC.
90
102
 
91
103
  Implements voice activity detection using Daily's native WebRTC VAD.
92
-
93
- Args:
94
- sample_rate: Audio sample rate in Hz.
95
- params: VAD configuration parameters (VADParams).
96
104
  """
97
105
 
98
106
  def __init__(self, *, sample_rate: Optional[int] = None, params: Optional[VADParams] = None):
107
+ """Initialize the WebRTC VAD analyzer.
108
+
109
+ Args:
110
+ sample_rate: Audio sample rate in Hz.
111
+ params: VAD configuration parameters.
112
+ """
99
113
  super().__init__(sample_rate=sample_rate, params=params)
100
114
 
101
115
  self._webrtc_vad = Daily.create_native_vad(
@@ -104,9 +118,22 @@ class WebRTCVADAnalyzer(VADAnalyzer):
104
118
  logger.debug("Loaded native WebRTC VAD")
105
119
 
106
120
  def num_frames_required(self) -> int:
121
+ """Get the number of audio frames required for VAD analysis.
122
+
123
+ Returns:
124
+ The number of frames needed (equivalent to 10ms of audio).
125
+ """
107
126
  return int(self.sample_rate / 100.0)
108
127
 
109
128
  def voice_confidence(self, buffer) -> float:
129
+ """Analyze audio buffer and return voice confidence score.
130
+
131
+ Args:
132
+ buffer: Audio buffer to analyze.
133
+
134
+ Returns:
135
+ Voice confidence score between 0.0 and 1.0.
136
+ """
110
137
  confidence = 0
111
138
  if len(buffer) > 0:
112
139
  confidence = self._webrtc_vad.analyze_frames(buffer)
@@ -116,7 +143,7 @@ class WebRTCVADAnalyzer(VADAnalyzer):
116
143
  class DailyDialinSettings(BaseModel):
117
144
  """Settings for Daily's dial-in functionality.
118
145
 
119
- Attributes:
146
+ Parameters:
120
147
  call_id: CallId is represented by UUID and represents the sessionId in the SIP Network.
121
148
  call_domain: Call Domain is represented by UUID and represents your Daily Domain on the SIP Network.
122
149
  """
@@ -128,7 +155,7 @@ class DailyDialinSettings(BaseModel):
128
155
  class DailyTranscriptionSettings(BaseModel):
129
156
  """Configuration settings for Daily's transcription service.
130
157
 
131
- Attributes:
158
+ Parameters:
132
159
  language: ISO language code for transcription (e.g. "en").
133
160
  model: Transcription model to use (e.g. "nova-2-general").
134
161
  profanity_filter: Whether to filter profanity from transcripts.
@@ -152,18 +179,20 @@ class DailyTranscriptionSettings(BaseModel):
152
179
  class DailyParams(TransportParams):
153
180
  """Configuration parameters for Daily transport.
154
181
 
155
- Args:
156
- api_url: Daily API base URL
157
- api_key: Daily API authentication key
158
- dialin_settings: Optional settings for dial-in functionality
159
- camera_out_enabled: Whether to enable the main camera output track. If enabled, it still needs `video_out_enabled=True`
160
- microphone_out_enabled: Whether to enable the main microphone track. If enabled, it still needs `audio_out_enabled=True`
161
- transcription_enabled: Whether to enable speech transcription
162
- transcription_settings: Configuration for transcription service
182
+ Parameters:
183
+ api_url: Daily API base URL.
184
+ api_key: Daily API authentication key.
185
+ audio_in_user_tracks: Receive users' audio in separate tracks
186
+ dialin_settings: Optional settings for dial-in functionality.
187
+ camera_out_enabled: Whether to enable the main camera output track.
188
+ microphone_out_enabled: Whether to enable the main microphone track.
189
+ transcription_enabled: Whether to enable speech transcription.
190
+ transcription_settings: Configuration for transcription service.
163
191
  """
164
192
 
165
193
  api_url: str = "https://api.daily.co/v1"
166
194
  api_key: str = ""
195
+ audio_in_user_tracks: bool = True
167
196
  dialin_settings: Optional[DailyDialinSettings] = None
168
197
  camera_out_enabled: bool = True
169
198
  microphone_out_enabled: bool = True
@@ -174,7 +203,7 @@ class DailyParams(TransportParams):
174
203
  class DailyCallbacks(BaseModel):
175
204
  """Callback handlers for Daily events.
176
205
 
177
- Attributes:
206
+ Parameters:
178
207
  on_active_speaker_changed: Called when the active speaker of the call has changed.
179
208
  on_joined: Called when bot successfully joined a room.
180
209
  on_left: Called when bot left a room.
@@ -197,6 +226,8 @@ class DailyCallbacks(BaseModel):
197
226
  on_participant_left: Called when a participant leaves.
198
227
  on_participant_updated: Called when participant info is updated.
199
228
  on_transcription_message: Called when receiving transcription.
229
+ on_transcription_stopped: Called when transcription is stopped.
230
+ on_transcription_error: Called when transcription encounters an error.
200
231
  on_recording_started: Called when recording starts.
201
232
  on_recording_stopped: Called when recording stops.
202
233
  on_recording_error: Called when recording encounters an error.
@@ -224,12 +255,23 @@ class DailyCallbacks(BaseModel):
224
255
  on_participant_left: Callable[[Mapping[str, Any], str], Awaitable[None]]
225
256
  on_participant_updated: Callable[[Mapping[str, Any]], Awaitable[None]]
226
257
  on_transcription_message: Callable[[Mapping[str, Any]], Awaitable[None]]
258
+ on_transcription_stopped: Callable[[str, bool], Awaitable[None]]
259
+ on_transcription_error: Callable[[str], Awaitable[None]]
227
260
  on_recording_started: Callable[[Mapping[str, Any]], Awaitable[None]]
228
261
  on_recording_stopped: Callable[[str], Awaitable[None]]
229
262
  on_recording_error: Callable[[str, str], Awaitable[None]]
230
263
 
231
264
 
232
265
  def completion_callback(future):
266
+ """Create a completion callback for Daily API calls.
267
+
268
+ Args:
269
+ future: The asyncio Future to set the result on.
270
+
271
+ Returns:
272
+ A callback function that sets the future result.
273
+ """
274
+
233
275
  def _callback(*args):
234
276
  def set_result(future, *args):
235
277
  try:
@@ -247,6 +289,13 @@ def completion_callback(future):
247
289
 
248
290
  @dataclass
249
291
  class DailyAudioTrack:
292
+ """Container for Daily audio track components.
293
+
294
+ Parameters:
295
+ source: The custom audio source for the track.
296
+ track: The custom audio track instance.
297
+ """
298
+
250
299
  source: CustomAudioSource
251
300
  track: CustomAudioTrack
252
301
 
@@ -254,21 +303,14 @@ class DailyAudioTrack:
254
303
  class DailyTransportClient(EventHandler):
255
304
  """Core client for interacting with Daily's API.
256
305
 
257
- Manages the connection to Daily rooms and handles all low-level API interactions.
258
-
259
- Args:
260
- room_url: URL of the Daily room to connect to.
261
- token: Optional authentication token for the room.
262
- bot_name: Display name for the bot in the call.
263
- params: Configuration parameters (DailyParams).
264
- callbacks: Event callback handlers (DailyCallbacks).
265
- transport_name: Name identifier for the transport.
306
+ Manages the connection to Daily rooms and handles all low-level API interactions
307
+ including room management, media streaming, transcription, and event handling.
266
308
  """
267
309
 
268
310
  _daily_initialized: bool = False
269
311
 
270
- # This is necessary to override EventHandler's __new__ method.
271
312
  def __new__(cls, *args, **kwargs):
313
+ """Override EventHandler's __new__ method to ensure Daily is initialized only once."""
272
314
  return super().__new__(cls)
273
315
 
274
316
  def __init__(
@@ -280,6 +322,16 @@ class DailyTransportClient(EventHandler):
280
322
  callbacks: DailyCallbacks,
281
323
  transport_name: str,
282
324
  ):
325
+ """Initialize the Daily transport client.
326
+
327
+ Args:
328
+ room_url: URL of the Daily room to connect to.
329
+ token: Optional authentication token for the room.
330
+ bot_name: Display name for the bot in the call.
331
+ params: Configuration parameters for the transport.
332
+ callbacks: Event callback handlers.
333
+ transport_name: Name identifier for the transport.
334
+ """
283
335
  super().__init__()
284
336
 
285
337
  if not DailyTransportClient._daily_initialized:
@@ -305,7 +357,6 @@ class DailyTransportClient(EventHandler):
305
357
  self._leave_counter = 0
306
358
 
307
359
  self._task_manager: Optional[BaseTaskManager] = None
308
- self._watchdog_timers_enabled = False
309
360
 
310
361
  # We use the executor to cleanup the client. We just do it from one
311
362
  # place, so only one thread is really needed.
@@ -331,29 +382,60 @@ class DailyTransportClient(EventHandler):
331
382
  self._out_sample_rate = 0
332
383
 
333
384
  self._camera: Optional[VirtualCameraDevice] = None
385
+ self._speaker: Optional[VirtualSpeakerDevice] = None
334
386
  self._microphone_track: Optional[DailyAudioTrack] = None
335
387
  self._custom_audio_tracks: Dict[str, DailyAudioTrack] = {}
336
388
 
337
389
  def _camera_name(self):
390
+ """Generate a unique virtual camera name for this client instance."""
338
391
  return f"camera-{self}"
339
392
 
393
+ def _speaker_name(self):
394
+ """Generate a unique virtual speaker name for this client instance."""
395
+ return f"speaker-{self}"
396
+
340
397
  @property
341
398
  def room_url(self) -> str:
399
+ """Get the Daily room URL.
400
+
401
+ Returns:
402
+ The room URL this client is connected to.
403
+ """
342
404
  return self._room_url
343
405
 
344
406
  @property
345
407
  def participant_id(self) -> str:
408
+ """Get the participant ID for this client.
409
+
410
+ Returns:
411
+ The participant ID assigned by Daily.
412
+ """
346
413
  return self._participant_id
347
414
 
348
415
  @property
349
416
  def in_sample_rate(self) -> int:
417
+ """Get the input audio sample rate.
418
+
419
+ Returns:
420
+ The input sample rate in Hz.
421
+ """
350
422
  return self._in_sample_rate
351
423
 
352
424
  @property
353
425
  def out_sample_rate(self) -> int:
426
+ """Get the output audio sample rate.
427
+
428
+ Returns:
429
+ The output sample rate in Hz.
430
+ """
354
431
  return self._out_sample_rate
355
432
 
356
433
  async def send_message(self, frame: TransportMessageFrame | TransportMessageUrgentFrame):
434
+ """Send an application message to participants.
435
+
436
+ Args:
437
+ frame: The message frame to send.
438
+ """
357
439
  if not self._joined:
358
440
  return
359
441
 
@@ -367,11 +449,45 @@ class DailyTransportClient(EventHandler):
367
449
  )
368
450
  await future
369
451
 
452
+ async def read_next_audio_frame(self) -> Optional[InputAudioRawFrame]:
453
+ """Reads the next 20ms audio frame from the virtual speaker."""
454
+ if not self._speaker:
455
+ return None
456
+
457
+ sample_rate = self._in_sample_rate
458
+ num_channels = self._params.audio_in_channels
459
+ num_frames = int(sample_rate / 100) * 2 # 20ms of audio
460
+
461
+ future = self._get_event_loop().create_future()
462
+ self._speaker.read_frames(num_frames, completion=completion_callback(future))
463
+ audio = await future
464
+
465
+ if len(audio) > 0:
466
+ return InputAudioRawFrame(
467
+ audio=audio, sample_rate=sample_rate, num_channels=num_channels
468
+ )
469
+ else:
470
+ # If we don't read any audio it could be there's no participant
471
+ # connected. daily-python will return immediately if that's the
472
+ # case, so let's sleep for a little bit (i.e. busy wait).
473
+ await asyncio.sleep(0.01)
474
+ return None
475
+
370
476
  async def register_audio_destination(self, destination: str):
477
+ """Register a custom audio destination for multi-track output.
478
+
479
+ Args:
480
+ destination: The destination identifier to register.
481
+ """
371
482
  self._custom_audio_tracks[destination] = await self.add_custom_audio_track(destination)
372
483
  self._client.update_publishing({"customAudio": {destination: True}})
373
484
 
374
485
  async def write_audio_frame(self, frame: OutputAudioRawFrame):
486
+ """Write an audio frame to the appropriate audio track.
487
+
488
+ Args:
489
+ frame: The audio frame to write.
490
+ """
375
491
  future = self._get_event_loop().create_future()
376
492
 
377
493
  destination = frame.transport_destination
@@ -391,23 +507,33 @@ class DailyTransportClient(EventHandler):
391
507
  await future
392
508
 
393
509
  async def write_video_frame(self, frame: OutputImageRawFrame):
510
+ """Write a video frame to the camera device.
511
+
512
+ Args:
513
+ frame: The image frame to write.
514
+ """
394
515
  if not frame.transport_destination and self._camera:
395
516
  self._camera.write_frame(frame.image)
396
517
 
397
518
  async def setup(self, setup: FrameProcessorSetup):
519
+ """Setup the client with task manager and event queues.
520
+
521
+ Args:
522
+ setup: The frame processor setup configuration.
523
+ """
398
524
  if self._task_manager:
399
525
  return
400
526
 
401
527
  self._task_manager = setup.task_manager
402
- self._watchdog_timers_enabled = setup.watchdog_timers_enabled
403
528
 
404
- self._event_queue = WatchdogQueue(self._task_manager)
529
+ self._event_queue = asyncio.Queue()
405
530
  self._event_task = self._task_manager.create_task(
406
531
  self._callback_task_handler(self._event_queue),
407
532
  f"{self}::event_callback_task",
408
533
  )
409
534
 
410
535
  async def cleanup(self):
536
+ """Cleanup client resources and cancel tasks."""
411
537
  if self._event_task and self._task_manager:
412
538
  await self._task_manager.cancel_task(self._event_task)
413
539
  self._event_task = None
@@ -422,18 +548,32 @@ class DailyTransportClient(EventHandler):
422
548
  await self._get_event_loop().run_in_executor(self._executor, self._cleanup)
423
549
 
424
550
  async def start(self, frame: StartFrame):
551
+ """Start the client and initialize audio/video components.
552
+
553
+ Args:
554
+ frame: The start frame containing initialization parameters.
555
+ """
425
556
  self._in_sample_rate = self._params.audio_in_sample_rate or frame.audio_in_sample_rate
426
557
  self._out_sample_rate = self._params.audio_out_sample_rate or frame.audio_out_sample_rate
427
558
 
428
- if self._params.audio_in_enabled and not self._audio_task and self._task_manager:
429
- self._audio_queue = WatchdogQueue(self._task_manager)
430
- self._audio_task = self._task_manager.create_task(
431
- self._callback_task_handler(self._audio_queue),
432
- f"{self}::audio_callback_task",
433
- )
559
+ if self._params.audio_in_enabled:
560
+ if self._params.audio_in_user_tracks and not self._audio_task and self._task_manager:
561
+ self._audio_queue = asyncio.Queue()
562
+ self._audio_task = self._task_manager.create_task(
563
+ self._callback_task_handler(self._audio_queue),
564
+ f"{self}::audio_callback_task",
565
+ )
566
+ elif not self._speaker:
567
+ self._speaker = Daily.create_speaker_device(
568
+ self._speaker_name(),
569
+ sample_rate=self._in_sample_rate,
570
+ channels=self._params.audio_in_channels,
571
+ non_blocking=True,
572
+ )
573
+ Daily.select_speaker_device(self._speaker_name())
434
574
 
435
575
  if self._params.video_in_enabled and not self._video_task and self._task_manager:
436
- self._video_queue = WatchdogQueue(self._task_manager)
576
+ self._video_queue = asyncio.Queue()
437
577
  self._video_task = self._task_manager.create_task(
438
578
  self._callback_task_handler(self._video_queue),
439
579
  f"{self}::video_callback_task",
@@ -452,6 +592,7 @@ class DailyTransportClient(EventHandler):
452
592
  self._microphone_track = DailyAudioTrack(source=audio_source, track=audio_track)
453
593
 
454
594
  async def join(self):
595
+ """Join the Daily room with configured settings."""
455
596
  # Transport already joined or joining, ignore.
456
597
  if self._joined or self._joining:
457
598
  # Increment leave counter if we already joined.
@@ -497,6 +638,7 @@ class DailyTransportClient(EventHandler):
497
638
  await self._callbacks.on_error(error_msg)
498
639
 
499
640
  async def _join(self):
641
+ """Execute the actual room join operation."""
500
642
  future = self._get_event_loop().create_future()
501
643
 
502
644
  camera_enabled = self._params.video_out_enabled and self._params.camera_out_enabled
@@ -552,6 +694,7 @@ class DailyTransportClient(EventHandler):
552
694
  return await asyncio.wait_for(future, timeout=10)
553
695
 
554
696
  async def leave(self):
697
+ """Leave the Daily room and cleanup resources."""
555
698
  # Decrement leave counter when leaving.
556
699
  self._leave_counter -= 1
557
700
 
@@ -586,22 +729,39 @@ class DailyTransportClient(EventHandler):
586
729
  await self._callbacks.on_error(error_msg)
587
730
 
588
731
  async def _leave(self):
732
+ """Execute the actual room leave operation."""
589
733
  future = self._get_event_loop().create_future()
590
734
  self._client.leave(completion=completion_callback(future))
591
735
  return await asyncio.wait_for(future, timeout=10)
592
736
 
593
737
  def _cleanup(self):
738
+ """Cleanup the Daily client instance."""
594
739
  if self._client:
595
740
  self._client.release()
596
741
  self._client = None
597
742
 
598
743
  def participants(self):
744
+ """Get current participants in the room.
745
+
746
+ Returns:
747
+ Dictionary of participants keyed by participant ID.
748
+ """
599
749
  return self._client.participants()
600
750
 
601
751
  def participant_counts(self):
752
+ """Get participant count information.
753
+
754
+ Returns:
755
+ Dictionary with participant count details.
756
+ """
602
757
  return self._client.participant_counts()
603
758
 
604
759
  async def start_dialout(self, settings):
760
+ """Start a dial-out call to a phone number.
761
+
762
+ Args:
763
+ settings: Dial-out configuration settings.
764
+ """
605
765
  logger.debug(f"Starting dialout: settings={settings}")
606
766
 
607
767
  future = self._get_event_loop().create_future()
@@ -611,6 +771,11 @@ class DailyTransportClient(EventHandler):
611
771
  logger.error(f"Unable to start dialout: {error}")
612
772
 
613
773
  async def stop_dialout(self, participant_id):
774
+ """Stop a dial-out call for a specific participant.
775
+
776
+ Args:
777
+ participant_id: ID of the participant to stop dial-out for.
778
+ """
614
779
  logger.debug(f"Stopping dialout: participant_id={participant_id}")
615
780
 
616
781
  future = self._get_event_loop().create_future()
@@ -620,21 +785,43 @@ class DailyTransportClient(EventHandler):
620
785
  logger.error(f"Unable to stop dialout: {error}")
621
786
 
622
787
  async def send_dtmf(self, settings):
788
+ """Send DTMF tones during a call.
789
+
790
+ Args:
791
+ settings: DTMF settings including tones and target session.
792
+ """
623
793
  future = self._get_event_loop().create_future()
624
794
  self._client.send_dtmf(settings, completion=completion_callback(future))
625
795
  await future
626
796
 
627
797
  async def sip_call_transfer(self, settings):
798
+ """Transfer a SIP call to another destination.
799
+
800
+ Args:
801
+ settings: SIP call transfer settings.
802
+ """
628
803
  future = self._get_event_loop().create_future()
629
804
  self._client.sip_call_transfer(settings, completion=completion_callback(future))
630
805
  await future
631
806
 
632
807
  async def sip_refer(self, settings):
808
+ """Send a SIP REFER request.
809
+
810
+ Args:
811
+ settings: SIP REFER settings.
812
+ """
633
813
  future = self._get_event_loop().create_future()
634
814
  self._client.sip_refer(settings, completion=completion_callback(future))
635
815
  await future
636
816
 
637
817
  async def start_recording(self, streaming_settings, stream_id, force_new):
818
+ """Start recording the call.
819
+
820
+ Args:
821
+ streaming_settings: Recording configuration settings.
822
+ stream_id: Unique identifier for the recording stream.
823
+ force_new: Whether to force a new recording session.
824
+ """
638
825
  logger.debug(
639
826
  f"Starting recording: stream_id={stream_id} force_new={force_new} settings={streaming_settings}"
640
827
  )
@@ -648,6 +835,11 @@ class DailyTransportClient(EventHandler):
648
835
  logger.error(f"Unable to start recording: {error}")
649
836
 
650
837
  async def stop_recording(self, stream_id):
838
+ """Stop recording the call.
839
+
840
+ Args:
841
+ stream_id: Unique identifier for the recording stream to stop.
842
+ """
651
843
  logger.debug(f"Stopping recording: stream_id={stream_id}")
652
844
 
653
845
  future = self._get_event_loop().create_future()
@@ -657,6 +849,11 @@ class DailyTransportClient(EventHandler):
657
849
  logger.error(f"Unable to stop recording: {error}")
658
850
 
659
851
  async def start_transcription(self, settings):
852
+ """Start transcription for the call.
853
+
854
+ Args:
855
+ settings: Transcription configuration settings.
856
+ """
660
857
  if not self._token:
661
858
  logger.warning("Transcription can't be started without a room token")
662
859
  return
@@ -673,6 +870,7 @@ class DailyTransportClient(EventHandler):
673
870
  logger.error(f"Unable to start transcription: {error}")
674
871
 
675
872
  async def stop_transcription(self):
873
+ """Stop transcription for the call."""
676
874
  if not self._token:
677
875
  return
678
876
 
@@ -685,6 +883,12 @@ class DailyTransportClient(EventHandler):
685
883
  logger.error(f"Unable to stop transcription: {error}")
686
884
 
687
885
  async def send_prebuilt_chat_message(self, message: str, user_name: Optional[str] = None):
886
+ """Send a chat message to Daily's Prebuilt main room.
887
+
888
+ Args:
889
+ message: The chat message to send.
890
+ user_name: Optional user name that will appear as sender of the message.
891
+ """
688
892
  if not self._joined:
689
893
  return
690
894
 
@@ -695,6 +899,11 @@ class DailyTransportClient(EventHandler):
695
899
  await future
696
900
 
697
901
  async def capture_participant_transcription(self, participant_id: str):
902
+ """Enable transcription capture for a specific participant.
903
+
904
+ Args:
905
+ participant_id: ID of the participant to capture transcription for.
906
+ """
698
907
  if not self._params.transcription_enabled:
699
908
  return
700
909
 
@@ -710,6 +919,15 @@ class DailyTransportClient(EventHandler):
710
919
  sample_rate: int = 16000,
711
920
  callback_interval_ms: int = 20,
712
921
  ):
922
+ """Capture audio from a specific participant.
923
+
924
+ Args:
925
+ participant_id: ID of the participant to capture audio from.
926
+ callback: Callback function to handle audio data.
927
+ audio_source: Audio source to capture (microphone, screenAudio, or custom).
928
+ sample_rate: Desired sample rate for audio capture.
929
+ callback_interval_ms: Interval between audio callbacks in milliseconds.
930
+ """
713
931
  # Only enable the desired audio source subscription on this participant.
714
932
  if audio_source in ("microphone", "screenAudio"):
715
933
  media = {"media": {audio_source: "subscribed"}}
@@ -740,6 +958,15 @@ class DailyTransportClient(EventHandler):
740
958
  video_source: str = "camera",
741
959
  color_format: str = "RGB",
742
960
  ):
961
+ """Capture video from a specific participant.
962
+
963
+ Args:
964
+ participant_id: ID of the participant to capture video from.
965
+ callback: Callback function to handle video frames.
966
+ framerate: Desired framerate for video capture.
967
+ video_source: Video source to capture (camera, screenVideo, or custom).
968
+ color_format: Color format for video frames.
969
+ """
743
970
  # Only enable the desired audio source subscription on this participant.
744
971
  if video_source in ("camera", "screenVideo"):
745
972
  media = {"media": {video_source: "subscribed"}}
@@ -762,6 +989,14 @@ class DailyTransportClient(EventHandler):
762
989
  )
763
990
 
764
991
  async def add_custom_audio_track(self, track_name: str) -> DailyAudioTrack:
992
+ """Add a custom audio track for multi-stream output.
993
+
994
+ Args:
995
+ track_name: Name for the custom audio track.
996
+
997
+ Returns:
998
+ The created DailyAudioTrack instance.
999
+ """
765
1000
  future = self._get_event_loop().create_future()
766
1001
 
767
1002
  audio_source = CustomAudioSource(self._out_sample_rate, 1)
@@ -782,6 +1017,11 @@ class DailyTransportClient(EventHandler):
782
1017
  return track
783
1018
 
784
1019
  async def remove_custom_audio_track(self, track_name: str):
1020
+ """Remove a custom audio track.
1021
+
1022
+ Args:
1023
+ track_name: Name of the custom audio track to remove.
1024
+ """
785
1025
  future = self._get_event_loop().create_future()
786
1026
  self._client.remove_custom_audio_track(
787
1027
  track_name=track_name,
@@ -790,6 +1030,12 @@ class DailyTransportClient(EventHandler):
790
1030
  await future
791
1031
 
792
1032
  async def update_transcription(self, participants=None, instance_id=None):
1033
+ """Update transcription settings for specific participants.
1034
+
1035
+ Args:
1036
+ participants: List of participant IDs to enable transcription for.
1037
+ instance_id: Optional transcription instance ID.
1038
+ """
793
1039
  future = self._get_event_loop().create_future()
794
1040
  self._client.update_transcription(
795
1041
  participants, instance_id, completion=completion_callback(future)
@@ -797,6 +1043,12 @@ class DailyTransportClient(EventHandler):
797
1043
  await future
798
1044
 
799
1045
  async def update_subscriptions(self, participant_settings=None, profile_settings=None):
1046
+ """Update media subscription settings.
1047
+
1048
+ Args:
1049
+ participant_settings: Per-participant subscription settings.
1050
+ profile_settings: Global subscription profile settings.
1051
+ """
800
1052
  future = self._get_event_loop().create_future()
801
1053
  self._client.update_subscriptions(
802
1054
  participant_settings=participant_settings,
@@ -806,6 +1058,11 @@ class DailyTransportClient(EventHandler):
806
1058
  await future
807
1059
 
808
1060
  async def update_publishing(self, publishing_settings: Mapping[str, Any]):
1061
+ """Update media publishing settings.
1062
+
1063
+ Args:
1064
+ publishing_settings: Publishing configuration settings.
1065
+ """
809
1066
  future = self._get_event_loop().create_future()
810
1067
  self._client.update_publishing(
811
1068
  publishing_settings=publishing_settings,
@@ -814,6 +1071,11 @@ class DailyTransportClient(EventHandler):
814
1071
  await future
815
1072
 
816
1073
  async def update_remote_participants(self, remote_participants: Mapping[str, Any]):
1074
+ """Update settings for remote participants.
1075
+
1076
+ Args:
1077
+ remote_participants: Remote participant configuration settings.
1078
+ """
817
1079
  future = self._get_event_loop().create_future()
818
1080
  self._client.update_remote_participants(
819
1081
  remote_participants=remote_participants, completion=completion_callback(future)
@@ -826,76 +1088,199 @@ class DailyTransportClient(EventHandler):
826
1088
  #
827
1089
 
828
1090
  def on_active_speaker_changed(self, participant):
1091
+ """Handle active speaker change events.
1092
+
1093
+ Args:
1094
+ participant: The new active speaker participant info.
1095
+ """
829
1096
  self._call_event_callback(self._callbacks.on_active_speaker_changed, participant)
830
1097
 
831
1098
  def on_app_message(self, message: Any, sender: str):
1099
+ """Handle application message events.
1100
+
1101
+ Args:
1102
+ message: The received message data.
1103
+ sender: ID of the message sender.
1104
+ """
832
1105
  self._call_event_callback(self._callbacks.on_app_message, message, sender)
833
1106
 
834
1107
  def on_call_state_updated(self, state: str):
1108
+ """Handle call state update events.
1109
+
1110
+ Args:
1111
+ state: The new call state.
1112
+ """
835
1113
  self._call_event_callback(self._callbacks.on_call_state_updated, state)
836
1114
 
837
1115
  def on_dialin_connected(self, data: Any):
1116
+ """Handle dial-in connected events.
1117
+
1118
+ Args:
1119
+ data: Dial-in connection data.
1120
+ """
838
1121
  self._call_event_callback(self._callbacks.on_dialin_connected, data)
839
1122
 
840
1123
  def on_dialin_ready(self, sip_endpoint: str):
1124
+ """Handle dial-in ready events.
1125
+
1126
+ Args:
1127
+ sip_endpoint: The SIP endpoint for dial-in.
1128
+ """
841
1129
  self._call_event_callback(self._callbacks.on_dialin_ready, sip_endpoint)
842
1130
 
843
1131
  def on_dialin_stopped(self, data: Any):
1132
+ """Handle dial-in stopped events.
1133
+
1134
+ Args:
1135
+ data: Dial-in stop data.
1136
+ """
844
1137
  self._call_event_callback(self._callbacks.on_dialin_stopped, data)
845
1138
 
846
1139
  def on_dialin_error(self, data: Any):
1140
+ """Handle dial-in error events.
1141
+
1142
+ Args:
1143
+ data: Dial-in error data.
1144
+ """
847
1145
  self._call_event_callback(self._callbacks.on_dialin_error, data)
848
1146
 
849
1147
  def on_dialin_warning(self, data: Any):
1148
+ """Handle dial-in warning events.
1149
+
1150
+ Args:
1151
+ data: Dial-in warning data.
1152
+ """
850
1153
  self._call_event_callback(self._callbacks.on_dialin_warning, data)
851
1154
 
852
1155
  def on_dialout_answered(self, data: Any):
1156
+ """Handle dial-out answered events.
1157
+
1158
+ Args:
1159
+ data: Dial-out answered data.
1160
+ """
853
1161
  self._call_event_callback(self._callbacks.on_dialout_answered, data)
854
1162
 
855
1163
  def on_dialout_connected(self, data: Any):
1164
+ """Handle dial-out connected events.
1165
+
1166
+ Args:
1167
+ data: Dial-out connection data.
1168
+ """
856
1169
  self._call_event_callback(self._callbacks.on_dialout_connected, data)
857
1170
 
858
1171
  def on_dialout_stopped(self, data: Any):
1172
+ """Handle dial-out stopped events.
1173
+
1174
+ Args:
1175
+ data: Dial-out stop data.
1176
+ """
859
1177
  self._call_event_callback(self._callbacks.on_dialout_stopped, data)
860
1178
 
861
1179
  def on_dialout_error(self, data: Any):
1180
+ """Handle dial-out error events.
1181
+
1182
+ Args:
1183
+ data: Dial-out error data.
1184
+ """
862
1185
  self._call_event_callback(self._callbacks.on_dialout_error, data)
863
1186
 
864
1187
  def on_dialout_warning(self, data: Any):
1188
+ """Handle dial-out warning events.
1189
+
1190
+ Args:
1191
+ data: Dial-out warning data.
1192
+ """
865
1193
  self._call_event_callback(self._callbacks.on_dialout_warning, data)
866
1194
 
867
1195
  def on_participant_joined(self, participant):
1196
+ """Handle participant joined events.
1197
+
1198
+ Args:
1199
+ participant: The participant that joined.
1200
+ """
868
1201
  self._call_event_callback(self._callbacks.on_participant_joined, participant)
869
1202
 
870
1203
  def on_participant_left(self, participant, reason):
1204
+ """Handle participant left events.
1205
+
1206
+ Args:
1207
+ participant: The participant that left.
1208
+ reason: Reason for leaving.
1209
+ """
871
1210
  self._call_event_callback(self._callbacks.on_participant_left, participant, reason)
872
1211
 
873
1212
  def on_participant_updated(self, participant):
1213
+ """Handle participant updated events.
1214
+
1215
+ Args:
1216
+ participant: The updated participant info.
1217
+ """
874
1218
  self._call_event_callback(self._callbacks.on_participant_updated, participant)
875
1219
 
876
1220
  def on_transcription_started(self, status):
1221
+ """Handle transcription started events.
1222
+
1223
+ Args:
1224
+ status: Transcription start status.
1225
+ """
877
1226
  logger.debug(f"Transcription started: {status}")
878
1227
  self._transcription_status = status
879
1228
  self._call_event_callback(self.update_transcription, self._transcription_ids)
880
1229
 
881
1230
  def on_transcription_stopped(self, stopped_by, stopped_by_error):
1231
+ """Handle transcription stopped events.
1232
+
1233
+ Args:
1234
+ stopped_by: Who stopped the transcription.
1235
+ stopped_by_error: Whether stopped due to error.
1236
+ """
882
1237
  logger.debug("Transcription stopped")
1238
+ self._call_event_callback(
1239
+ self._callbacks.on_transcription_stopped, stopped_by, stopped_by_error
1240
+ )
883
1241
 
884
1242
  def on_transcription_error(self, message):
1243
+ """Handle transcription error events.
1244
+
1245
+ Args:
1246
+ message: Error message.
1247
+ """
885
1248
  logger.error(f"Transcription error: {message}")
1249
+ self._call_event_callback(self._callbacks.on_transcription_error, message)
886
1250
 
887
1251
  def on_transcription_message(self, message):
1252
+ """Handle transcription message events.
1253
+
1254
+ Args:
1255
+ message: The transcription message data.
1256
+ """
888
1257
  self._call_event_callback(self._callbacks.on_transcription_message, message)
889
1258
 
890
1259
  def on_recording_started(self, status):
1260
+ """Handle recording started events.
1261
+
1262
+ Args:
1263
+ status: Recording start status.
1264
+ """
891
1265
  logger.debug(f"Recording started: {status}")
892
1266
  self._call_event_callback(self._callbacks.on_recording_started, status)
893
1267
 
894
1268
  def on_recording_stopped(self, stream_id):
1269
+ """Handle recording stopped events.
1270
+
1271
+ Args:
1272
+ stream_id: ID of the stopped recording stream.
1273
+ """
895
1274
  logger.debug(f"Recording stopped: {stream_id}")
896
1275
  self._call_event_callback(self._callbacks.on_recording_stopped, stream_id)
897
1276
 
898
1277
  def on_recording_error(self, stream_id, message):
1278
+ """Handle recording error events.
1279
+
1280
+ Args:
1281
+ stream_id: ID of the recording stream with error.
1282
+ message: Error message.
1283
+ """
899
1284
  logger.error(f"Recording error for {stream_id}: {message}")
900
1285
  self._call_event_callback(self._callbacks.on_recording_error, stream_id, message)
901
1286
 
@@ -904,12 +1289,14 @@ class DailyTransportClient(EventHandler):
904
1289
  #
905
1290
 
906
1291
  def _audio_data_received(self, participant_id: str, audio_data: AudioData, audio_source: str):
1292
+ """Handle received audio data from participants."""
907
1293
  callback = self._audio_renderers[participant_id][audio_source]
908
1294
  self._call_audio_callback(callback, participant_id, audio_data, audio_source)
909
1295
 
910
1296
  def _video_frame_received(
911
1297
  self, participant_id: str, video_frame: VideoFrame, video_source: str
912
1298
  ):
1299
+ """Handle received video frames from participants."""
913
1300
  callback = self._video_renderers[participant_id][video_source]
914
1301
  self._call_video_callback(callback, participant_id, video_frame, video_source)
915
1302
 
@@ -918,21 +1305,29 @@ class DailyTransportClient(EventHandler):
918
1305
  #
919
1306
 
920
1307
  def _call_audio_callback(self, callback, *args):
1308
+ """Queue an audio callback for async execution."""
921
1309
  self._call_async_callback(self._audio_queue, callback, *args)
922
1310
 
923
1311
  def _call_video_callback(self, callback, *args):
1312
+ """Queue a video callback for async execution."""
924
1313
  self._call_async_callback(self._video_queue, callback, *args)
925
1314
 
926
1315
  def _call_event_callback(self, callback, *args):
1316
+ """Queue an event callback for async execution."""
927
1317
  self._call_async_callback(self._event_queue, callback, *args)
928
1318
 
929
1319
  def _call_async_callback(self, queue: asyncio.Queue, callback, *args):
930
- future = asyncio.run_coroutine_threadsafe(
931
- queue.put((callback, *args)), self._get_event_loop()
932
- )
933
- future.result()
1320
+ """Queue a callback for async execution on the event loop."""
1321
+ try:
1322
+ future = asyncio.run_coroutine_threadsafe(
1323
+ queue.put((callback, *args)), self._get_event_loop()
1324
+ )
1325
+ future.result()
1326
+ except FuturesCancelledError:
1327
+ pass
934
1328
 
935
1329
  async def _callback_task_handler(self, queue: asyncio.Queue):
1330
+ """Handle queued callbacks from the specified queue."""
936
1331
  while True:
937
1332
  # Wait to process any callback until we are joined.
938
1333
  await self._joined_event.wait()
@@ -941,22 +1336,21 @@ class DailyTransportClient(EventHandler):
941
1336
  queue.task_done()
942
1337
 
943
1338
  def _get_event_loop(self) -> asyncio.AbstractEventLoop:
1339
+ """Get the event loop from the task manager."""
944
1340
  if not self._task_manager:
945
1341
  raise Exception(f"{self}: missing task manager (pipeline not started?)")
946
1342
  return self._task_manager.get_event_loop()
947
1343
 
948
1344
  def __str__(self):
1345
+ """String representation of the DailyTransportClient."""
949
1346
  return f"{self._transport_name}::DailyTransportClient"
950
1347
 
951
1348
 
952
1349
  class DailyInputTransport(BaseInputTransport):
953
1350
  """Handles incoming media streams and events from Daily calls.
954
1351
 
955
- Processes incoming audio, video, transcriptions and other events from Daily.
956
-
957
- Args:
958
- client: DailyTransportClient instance.
959
- params: Configuration parameters.
1352
+ Processes incoming audio, video, transcriptions and other events from Daily
1353
+ room participants, including participant media capture and event forwarding.
960
1354
  """
961
1355
 
962
1356
  def __init__(
@@ -966,6 +1360,14 @@ class DailyInputTransport(BaseInputTransport):
966
1360
  params: DailyParams,
967
1361
  **kwargs,
968
1362
  ):
1363
+ """Initialize the Daily input transport.
1364
+
1365
+ Args:
1366
+ transport: The parent transport instance.
1367
+ client: DailyTransportClient instance.
1368
+ params: Configuration parameters.
1369
+ **kwargs: Additional arguments passed to parent class.
1370
+ """
969
1371
  super().__init__(params, **kwargs)
970
1372
 
971
1373
  self._transport = transport
@@ -984,34 +1386,62 @@ class DailyInputTransport(BaseInputTransport):
984
1386
  # case we don't start streaming right away.
985
1387
  self._capture_participant_audio = []
986
1388
 
1389
+ # Audio task when using a virtual speaker (i.e. no user tracks).
1390
+ self._audio_in_task: Optional[asyncio.Task] = None
1391
+
987
1392
  self._vad_analyzer: Optional[VADAnalyzer] = params.vad_analyzer
988
1393
 
989
1394
  @property
990
1395
  def vad_analyzer(self) -> Optional[VADAnalyzer]:
1396
+ """Get the Voice Activity Detection analyzer.
1397
+
1398
+ Returns:
1399
+ The VAD analyzer instance if configured.
1400
+ """
991
1401
  return self._vad_analyzer
992
1402
 
993
1403
  async def start_audio_in_streaming(self):
1404
+ """Start receiving audio from participants."""
994
1405
  if not self._params.audio_in_enabled:
995
1406
  return
996
1407
 
997
1408
  logger.debug(f"Start receiving audio")
998
- for participant_id, audio_source, sample_rate in self._capture_participant_audio:
999
- await self._client.capture_participant_audio(
1000
- participant_id, self._on_participant_audio_data, audio_source, sample_rate
1001
- )
1409
+
1410
+ if self._params.audio_in_enabled:
1411
+ if self._params.audio_in_user_tracks:
1412
+ # Capture invididual participant tracks.
1413
+ for participant_id, audio_source, sample_rate in self._capture_participant_audio:
1414
+ await self._client.capture_participant_audio(
1415
+ participant_id, self._on_participant_audio_data, audio_source, sample_rate
1416
+ )
1417
+ elif not self._audio_in_task:
1418
+ # Create audio task. It reads audio frames from a single room
1419
+ # track and pushes them internally for VAD processing.
1420
+ self._audio_in_task = self.create_task(self._audio_in_task_handler())
1002
1421
 
1003
1422
  self._streaming_started = True
1004
1423
 
1005
1424
  async def setup(self, setup: FrameProcessorSetup):
1425
+ """Setup the input transport with shared client setup.
1426
+
1427
+ Args:
1428
+ setup: The frame processor setup configuration.
1429
+ """
1006
1430
  await super().setup(setup)
1007
1431
  await self._client.setup(setup)
1008
1432
 
1009
1433
  async def cleanup(self):
1434
+ """Cleanup input transport and shared resources."""
1010
1435
  await super().cleanup()
1011
1436
  await self._client.cleanup()
1012
1437
  await self._transport.cleanup()
1013
1438
 
1014
1439
  async def start(self, frame: StartFrame):
1440
+ """Start the input transport and join the Daily room.
1441
+
1442
+ Args:
1443
+ frame: The start frame containing initialization parameters.
1444
+ """
1015
1445
  # Parent start.
1016
1446
  await super().start(frame)
1017
1447
 
@@ -1033,22 +1463,46 @@ class DailyInputTransport(BaseInputTransport):
1033
1463
  await self.start_audio_in_streaming()
1034
1464
 
1035
1465
  async def stop(self, frame: EndFrame):
1466
+ """Stop the input transport and leave the Daily room.
1467
+
1468
+ Args:
1469
+ frame: The end frame signaling transport shutdown.
1470
+ """
1036
1471
  # Parent stop.
1037
1472
  await super().stop(frame)
1038
1473
  # Leave the room.
1039
1474
  await self._client.leave()
1475
+ # Stop audio thread.
1476
+ if self._audio_in_task:
1477
+ await self.cancel_task(self._audio_in_task)
1478
+ self._audio_in_task = None
1040
1479
 
1041
1480
  async def cancel(self, frame: CancelFrame):
1481
+ """Cancel the input transport and leave the Daily room.
1482
+
1483
+ Args:
1484
+ frame: The cancel frame signaling immediate cancellation.
1485
+ """
1042
1486
  # Parent stop.
1043
1487
  await super().cancel(frame)
1044
1488
  # Leave the room.
1045
1489
  await self._client.leave()
1490
+ # Stop audio thread.
1491
+ if self._audio_in_task:
1492
+ await self.cancel_task(self._audio_in_task)
1493
+ self._audio_in_task = None
1046
1494
 
1047
1495
  #
1048
1496
  # FrameProcessor
1049
1497
  #
1050
1498
 
1051
1499
  async def process_frame(self, frame: Frame, direction: FrameDirection):
1500
+ """Process incoming frames, including user image requests.
1501
+
1502
+ Args:
1503
+ frame: The frame to process.
1504
+ direction: The direction of frame flow in the pipeline.
1505
+ """
1052
1506
  await super().process_frame(frame, direction)
1053
1507
 
1054
1508
  if isinstance(frame, UserImageRequestFrame):
@@ -1059,9 +1513,20 @@ class DailyInputTransport(BaseInputTransport):
1059
1513
  #
1060
1514
 
1061
1515
  async def push_transcription_frame(self, frame: TranscriptionFrame | InterimTranscriptionFrame):
1516
+ """Push a transcription frame downstream.
1517
+
1518
+ Args:
1519
+ frame: The transcription frame to push.
1520
+ """
1062
1521
  await self.push_frame(frame)
1063
1522
 
1064
1523
  async def push_app_message(self, message: Any, sender: str):
1524
+ """Push an application message as an urgent transport frame.
1525
+
1526
+ Args:
1527
+ message: The message data to send.
1528
+ sender: ID of the message sender.
1529
+ """
1065
1530
  frame = DailyTransportMessageUrgentFrame(message=message, participant_id=sender)
1066
1531
  await self.push_frame(frame)
1067
1532
 
@@ -1075,6 +1540,13 @@ class DailyInputTransport(BaseInputTransport):
1075
1540
  audio_source: str = "microphone",
1076
1541
  sample_rate: int = 16000,
1077
1542
  ):
1543
+ """Capture audio from a specific participant.
1544
+
1545
+ Args:
1546
+ participant_id: ID of the participant to capture audio from.
1547
+ audio_source: Audio source to capture from.
1548
+ sample_rate: Desired sample rate for audio capture.
1549
+ """
1078
1550
  if self._streaming_started:
1079
1551
  await self._client.capture_participant_audio(
1080
1552
  participant_id, self._on_participant_audio_data, audio_source, sample_rate
@@ -1085,6 +1557,7 @@ class DailyInputTransport(BaseInputTransport):
1085
1557
  async def _on_participant_audio_data(
1086
1558
  self, participant_id: str, audio: AudioData, audio_source: str
1087
1559
  ):
1560
+ """Handle received participant audio data."""
1088
1561
  frame = UserAudioRawFrame(
1089
1562
  user_id=participant_id,
1090
1563
  audio=audio.audio_frames,
@@ -1094,6 +1567,12 @@ class DailyInputTransport(BaseInputTransport):
1094
1567
  frame.transport_source = audio_source
1095
1568
  await self.push_audio_frame(frame)
1096
1569
 
1570
+ async def _audio_in_task_handler(self):
1571
+ while True:
1572
+ frame = await self._client.read_next_audio_frame()
1573
+ if frame:
1574
+ await self.push_audio_frame(frame)
1575
+
1097
1576
  #
1098
1577
  # Camera in
1099
1578
  #
@@ -1105,6 +1584,14 @@ class DailyInputTransport(BaseInputTransport):
1105
1584
  video_source: str = "camera",
1106
1585
  color_format: str = "RGB",
1107
1586
  ):
1587
+ """Capture video from a specific participant.
1588
+
1589
+ Args:
1590
+ participant_id: ID of the participant to capture video from.
1591
+ framerate: Desired framerate for video capture.
1592
+ video_source: Video source to capture from.
1593
+ color_format: Color format for video frames.
1594
+ """
1108
1595
  if participant_id not in self._video_renderers:
1109
1596
  self._video_renderers[participant_id] = {}
1110
1597
 
@@ -1119,6 +1606,11 @@ class DailyInputTransport(BaseInputTransport):
1119
1606
  )
1120
1607
 
1121
1608
  async def request_participant_image(self, frame: UserImageRequestFrame):
1609
+ """Request a video frame from a specific participant.
1610
+
1611
+ Args:
1612
+ frame: The user image request frame.
1613
+ """
1122
1614
  if frame.user_id in self._video_renderers:
1123
1615
  video_source = frame.video_source if frame.video_source else "camera"
1124
1616
  self._video_renderers[frame.user_id][video_source]["render_next_frame"].append(frame)
@@ -1126,6 +1618,7 @@ class DailyInputTransport(BaseInputTransport):
1126
1618
  async def _on_participant_video_frame(
1127
1619
  self, participant_id: str, video_frame: VideoFrame, video_source: str
1128
1620
  ):
1621
+ """Handle received participant video frames."""
1129
1622
  render_frame = False
1130
1623
 
1131
1624
  curr_time = time.time()
@@ -1161,16 +1654,21 @@ class DailyInputTransport(BaseInputTransport):
1161
1654
  class DailyOutputTransport(BaseOutputTransport):
1162
1655
  """Handles outgoing media streams and events to Daily calls.
1163
1656
 
1164
- Manages sending audio, video and other data to Daily calls.
1165
-
1166
- Args:
1167
- client: DailyTransportClient instance.
1168
- params: Configuration parameters.
1657
+ Manages sending audio, video, DTMF tones, and other data to Daily calls,
1658
+ including audio destination registration and message transmission.
1169
1659
  """
1170
1660
 
1171
1661
  def __init__(
1172
1662
  self, transport: BaseTransport, client: DailyTransportClient, params: DailyParams, **kwargs
1173
1663
  ):
1664
+ """Initialize the Daily output transport.
1665
+
1666
+ Args:
1667
+ transport: The parent transport instance.
1668
+ client: DailyTransportClient instance.
1669
+ params: Configuration parameters.
1670
+ **kwargs: Additional arguments passed to parent class.
1671
+ """
1174
1672
  super().__init__(params, **kwargs)
1175
1673
 
1176
1674
  self._transport = transport
@@ -1180,15 +1678,26 @@ class DailyOutputTransport(BaseOutputTransport):
1180
1678
  self._initialized = False
1181
1679
 
1182
1680
  async def setup(self, setup: FrameProcessorSetup):
1681
+ """Setup the output transport with shared client setup.
1682
+
1683
+ Args:
1684
+ setup: The frame processor setup configuration.
1685
+ """
1183
1686
  await super().setup(setup)
1184
1687
  await self._client.setup(setup)
1185
1688
 
1186
1689
  async def cleanup(self):
1690
+ """Cleanup output transport and shared resources."""
1187
1691
  await super().cleanup()
1188
1692
  await self._client.cleanup()
1189
1693
  await self._transport.cleanup()
1190
1694
 
1191
1695
  async def start(self, frame: StartFrame):
1696
+ """Start the output transport and join the Daily room.
1697
+
1698
+ Args:
1699
+ frame: The start frame containing initialization parameters.
1700
+ """
1192
1701
  # Parent start.
1193
1702
  await super().start(frame)
1194
1703
 
@@ -1207,27 +1716,57 @@ class DailyOutputTransport(BaseOutputTransport):
1207
1716
  await self.set_transport_ready(frame)
1208
1717
 
1209
1718
  async def stop(self, frame: EndFrame):
1719
+ """Stop the output transport and leave the Daily room.
1720
+
1721
+ Args:
1722
+ frame: The end frame signaling transport shutdown.
1723
+ """
1210
1724
  # Parent stop.
1211
1725
  await super().stop(frame)
1212
1726
  # Leave the room.
1213
1727
  await self._client.leave()
1214
1728
 
1215
1729
  async def cancel(self, frame: CancelFrame):
1730
+ """Cancel the output transport and leave the Daily room.
1731
+
1732
+ Args:
1733
+ frame: The cancel frame signaling immediate cancellation.
1734
+ """
1216
1735
  # Parent stop.
1217
1736
  await super().cancel(frame)
1218
1737
  # Leave the room.
1219
1738
  await self._client.leave()
1220
1739
 
1221
1740
  async def send_message(self, frame: TransportMessageFrame | TransportMessageUrgentFrame):
1741
+ """Send a transport message to participants.
1742
+
1743
+ Args:
1744
+ frame: The transport message frame to send.
1745
+ """
1222
1746
  await self._client.send_message(frame)
1223
1747
 
1224
1748
  async def register_video_destination(self, destination: str):
1749
+ """Register a video output destination.
1750
+
1751
+ Args:
1752
+ destination: The destination identifier to register.
1753
+ """
1225
1754
  logger.warning(f"{self} registering video destinations is not supported yet")
1226
1755
 
1227
1756
  async def register_audio_destination(self, destination: str):
1757
+ """Register an audio output destination.
1758
+
1759
+ Args:
1760
+ destination: The destination identifier to register.
1761
+ """
1228
1762
  await self._client.register_audio_destination(destination)
1229
1763
 
1230
1764
  async def write_dtmf(self, frame: OutputDTMFFrame | OutputDTMFUrgentFrame):
1765
+ """Write DTMF tones to the call.
1766
+
1767
+ Args:
1768
+ frame: The DTMF frame containing tone information.
1769
+ """
1231
1770
  await self._client.send_dtmf(
1232
1771
  {
1233
1772
  "sessionId": frame.transport_destination,
@@ -1236,25 +1775,28 @@ class DailyOutputTransport(BaseOutputTransport):
1236
1775
  )
1237
1776
 
1238
1777
  async def write_audio_frame(self, frame: OutputAudioRawFrame):
1778
+ """Write an audio frame to the Daily call.
1779
+
1780
+ Args:
1781
+ frame: The audio frame to write.
1782
+ """
1239
1783
  await self._client.write_audio_frame(frame)
1240
1784
 
1241
1785
  async def write_video_frame(self, frame: OutputImageRawFrame):
1786
+ """Write a video frame to the Daily call.
1787
+
1788
+ Args:
1789
+ frame: The video frame to write.
1790
+ """
1242
1791
  await self._client.write_video_frame(frame)
1243
1792
 
1244
1793
 
1245
1794
  class DailyTransport(BaseTransport):
1246
1795
  """Transport implementation for Daily audio and video calls.
1247
1796
 
1248
- Handles audio/video streaming, transcription, recordings, dial-in,
1249
- dial-out, and call management through Daily's API.
1250
-
1251
- Args:
1252
- room_url: URL of the Daily room to connect to.
1253
- token: Optional authentication token for the room.
1254
- bot_name: Display name for the bot in the call.
1255
- params: Configuration parameters (DailyParams) for the transport.
1256
- input_name: Optional name for the input transport.
1257
- output_name: Optional name for the output transport.
1797
+ Provides comprehensive Daily integration including audio/video streaming,
1798
+ transcription, recording, dial-in/out functionality, and real-time communication
1799
+ features for conversational AI applications.
1258
1800
  """
1259
1801
 
1260
1802
  def __init__(
@@ -1266,6 +1808,16 @@ class DailyTransport(BaseTransport):
1266
1808
  input_name: Optional[str] = None,
1267
1809
  output_name: Optional[str] = None,
1268
1810
  ):
1811
+ """Initialize the Daily transport.
1812
+
1813
+ Args:
1814
+ room_url: URL of the Daily room to connect to.
1815
+ token: Optional authentication token for the room.
1816
+ bot_name: Display name for the bot in the call.
1817
+ params: Configuration parameters for the transport.
1818
+ input_name: Optional name for the input transport.
1819
+ output_name: Optional name for the output transport.
1820
+ """
1269
1821
  super().__init__(input_name=input_name, output_name=output_name)
1270
1822
 
1271
1823
  callbacks = DailyCallbacks(
@@ -1291,6 +1843,8 @@ class DailyTransport(BaseTransport):
1291
1843
  on_participant_left=self._on_participant_left,
1292
1844
  on_participant_updated=self._on_participant_updated,
1293
1845
  on_transcription_message=self._on_transcription_message,
1846
+ on_transcription_stopped=self._on_transcription_stopped,
1847
+ on_transcription_error=self._on_transcription_error,
1294
1848
  on_recording_started=self._on_recording_started,
1295
1849
  on_recording_stopped=self._on_recording_stopped,
1296
1850
  on_recording_error=self._on_recording_error,
@@ -1339,6 +1893,11 @@ class DailyTransport(BaseTransport):
1339
1893
  #
1340
1894
 
1341
1895
  def input(self) -> DailyInputTransport:
1896
+ """Get the input transport for receiving media and events.
1897
+
1898
+ Returns:
1899
+ The Daily input transport instance.
1900
+ """
1342
1901
  if not self._input:
1343
1902
  self._input = DailyInputTransport(
1344
1903
  self, self._client, self._params, name=self._input_name
@@ -1346,6 +1905,11 @@ class DailyTransport(BaseTransport):
1346
1905
  return self._input
1347
1906
 
1348
1907
  def output(self) -> DailyOutputTransport:
1908
+ """Get the output transport for sending media and events.
1909
+
1910
+ Returns:
1911
+ The Daily output transport instance.
1912
+ """
1349
1913
  if not self._output:
1350
1914
  self._output = DailyOutputTransport(
1351
1915
  self, self._client, self._params, name=self._output_name
@@ -1358,33 +1922,93 @@ class DailyTransport(BaseTransport):
1358
1922
 
1359
1923
  @property
1360
1924
  def room_url(self) -> str:
1925
+ """Get the Daily room URL.
1926
+
1927
+ Returns:
1928
+ The room URL this transport is connected to.
1929
+ """
1361
1930
  return self._client.room_url
1362
1931
 
1363
1932
  @property
1364
1933
  def participant_id(self) -> str:
1934
+ """Get the participant ID for this transport.
1935
+
1936
+ Returns:
1937
+ The participant ID assigned by Daily.
1938
+ """
1365
1939
  return self._client.participant_id
1366
1940
 
1941
+ def set_log_level(self, level: DailyLogLevel):
1942
+ """Set the logging level for Daily's internal logging system.
1943
+
1944
+ Args:
1945
+ level: The log level to set. Should be a member of the DailyLogLevel enum,
1946
+ such as DailyLogLevel.Info, DailyLogLevel.Debug, etc.
1947
+
1948
+ Example:
1949
+ transport.set_log_level(DailyLogLevel.Info)
1950
+ """
1951
+ Daily.set_log_level(level)
1952
+
1367
1953
  async def send_image(self, frame: OutputImageRawFrame | SpriteFrame):
1954
+ """Send an image frame to the Daily call.
1955
+
1956
+ Args:
1957
+ frame: The image frame to send.
1958
+ """
1368
1959
  if self._output:
1369
1960
  await self._output.queue_frame(frame, FrameDirection.DOWNSTREAM)
1370
1961
 
1371
1962
  async def send_audio(self, frame: OutputAudioRawFrame):
1963
+ """Send an audio frame to the Daily call.
1964
+
1965
+ Args:
1966
+ frame: The audio frame to send.
1967
+ """
1372
1968
  if self._output:
1373
1969
  await self._output.queue_frame(frame, FrameDirection.DOWNSTREAM)
1374
1970
 
1375
1971
  def participants(self):
1972
+ """Get current participants in the room.
1973
+
1974
+ Returns:
1975
+ Dictionary of participants keyed by participant ID.
1976
+ """
1376
1977
  return self._client.participants()
1377
1978
 
1378
1979
  def participant_counts(self):
1980
+ """Get participant count information.
1981
+
1982
+ Returns:
1983
+ Dictionary with participant count details.
1984
+ """
1379
1985
  return self._client.participant_counts()
1380
1986
 
1381
1987
  async def start_dialout(self, settings=None):
1988
+ """Start a dial-out call to a phone number.
1989
+
1990
+ Args:
1991
+ settings: Dial-out configuration settings.
1992
+ """
1382
1993
  await self._client.start_dialout(settings)
1383
1994
 
1384
1995
  async def stop_dialout(self, participant_id):
1996
+ """Stop a dial-out call for a specific participant.
1997
+
1998
+ Args:
1999
+ participant_id: ID of the participant to stop dial-out for.
2000
+ """
1385
2001
  await self._client.stop_dialout(participant_id)
1386
2002
 
1387
2003
  async def send_dtmf(self, settings):
2004
+ """Send DTMF tones during a call (deprecated).
2005
+
2006
+ .. deprecated:: 0.0.69
2007
+ Push an `OutputDTMFFrame` or an `OutputDTMFUrgentFrame` instead.
2008
+
2009
+ Args:
2010
+ settings: DTMF settings including tones and target session.
2011
+ """
1388
2012
  import warnings
1389
2013
 
1390
2014
  with warnings.catch_warnings():
@@ -1396,33 +2020,66 @@ class DailyTransport(BaseTransport):
1396
2020
  await self._client.send_dtmf(settings)
1397
2021
 
1398
2022
  async def sip_call_transfer(self, settings):
2023
+ """Transfer a SIP call to another destination.
2024
+
2025
+ Args:
2026
+ settings: SIP call transfer settings.
2027
+ """
1399
2028
  await self._client.sip_call_transfer(settings)
1400
2029
 
1401
2030
  async def sip_refer(self, settings):
2031
+ """Send a SIP REFER request.
2032
+
2033
+ Args:
2034
+ settings: SIP REFER settings.
2035
+ """
1402
2036
  await self._client.sip_refer(settings)
1403
2037
 
1404
2038
  async def start_recording(self, streaming_settings=None, stream_id=None, force_new=None):
2039
+ """Start recording the call.
2040
+
2041
+ Args:
2042
+ streaming_settings: Recording configuration settings.
2043
+ stream_id: Unique identifier for the recording stream.
2044
+ force_new: Whether to force a new recording session.
2045
+ """
1405
2046
  await self._client.start_recording(streaming_settings, stream_id, force_new)
1406
2047
 
1407
2048
  async def stop_recording(self, stream_id=None):
2049
+ """Stop recording the call.
2050
+
2051
+ Args:
2052
+ stream_id: Unique identifier for the recording stream to stop.
2053
+ """
1408
2054
  await self._client.stop_recording(stream_id)
1409
2055
 
1410
2056
  async def start_transcription(self, settings=None):
2057
+ """Start transcription for the call.
2058
+
2059
+ Args:
2060
+ settings: Transcription configuration settings.
2061
+ """
1411
2062
  await self._client.start_transcription(settings)
1412
2063
 
1413
2064
  async def stop_transcription(self):
2065
+ """Stop transcription for the call."""
1414
2066
  await self._client.stop_transcription()
1415
2067
 
1416
2068
  async def send_prebuilt_chat_message(self, message: str, user_name: Optional[str] = None):
1417
- """Sends a chat message to Daily's Prebuilt main room.
2069
+ """Send a chat message to Daily's Prebuilt main room.
1418
2070
 
1419
2071
  Args:
1420
- message: The chat message to send
1421
- user_name: Optional user name that will appear as sender of the message
2072
+ message: The chat message to send.
2073
+ user_name: Optional user name that will appear as sender of the message.
1422
2074
  """
1423
2075
  await self._client.send_prebuilt_chat_message(message, user_name)
1424
2076
 
1425
2077
  async def capture_participant_transcription(self, participant_id: str):
2078
+ """Enable transcription capture for a specific participant.
2079
+
2080
+ Args:
2081
+ participant_id: ID of the participant to capture transcription for.
2082
+ """
1426
2083
  await self._client.capture_participant_transcription(participant_id)
1427
2084
 
1428
2085
  async def capture_participant_audio(
@@ -1431,6 +2088,13 @@ class DailyTransport(BaseTransport):
1431
2088
  audio_source: str = "microphone",
1432
2089
  sample_rate: int = 16000,
1433
2090
  ):
2091
+ """Capture audio from a specific participant.
2092
+
2093
+ Args:
2094
+ participant_id: ID of the participant to capture audio from.
2095
+ audio_source: Audio source to capture from.
2096
+ sample_rate: Desired sample rate for audio capture.
2097
+ """
1434
2098
  if self._input:
1435
2099
  await self._input.capture_participant_audio(participant_id, audio_source, sample_rate)
1436
2100
 
@@ -1441,32 +2105,60 @@ class DailyTransport(BaseTransport):
1441
2105
  video_source: str = "camera",
1442
2106
  color_format: str = "RGB",
1443
2107
  ):
2108
+ """Capture video from a specific participant.
2109
+
2110
+ Args:
2111
+ participant_id: ID of the participant to capture video from.
2112
+ framerate: Desired framerate for video capture.
2113
+ video_source: Video source to capture from.
2114
+ color_format: Color format for video frames.
2115
+ """
1444
2116
  if self._input:
1445
2117
  await self._input.capture_participant_video(
1446
2118
  participant_id, framerate, video_source, color_format
1447
2119
  )
1448
2120
 
1449
2121
  async def update_publishing(self, publishing_settings: Mapping[str, Any]):
2122
+ """Update media publishing settings.
2123
+
2124
+ Args:
2125
+ publishing_settings: Publishing configuration settings.
2126
+ """
1450
2127
  await self._client.update_publishing(publishing_settings=publishing_settings)
1451
2128
 
1452
2129
  async def update_subscriptions(self, participant_settings=None, profile_settings=None):
2130
+ """Update media subscription settings.
2131
+
2132
+ Args:
2133
+ participant_settings: Per-participant subscription settings.
2134
+ profile_settings: Global subscription profile settings.
2135
+ """
1453
2136
  await self._client.update_subscriptions(
1454
2137
  participant_settings=participant_settings, profile_settings=profile_settings
1455
2138
  )
1456
2139
 
1457
2140
  async def update_remote_participants(self, remote_participants: Mapping[str, Any]):
2141
+ """Update settings for remote participants.
2142
+
2143
+ Args:
2144
+ remote_participants: Remote participant configuration settings.
2145
+ """
1458
2146
  await self._client.update_remote_participants(remote_participants=remote_participants)
1459
2147
 
1460
2148
  async def _on_active_speaker_changed(self, participant: Any):
2149
+ """Handle active speaker change events."""
1461
2150
  await self._call_event_handler("on_active_speaker_changed", participant)
1462
2151
 
1463
2152
  async def _on_joined(self, data):
2153
+ """Handle room joined events."""
1464
2154
  await self._call_event_handler("on_joined", data)
1465
2155
 
1466
2156
  async def _on_left(self):
2157
+ """Handle room left events."""
1467
2158
  await self._call_event_handler("on_left")
1468
2159
 
1469
2160
  async def _on_error(self, error):
2161
+ """Handle error events and push error frames."""
1470
2162
  await self._call_event_handler("on_error", error)
1471
2163
  # Push error frame to notify the pipeline
1472
2164
  error_frame = ErrorFrame(error)
@@ -1480,20 +2172,25 @@ class DailyTransport(BaseTransport):
1480
2172
  raise Exception("No valid input or output channel to push error")
1481
2173
 
1482
2174
  async def _on_app_message(self, message: Any, sender: str):
2175
+ """Handle application message events."""
1483
2176
  if self._input:
1484
2177
  await self._input.push_app_message(message, sender)
1485
2178
  await self._call_event_handler("on_app_message", message, sender)
1486
2179
 
1487
2180
  async def _on_call_state_updated(self, state: str):
2181
+ """Handle call state update events."""
1488
2182
  await self._call_event_handler("on_call_state_updated", state)
1489
2183
 
1490
2184
  async def _on_client_connected(self, participant: Any):
2185
+ """Handle client connected events."""
1491
2186
  await self._call_event_handler("on_client_connected", participant)
1492
2187
 
1493
2188
  async def _on_client_disconnected(self, participant: Any):
2189
+ """Handle client disconnected events."""
1494
2190
  await self._call_event_handler("on_client_disconnected", participant)
1495
2191
 
1496
2192
  async def _handle_dialin_ready(self, sip_endpoint: str):
2193
+ """Handle dial-in ready events by updating SIP configuration."""
1497
2194
  if not self._params.dialin_settings:
1498
2195
  return
1499
2196
 
@@ -1528,42 +2225,53 @@ class DailyTransport(BaseTransport):
1528
2225
  logger.exception(f"Error handling dialin-ready event ({url}): {e}")
1529
2226
 
1530
2227
  async def _on_dialin_connected(self, data):
2228
+ """Handle dial-in connected events."""
1531
2229
  await self._call_event_handler("on_dialin_connected", data)
1532
2230
 
1533
2231
  async def _on_dialin_ready(self, sip_endpoint):
2232
+ """Handle dial-in ready events."""
1534
2233
  if self._params.dialin_settings:
1535
2234
  await self._handle_dialin_ready(sip_endpoint)
1536
2235
  await self._call_event_handler("on_dialin_ready", sip_endpoint)
1537
2236
 
1538
2237
  async def _on_dialin_stopped(self, data):
2238
+ """Handle dial-in stopped events."""
1539
2239
  await self._call_event_handler("on_dialin_stopped", data)
1540
2240
 
1541
2241
  async def _on_dialin_error(self, data):
2242
+ """Handle dial-in error events."""
1542
2243
  await self._call_event_handler("on_dialin_error", data)
1543
2244
 
1544
2245
  async def _on_dialin_warning(self, data):
2246
+ """Handle dial-in warning events."""
1545
2247
  await self._call_event_handler("on_dialin_warning", data)
1546
2248
 
1547
2249
  async def _on_dialout_answered(self, data):
2250
+ """Handle dial-out answered events."""
1548
2251
  await self._call_event_handler("on_dialout_answered", data)
1549
2252
 
1550
2253
  async def _on_dialout_connected(self, data):
2254
+ """Handle dial-out connected events."""
1551
2255
  await self._call_event_handler("on_dialout_connected", data)
1552
2256
 
1553
2257
  async def _on_dialout_stopped(self, data):
2258
+ """Handle dial-out stopped events."""
1554
2259
  await self._call_event_handler("on_dialout_stopped", data)
1555
2260
 
1556
2261
  async def _on_dialout_error(self, data):
2262
+ """Handle dial-out error events."""
1557
2263
  await self._call_event_handler("on_dialout_error", data)
1558
2264
 
1559
2265
  async def _on_dialout_warning(self, data):
2266
+ """Handle dial-out warning events."""
1560
2267
  await self._call_event_handler("on_dialout_warning", data)
1561
2268
 
1562
2269
  async def _on_participant_joined(self, participant):
2270
+ """Handle participant joined events."""
1563
2271
  id = participant["id"]
1564
2272
  logger.info(f"Participant joined {id}")
1565
2273
 
1566
- if self._input and self._params.audio_in_enabled:
2274
+ if self._input and self._params.audio_in_enabled and self._params.audio_in_user_tracks:
1567
2275
  await self._input.capture_participant_audio(
1568
2276
  id, "microphone", self._client.in_sample_rate
1569
2277
  )
@@ -1577,6 +2285,7 @@ class DailyTransport(BaseTransport):
1577
2285
  await self._call_event_handler("on_client_connected", participant)
1578
2286
 
1579
2287
  async def _on_participant_left(self, participant, reason):
2288
+ """Handle participant left events."""
1580
2289
  id = participant["id"]
1581
2290
  logger.info(f"Participant left {id}")
1582
2291
  await self._call_event_handler("on_participant_left", participant, reason)
@@ -1584,9 +2293,11 @@ class DailyTransport(BaseTransport):
1584
2293
  await self._call_event_handler("on_client_disconnected", participant)
1585
2294
 
1586
2295
  async def _on_participant_updated(self, participant):
2296
+ """Handle participant updated events."""
1587
2297
  await self._call_event_handler("on_participant_updated", participant)
1588
2298
 
1589
2299
  async def _on_transcription_message(self, message):
2300
+ """Handle transcription message events."""
1590
2301
  await self._call_event_handler("on_transcription_message", message)
1591
2302
 
1592
2303
  participant_id = ""
@@ -1618,11 +2329,22 @@ class DailyTransport(BaseTransport):
1618
2329
  if self._input:
1619
2330
  await self._input.push_transcription_frame(frame)
1620
2331
 
2332
+ async def _on_transcription_stopped(self, stopped_by, stopped_by_error):
2333
+ """Handle transcription stopped events."""
2334
+ await self._call_event_handler("on_transcription_stopped", stopped_by, stopped_by_error)
2335
+
2336
+ async def _on_transcription_error(self, message):
2337
+ """Handle transcription error events."""
2338
+ await self._call_event_handler("on_transcription_error", message)
2339
+
1621
2340
  async def _on_recording_started(self, status):
2341
+ """Handle recording started events."""
1622
2342
  await self._call_event_handler("on_recording_started", status)
1623
2343
 
1624
2344
  async def _on_recording_stopped(self, stream_id):
2345
+ """Handle recording stopped events."""
1625
2346
  await self._call_event_handler("on_recording_stopped", stream_id)
1626
2347
 
1627
2348
  async def _on_recording_error(self, stream_id, message):
2349
+ """Handle recording error events."""
1628
2350
  await self._call_event_handler("on_recording_error", stream_id, message)