dv-pipecat-ai 0.0.82.dev857__py3-none-any.whl → 0.0.85.dev837__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 (195) hide show
  1. {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/METADATA +98 -130
  2. {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/RECORD +192 -140
  3. pipecat/adapters/base_llm_adapter.py +38 -1
  4. pipecat/adapters/services/anthropic_adapter.py +9 -14
  5. pipecat/adapters/services/aws_nova_sonic_adapter.py +120 -5
  6. pipecat/adapters/services/bedrock_adapter.py +236 -13
  7. pipecat/adapters/services/gemini_adapter.py +12 -8
  8. pipecat/adapters/services/open_ai_adapter.py +19 -7
  9. pipecat/adapters/services/open_ai_realtime_adapter.py +5 -0
  10. pipecat/audio/dtmf/dtmf-0.wav +0 -0
  11. pipecat/audio/dtmf/dtmf-1.wav +0 -0
  12. pipecat/audio/dtmf/dtmf-2.wav +0 -0
  13. pipecat/audio/dtmf/dtmf-3.wav +0 -0
  14. pipecat/audio/dtmf/dtmf-4.wav +0 -0
  15. pipecat/audio/dtmf/dtmf-5.wav +0 -0
  16. pipecat/audio/dtmf/dtmf-6.wav +0 -0
  17. pipecat/audio/dtmf/dtmf-7.wav +0 -0
  18. pipecat/audio/dtmf/dtmf-8.wav +0 -0
  19. pipecat/audio/dtmf/dtmf-9.wav +0 -0
  20. pipecat/audio/dtmf/dtmf-pound.wav +0 -0
  21. pipecat/audio/dtmf/dtmf-star.wav +0 -0
  22. pipecat/audio/filters/krisp_viva_filter.py +193 -0
  23. pipecat/audio/filters/noisereduce_filter.py +15 -0
  24. pipecat/audio/turn/base_turn_analyzer.py +9 -1
  25. pipecat/audio/turn/smart_turn/base_smart_turn.py +14 -8
  26. pipecat/audio/turn/smart_turn/data/__init__.py +0 -0
  27. pipecat/audio/turn/smart_turn/data/smart-turn-v3.0.onnx +0 -0
  28. pipecat/audio/turn/smart_turn/http_smart_turn.py +6 -2
  29. pipecat/audio/turn/smart_turn/local_smart_turn.py +1 -1
  30. pipecat/audio/turn/smart_turn/local_smart_turn_v2.py +1 -1
  31. pipecat/audio/turn/smart_turn/local_smart_turn_v3.py +124 -0
  32. pipecat/audio/vad/data/README.md +10 -0
  33. pipecat/audio/vad/data/silero_vad_v2.onnx +0 -0
  34. pipecat/audio/vad/silero.py +9 -3
  35. pipecat/audio/vad/vad_analyzer.py +13 -1
  36. pipecat/extensions/voicemail/voicemail_detector.py +5 -5
  37. pipecat/frames/frames.py +277 -86
  38. pipecat/observers/loggers/debug_log_observer.py +3 -3
  39. pipecat/observers/loggers/llm_log_observer.py +7 -3
  40. pipecat/observers/loggers/user_bot_latency_log_observer.py +22 -10
  41. pipecat/pipeline/runner.py +18 -6
  42. pipecat/pipeline/service_switcher.py +64 -36
  43. pipecat/pipeline/task.py +125 -79
  44. pipecat/pipeline/tts_switcher.py +30 -0
  45. pipecat/processors/aggregators/dtmf_aggregator.py +2 -3
  46. pipecat/processors/aggregators/{gated_openai_llm_context.py → gated_llm_context.py} +9 -9
  47. pipecat/processors/aggregators/gated_open_ai_llm_context.py +12 -0
  48. pipecat/processors/aggregators/llm_context.py +40 -2
  49. pipecat/processors/aggregators/llm_response.py +32 -15
  50. pipecat/processors/aggregators/llm_response_universal.py +19 -15
  51. pipecat/processors/aggregators/user_response.py +6 -6
  52. pipecat/processors/aggregators/vision_image_frame.py +24 -2
  53. pipecat/processors/audio/audio_buffer_processor.py +43 -8
  54. pipecat/processors/dtmf_aggregator.py +174 -77
  55. pipecat/processors/filters/stt_mute_filter.py +17 -0
  56. pipecat/processors/frame_processor.py +110 -24
  57. pipecat/processors/frameworks/langchain.py +8 -2
  58. pipecat/processors/frameworks/rtvi.py +210 -68
  59. pipecat/processors/frameworks/strands_agents.py +170 -0
  60. pipecat/processors/logger.py +2 -2
  61. pipecat/processors/transcript_processor.py +26 -5
  62. pipecat/processors/user_idle_processor.py +35 -11
  63. pipecat/runner/daily.py +59 -20
  64. pipecat/runner/run.py +395 -93
  65. pipecat/runner/types.py +6 -4
  66. pipecat/runner/utils.py +51 -10
  67. pipecat/serializers/__init__.py +5 -1
  68. pipecat/serializers/asterisk.py +16 -2
  69. pipecat/serializers/convox.py +41 -4
  70. pipecat/serializers/custom.py +257 -0
  71. pipecat/serializers/exotel.py +5 -5
  72. pipecat/serializers/livekit.py +20 -0
  73. pipecat/serializers/plivo.py +5 -5
  74. pipecat/serializers/protobuf.py +6 -5
  75. pipecat/serializers/telnyx.py +2 -2
  76. pipecat/serializers/twilio.py +43 -23
  77. pipecat/serializers/vi.py +324 -0
  78. pipecat/services/ai_service.py +2 -6
  79. pipecat/services/anthropic/llm.py +2 -25
  80. pipecat/services/assemblyai/models.py +6 -0
  81. pipecat/services/assemblyai/stt.py +13 -5
  82. pipecat/services/asyncai/tts.py +5 -3
  83. pipecat/services/aws/__init__.py +1 -0
  84. pipecat/services/aws/llm.py +147 -105
  85. pipecat/services/aws/nova_sonic/__init__.py +0 -0
  86. pipecat/services/aws/nova_sonic/context.py +436 -0
  87. pipecat/services/aws/nova_sonic/frames.py +25 -0
  88. pipecat/services/aws/nova_sonic/llm.py +1265 -0
  89. pipecat/services/aws/stt.py +3 -3
  90. pipecat/services/aws_nova_sonic/__init__.py +19 -1
  91. pipecat/services/aws_nova_sonic/aws.py +11 -1151
  92. pipecat/services/aws_nova_sonic/context.py +8 -354
  93. pipecat/services/aws_nova_sonic/frames.py +13 -17
  94. pipecat/services/azure/llm.py +51 -1
  95. pipecat/services/azure/realtime/__init__.py +0 -0
  96. pipecat/services/azure/realtime/llm.py +65 -0
  97. pipecat/services/azure/stt.py +15 -0
  98. pipecat/services/cartesia/stt.py +77 -70
  99. pipecat/services/cartesia/tts.py +80 -13
  100. pipecat/services/deepgram/__init__.py +1 -0
  101. pipecat/services/deepgram/flux/__init__.py +0 -0
  102. pipecat/services/deepgram/flux/stt.py +640 -0
  103. pipecat/services/elevenlabs/__init__.py +4 -1
  104. pipecat/services/elevenlabs/stt.py +339 -0
  105. pipecat/services/elevenlabs/tts.py +87 -46
  106. pipecat/services/fish/tts.py +5 -2
  107. pipecat/services/gemini_multimodal_live/events.py +38 -524
  108. pipecat/services/gemini_multimodal_live/file_api.py +23 -173
  109. pipecat/services/gemini_multimodal_live/gemini.py +41 -1403
  110. pipecat/services/gladia/stt.py +56 -72
  111. pipecat/services/google/__init__.py +1 -0
  112. pipecat/services/google/gemini_live/__init__.py +3 -0
  113. pipecat/services/google/gemini_live/file_api.py +189 -0
  114. pipecat/services/google/gemini_live/llm.py +1582 -0
  115. pipecat/services/google/gemini_live/llm_vertex.py +184 -0
  116. pipecat/services/google/llm.py +15 -11
  117. pipecat/services/google/llm_openai.py +3 -3
  118. pipecat/services/google/llm_vertex.py +86 -16
  119. pipecat/services/google/stt.py +4 -0
  120. pipecat/services/google/tts.py +7 -3
  121. pipecat/services/heygen/api.py +2 -0
  122. pipecat/services/heygen/client.py +8 -4
  123. pipecat/services/heygen/video.py +2 -0
  124. pipecat/services/hume/__init__.py +5 -0
  125. pipecat/services/hume/tts.py +220 -0
  126. pipecat/services/inworld/tts.py +6 -6
  127. pipecat/services/llm_service.py +15 -5
  128. pipecat/services/lmnt/tts.py +4 -2
  129. pipecat/services/mcp_service.py +4 -2
  130. pipecat/services/mem0/memory.py +6 -5
  131. pipecat/services/mistral/llm.py +29 -8
  132. pipecat/services/moondream/vision.py +42 -16
  133. pipecat/services/neuphonic/tts.py +5 -2
  134. pipecat/services/openai/__init__.py +1 -0
  135. pipecat/services/openai/base_llm.py +27 -20
  136. pipecat/services/openai/realtime/__init__.py +0 -0
  137. pipecat/services/openai/realtime/context.py +272 -0
  138. pipecat/services/openai/realtime/events.py +1106 -0
  139. pipecat/services/openai/realtime/frames.py +37 -0
  140. pipecat/services/openai/realtime/llm.py +829 -0
  141. pipecat/services/openai/tts.py +49 -10
  142. pipecat/services/openai_realtime/__init__.py +27 -0
  143. pipecat/services/openai_realtime/azure.py +21 -0
  144. pipecat/services/openai_realtime/context.py +21 -0
  145. pipecat/services/openai_realtime/events.py +21 -0
  146. pipecat/services/openai_realtime/frames.py +21 -0
  147. pipecat/services/openai_realtime_beta/azure.py +16 -0
  148. pipecat/services/openai_realtime_beta/openai.py +17 -5
  149. pipecat/services/piper/tts.py +7 -9
  150. pipecat/services/playht/tts.py +34 -4
  151. pipecat/services/rime/tts.py +12 -12
  152. pipecat/services/riva/stt.py +3 -1
  153. pipecat/services/salesforce/__init__.py +9 -0
  154. pipecat/services/salesforce/llm.py +700 -0
  155. pipecat/services/sarvam/__init__.py +7 -0
  156. pipecat/services/sarvam/stt.py +540 -0
  157. pipecat/services/sarvam/tts.py +97 -13
  158. pipecat/services/simli/video.py +2 -2
  159. pipecat/services/speechmatics/stt.py +22 -10
  160. pipecat/services/stt_service.py +47 -0
  161. pipecat/services/tavus/video.py +2 -2
  162. pipecat/services/tts_service.py +75 -22
  163. pipecat/services/vision_service.py +7 -6
  164. pipecat/services/vistaar/llm.py +51 -9
  165. pipecat/tests/utils.py +4 -4
  166. pipecat/transcriptions/language.py +41 -1
  167. pipecat/transports/base_input.py +13 -34
  168. pipecat/transports/base_output.py +140 -104
  169. pipecat/transports/daily/transport.py +199 -26
  170. pipecat/transports/heygen/__init__.py +0 -0
  171. pipecat/transports/heygen/transport.py +381 -0
  172. pipecat/transports/livekit/transport.py +228 -63
  173. pipecat/transports/local/audio.py +6 -1
  174. pipecat/transports/local/tk.py +11 -2
  175. pipecat/transports/network/fastapi_websocket.py +1 -1
  176. pipecat/transports/smallwebrtc/connection.py +103 -19
  177. pipecat/transports/smallwebrtc/request_handler.py +246 -0
  178. pipecat/transports/smallwebrtc/transport.py +65 -23
  179. pipecat/transports/tavus/transport.py +23 -12
  180. pipecat/transports/websocket/client.py +41 -5
  181. pipecat/transports/websocket/fastapi.py +21 -11
  182. pipecat/transports/websocket/server.py +14 -7
  183. pipecat/transports/whatsapp/api.py +8 -0
  184. pipecat/transports/whatsapp/client.py +47 -0
  185. pipecat/utils/base_object.py +54 -22
  186. pipecat/utils/redis.py +58 -0
  187. pipecat/utils/string.py +13 -1
  188. pipecat/utils/tracing/service_decorators.py +21 -21
  189. pipecat/serializers/genesys.py +0 -95
  190. pipecat/services/google/test-google-chirp.py +0 -45
  191. pipecat/services/openai.py +0 -698
  192. {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/WHEEL +0 -0
  193. {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/licenses/LICENSE +0 -0
  194. {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/top_level.txt +0 -0
  195. /pipecat/services/{aws_nova_sonic → aws/nova_sonic}/ready.wav +0 -0
@@ -95,15 +95,20 @@ class SmallWebRTCTrack:
95
95
  enable/disable control and frame discarding for audio and video streams.
96
96
  """
97
97
 
98
- def __init__(self, track: MediaStreamTrack):
98
+ def __init__(self, receiver):
99
99
  """Initialize the WebRTC track wrapper.
100
100
 
101
101
  Args:
102
- track: The underlying MediaStreamTrack to wrap.
103
- index: The index of the track in the transceiver (0 for mic, 1 for cam, 2 for screen)
102
+ receiver: The RemoteStreamTrack receiver instance.
104
103
  """
105
- self._track = track
104
+ self._receiver = receiver
105
+ # Configuring the receiver for not consuming the track by default to prevent memory grow
106
+ self._receiver._enabled = False
107
+ self._track = receiver.track
106
108
  self._enabled = True
109
+ self._last_recv_time: float = 0.0
110
+ self._idle_task: Optional[asyncio.Task] = None
111
+ self._idle_timeout: float = 2.0 # seconds before discarding old frames
107
112
 
108
113
  def set_enabled(self, enabled: bool) -> None:
109
114
  """Enable or disable the track.
@@ -138,13 +143,44 @@ class SmallWebRTCTrack:
138
143
  async def recv(self) -> Optional[Frame]:
139
144
  """Receive the next frame from the track.
140
145
 
146
+ Enables the internal receiving state and starts idle watcher.
147
+
141
148
  Returns:
142
149
  The next frame, except for video tracks, where it returns the frame only if the track is enabled, otherwise, returns None.
143
150
  """
151
+ self._receiver._enabled = True
152
+ self._last_recv_time = time.time()
153
+
154
+ # start idle watcher if not already running
155
+ if not self._idle_task or self._idle_task.done():
156
+ self._idle_task = asyncio.create_task(self._idle_watcher())
157
+
144
158
  if not self._enabled and self._track.kind == "video":
145
159
  return None
146
160
  return await self._track.recv()
147
161
 
162
+ async def _idle_watcher(self):
163
+ """Disable receiving if idle for more than _idle_timeout and monitor queue size."""
164
+ while self._receiver._enabled:
165
+ await asyncio.sleep(self._idle_timeout)
166
+ idle_duration = time.time() - self._last_recv_time
167
+ if idle_duration >= self._idle_timeout:
168
+ # discard old frames to prevent memory growth
169
+ logger.debug(
170
+ f"Disabling receiver for {self._track.kind} track after {idle_duration:.2f}s idle"
171
+ )
172
+ await self.discard_old_frames()
173
+ self._receiver._enabled = False
174
+
175
+ def stop(self):
176
+ """Stop receiving frames from the track."""
177
+ self._receiver._enabled = False
178
+ if self._idle_task:
179
+ self._idle_task.cancel()
180
+ self._idle_task = None
181
+ if self._track:
182
+ self._track.stop()
183
+
148
184
  def __getattr__(self, name):
149
185
  """Forward attribute access to the underlying track.
150
186
 
@@ -170,11 +206,16 @@ class SmallWebRTCConnection(BaseObject):
170
206
  for real-time audio/video communication.
171
207
  """
172
208
 
173
- def __init__(self, ice_servers: Optional[Union[List[str], List[IceServer]]] = None):
209
+ def __init__(
210
+ self,
211
+ ice_servers: Optional[Union[List[str], List[IceServer]]] = None,
212
+ connection_timeout_secs: int = 60,
213
+ ):
174
214
  """Initialize the WebRTC connection.
175
215
 
176
216
  Args:
177
217
  ice_servers: List of ICE servers as URLs or IceServer objects.
218
+ connection_timeout_secs: Timeout in seconds for connecting to the peer.
178
219
 
179
220
  Raises:
180
221
  TypeError: If ice_servers contains mixed types or unsupported types.
@@ -195,6 +236,7 @@ class SmallWebRTCConnection(BaseObject):
195
236
  VIDEO_TRANSCEIVER_INDEX: self.video_input_track,
196
237
  SCREEN_VIDEO_TRANSCEIVER_INDEX: self.screen_video_input_track,
197
238
  }
239
+ self.connection_timeout_secs = connection_timeout_secs
198
240
 
199
241
  self._initialize()
200
242
 
@@ -241,8 +283,8 @@ class SmallWebRTCConnection(BaseObject):
241
283
  self._data_channel = None
242
284
  self._renegotiation_in_progress = False
243
285
  self._last_received_time = None
244
- self._message_queue = []
245
286
  self._pending_app_messages = []
287
+ self._connecting_timeout_task = None
246
288
 
247
289
  def _setup_listeners(self):
248
290
  """Set up event listeners for the peer connection."""
@@ -254,10 +296,7 @@ class SmallWebRTCConnection(BaseObject):
254
296
  # Flush queued messages once the data channel is open
255
297
  @channel.on("open")
256
298
  async def on_open():
257
- logger.debug("Data channel is open, flushing queued messages")
258
- while self._message_queue:
259
- message = self._message_queue.pop(0)
260
- self._data_channel.send(message)
299
+ logger.debug("Data channel is open!")
261
300
 
262
301
  @channel.on("message")
263
302
  async def on_message(message):
@@ -454,11 +493,15 @@ class SmallWebRTCConnection(BaseObject):
454
493
 
455
494
  async def _close(self):
456
495
  """Close the peer connection and cleanup resources."""
496
+ for track in self._track_map.values():
497
+ if track:
498
+ track.stop()
499
+ self._track_map.clear()
457
500
  if self._pc:
458
501
  await self._pc.close()
459
- self._message_queue.clear()
460
502
  self._pending_app_messages.clear()
461
503
  self._track_map = {}
504
+ self._cancel_monitoring_connecting_state()
462
505
 
463
506
  def get_answer(self):
464
507
  """Get the SDP answer for the current connection.
@@ -476,9 +519,45 @@ class SmallWebRTCConnection(BaseObject):
476
519
  "pc_id": self._pc_id,
477
520
  }
478
521
 
522
+ def _monitoring_connecting_state(self) -> None:
523
+ """Start monitoring the peer connection while it is in the *connecting* state.
524
+
525
+ This method schedules a timeout task that will automatically close the
526
+ connection if it remains in the connecting state for more than the specified
527
+ timeout, default to 60 seconds.
528
+ """
529
+ logger.debug("Monitoring connecting state")
530
+
531
+ async def timeout_handler():
532
+ # We will close the connection in case we have remained in the connecting state for over 1 minute
533
+ await asyncio.sleep(self.connection_timeout_secs)
534
+ logger.warning("Timeout establishing the connection to the remote peer. Closing.")
535
+
536
+ await self._close()
537
+
538
+ # Create and store the timeout task
539
+ self._connecting_timeout_task = asyncio.create_task(timeout_handler())
540
+
541
+ def _cancel_monitoring_connecting_state(self) -> None:
542
+ """Cancel the ongoing connecting-state timeout task, if any.
543
+
544
+ This method should be called once the connection has either succeeded or
545
+ transitioned out of the connecting state. If the timeout task is still
546
+ pending, it will be canceled and the reference cleared.
547
+ """
548
+ if self._connecting_timeout_task and not self._connecting_timeout_task.done():
549
+ logger.debug("Cancelling the connecting timeout task")
550
+ self._connecting_timeout_task.cancel()
551
+ self._connecting_timeout_task = None
552
+
479
553
  async def _handle_new_connection_state(self):
480
554
  """Handle changes in the peer connection state."""
481
555
  state = self._pc.connectionState
556
+ if state == "connecting":
557
+ self._monitoring_connecting_state()
558
+ else:
559
+ self._cancel_monitoring_connecting_state()
560
+
482
561
  if state == "connected" and not self._connect_invoked:
483
562
  # We are going to wait until the pipeline is ready before triggering the event
484
563
  return
@@ -526,8 +605,8 @@ class SmallWebRTCConnection(BaseObject):
526
605
  logger.warning("No audio transceiver is available")
527
606
  return None
528
607
 
529
- track = transceivers[AUDIO_TRANSCEIVER_INDEX].receiver.track
530
- audio_track = SmallWebRTCTrack(track) if track else None
608
+ receiver = transceivers[AUDIO_TRANSCEIVER_INDEX].receiver
609
+ audio_track = SmallWebRTCTrack(receiver) if receiver else None
531
610
  self._track_map[AUDIO_TRANSCEIVER_INDEX] = audio_track
532
611
  return audio_track
533
612
 
@@ -548,8 +627,8 @@ class SmallWebRTCConnection(BaseObject):
548
627
  logger.warning("No video transceiver is available")
549
628
  return None
550
629
 
551
- track = transceivers[VIDEO_TRANSCEIVER_INDEX].receiver.track
552
- video_track = SmallWebRTCTrack(track) if track else None
630
+ receiver = transceivers[VIDEO_TRANSCEIVER_INDEX].receiver
631
+ video_track = SmallWebRTCTrack(receiver) if receiver else None
553
632
  self._track_map[VIDEO_TRANSCEIVER_INDEX] = video_track
554
633
  return video_track
555
634
 
@@ -570,8 +649,8 @@ class SmallWebRTCConnection(BaseObject):
570
649
  logger.warning("No screen video transceiver is available")
571
650
  return None
572
651
 
573
- track = transceivers[SCREEN_VIDEO_TRANSCEIVER_INDEX].receiver.track
574
- video_track = SmallWebRTCTrack(track) if track else None
652
+ receiver = transceivers[SCREEN_VIDEO_TRANSCEIVER_INDEX].receiver
653
+ video_track = SmallWebRTCTrack(receiver) if receiver else None
575
654
  self._track_map[SCREEN_VIDEO_TRANSCEIVER_INDEX] = video_track
576
655
  return video_track
577
656
 
@@ -585,8 +664,8 @@ class SmallWebRTCConnection(BaseObject):
585
664
  if self._data_channel and self._data_channel.readyState == "open":
586
665
  self._data_channel.send(json_message)
587
666
  else:
588
- logger.debug("Data channel not ready, queuing message")
589
- self._message_queue.append(json_message)
667
+ # The client might choose never to create a data channel.
668
+ logger.trace("Data channel not ready, discarding message!")
590
669
 
591
670
  def ask_to_renegotiate(self):
592
671
  """Request renegotiation of the WebRTC connection."""
@@ -610,3 +689,8 @@ class SmallWebRTCConnection(BaseObject):
610
689
  )()
611
690
  if track:
612
691
  track.set_enabled(signalling_message.enabled)
692
+
693
+ async def add_ice_candidate(self, candidate):
694
+ """Handle incoming ICE candidates."""
695
+ logger.debug(f"Adding remote candidate: {candidate}")
696
+ await self.pc.addIceCandidate(candidate)
@@ -0,0 +1,246 @@
1
+ #
2
+ # Copyright (c) 2024–2025, Daily
3
+ #
4
+ # SPDX-License-Identifier: BSD 2-Clause License
5
+ #
6
+
7
+ """SmallWebRTC request handler for managing peer connections.
8
+
9
+ This module provides a client for handling web requests and managing WebRTC connections.
10
+ """
11
+
12
+ import asyncio
13
+ from dataclasses import dataclass
14
+ from enum import Enum
15
+ from typing import Any, Awaitable, Callable, Dict, List, Optional
16
+
17
+ from aiortc.sdp import candidate_from_sdp
18
+ from fastapi import HTTPException
19
+ from loguru import logger
20
+
21
+ from pipecat.transports.smallwebrtc.connection import IceServer, SmallWebRTCConnection
22
+
23
+
24
+ @dataclass
25
+ class SmallWebRTCRequest:
26
+ """Small WebRTC transport session arguments for the runner.
27
+
28
+ Parameters:
29
+ sdp: The SDP string (Session Description Protocol).
30
+ type: The type of the SDP, either "offer" or "answer".
31
+ pc_id: Optional identifier for the peer connection.
32
+ restart_pc: Optional whether to restart the peer connection.
33
+ request_data: Optional custom data sent by the customer.
34
+ """
35
+
36
+ sdp: str
37
+ type: str
38
+ pc_id: Optional[str] = None
39
+ restart_pc: Optional[bool] = None
40
+ request_data: Optional[Any] = None
41
+
42
+
43
+ @dataclass
44
+ class IceCandidate:
45
+ """The remote ice candidate object received from the peer connection.
46
+
47
+ Parameters:
48
+ candidate: The ice candidate patch SDP string (Session Description Protocol).
49
+ sdp_mid: The SDP mid for the candidate patch.
50
+ sdp_mline_index: The SDP mline index for the candidate patch.
51
+ """
52
+
53
+ candidate: str
54
+ sdp_mid: str
55
+ sdp_mline_index: int
56
+
57
+
58
+ @dataclass
59
+ class SmallWebRTCPatchRequest:
60
+ """Small WebRTC transport session arguments for the runner.
61
+
62
+ Parameters:
63
+ pc_id: Identifier for the peer connection.
64
+ candidates: A list of ICE candidate patches.
65
+ """
66
+
67
+ pc_id: str
68
+ candidates: List[IceCandidate]
69
+
70
+
71
+ class ConnectionMode(Enum):
72
+ """Enum defining the connection handling modes."""
73
+
74
+ SINGLE = "single" # Only one active connection allowed
75
+ MULTIPLE = "multiple" # Multiple simultaneous connections allowed
76
+
77
+
78
+ class SmallWebRTCRequestHandler:
79
+ """SmallWebRTC request handler for managing peer connections.
80
+
81
+ This class is responsible for:
82
+ - Handling incoming SmallWebRTC requests.
83
+ - Creating and managing WebRTC peer connections.
84
+ - Supporting ESP32-specific SDP munging if enabled.
85
+ - Invoking callbacks for newly initialized connections.
86
+ - Supporting both single and multiple connection modes.
87
+ """
88
+
89
+ def __init__(
90
+ self,
91
+ ice_servers: Optional[List[IceServer]] = None,
92
+ esp32_mode: bool = False,
93
+ host: Optional[str] = None,
94
+ connection_mode: ConnectionMode = ConnectionMode.MULTIPLE,
95
+ ) -> None:
96
+ """Initialize a SmallWebRTC request handler.
97
+
98
+ Args:
99
+ ice_servers (Optional[List[IceServer]]): List of ICE servers to use for WebRTC
100
+ connections.
101
+ esp32_mode (bool): If True, enables ESP32-specific SDP munging.
102
+ host (Optional[str]): Host address used for SDP munging in ESP32 mode.
103
+ Ignored if `esp32_mode` is False.
104
+ connection_mode (ConnectionMode): Mode of operation for handling connections.
105
+ SINGLE allows only one active connection, MULTIPLE allows several.
106
+ """
107
+ self._ice_servers = ice_servers
108
+ self._esp32_mode = esp32_mode
109
+ self._host = host
110
+ self._connection_mode = connection_mode
111
+
112
+ # Store connections by pc_id
113
+ self._pcs_map: Dict[str, SmallWebRTCConnection] = {}
114
+
115
+ def _check_single_connection_constraints(self, pc_id: Optional[str]) -> None:
116
+ """Check if the connection request satisfies single connection mode constraints.
117
+
118
+ Args:
119
+ pc_id: The peer connection ID from the request
120
+
121
+ Raises:
122
+ HTTPException: If constraints are violated in single connection mode
123
+ """
124
+ if self._connection_mode != ConnectionMode.SINGLE:
125
+ return
126
+
127
+ if not self._pcs_map: # No existing connections
128
+ return
129
+
130
+ # Get the existing connection (should be only one in single mode)
131
+ existing_connection = next(iter(self._pcs_map.values()))
132
+
133
+ if existing_connection.pc_id != pc_id and pc_id:
134
+ logger.warning(
135
+ f"Connection pc_id mismatch: existing={existing_connection.pc_id}, received={pc_id}"
136
+ )
137
+ raise HTTPException(status_code=400, detail="PC ID mismatch with existing connection")
138
+
139
+ if not pc_id:
140
+ logger.warning(
141
+ "Cannot create new connection: existing connection found but no pc_id received"
142
+ )
143
+ raise HTTPException(
144
+ status_code=400,
145
+ detail="Cannot create new connection with existing connection active",
146
+ )
147
+
148
+ def update_ice_servers(self, ice_servers: Optional[List[IceServer]] = None):
149
+ """Update the list of ICE servers used for WebRTC connections."""
150
+ self._ice_servers = ice_servers
151
+
152
+ async def handle_web_request(
153
+ self,
154
+ request: SmallWebRTCRequest,
155
+ webrtc_connection_callback: Callable[[Any], Awaitable[None]],
156
+ ) -> None:
157
+ """Handle a SmallWebRTC request and resolve the pending answer.
158
+
159
+ This method will:
160
+ - Reuse an existing WebRTC connection if `pc_id` exists.
161
+ - Otherwise, create a new `SmallWebRTCConnection`.
162
+ - Invoke the provided callback with the connection.
163
+ - Manage ESP32-specific munging if enabled.
164
+ - Enforce single/multiple connection mode constraints.
165
+
166
+ Args:
167
+ request (SmallWebRTCRequest): The incoming WebRTC request, containing
168
+ SDP, type, and optionally a `pc_id`.
169
+ webrtc_connection_callback (Callable[[Any], Awaitable[None]]): An
170
+ asynchronous callback function that is invoked with the WebRTC connection.
171
+
172
+ Raises:
173
+ HTTPException: If connection mode constraints are violated
174
+ Exception: Any exception raised during request handling or callback execution
175
+ will be logged and propagated.
176
+ """
177
+ try:
178
+ pc_id = request.pc_id
179
+
180
+ # Check connection mode constraints first
181
+ self._check_single_connection_constraints(pc_id)
182
+
183
+ # After constraints are satisfied, get the existing connection if any
184
+ existing_connection = self._pcs_map.get(pc_id) if pc_id else None
185
+
186
+ if existing_connection:
187
+ pipecat_connection = existing_connection
188
+ logger.info(f"Reusing existing connection for pc_id: {pc_id}")
189
+ await pipecat_connection.renegotiate(
190
+ sdp=request.sdp,
191
+ type=request.type,
192
+ restart_pc=request.restart_pc or False,
193
+ )
194
+ else:
195
+ pipecat_connection = SmallWebRTCConnection(ice_servers=self._ice_servers)
196
+ await pipecat_connection.initialize(sdp=request.sdp, type=request.type)
197
+
198
+ @pipecat_connection.event_handler("closed")
199
+ async def handle_disconnected(webrtc_connection: SmallWebRTCConnection):
200
+ logger.info(f"Discarding peer connection for pc_id: {webrtc_connection.pc_id}")
201
+ self._pcs_map.pop(webrtc_connection.pc_id, None)
202
+
203
+ # Invoke callback provided in runner arguments
204
+ try:
205
+ await webrtc_connection_callback(pipecat_connection)
206
+ logger.debug(
207
+ f"webrtc_connection_callback executed successfully for peer: {pipecat_connection.pc_id}"
208
+ )
209
+ except Exception as callback_error:
210
+ logger.error(
211
+ f"webrtc_connection_callback failed for peer {pipecat_connection.pc_id}: {callback_error}"
212
+ )
213
+
214
+ answer = pipecat_connection.get_answer()
215
+
216
+ if self._esp32_mode:
217
+ from pipecat.runner.utils import smallwebrtc_sdp_munging
218
+
219
+ answer["sdp"] = smallwebrtc_sdp_munging(answer["sdp"], self._host)
220
+
221
+ self._pcs_map[answer["pc_id"]] = pipecat_connection
222
+
223
+ return answer
224
+ except Exception as e:
225
+ logger.error(f"Error processing SmallWebRTC request: {e}")
226
+ logger.debug(f"SmallWebRTC request details: {request}")
227
+ raise
228
+
229
+ async def handle_patch_request(self, request: SmallWebRTCPatchRequest):
230
+ """Handle a SmallWebRTC patch candidate request."""
231
+ peer_connection = self._pcs_map.get(request.pc_id)
232
+
233
+ if not peer_connection:
234
+ raise HTTPException(status_code=404, detail="Peer connection not found")
235
+
236
+ for c in request.candidates:
237
+ candidate = candidate_from_sdp(c.candidate)
238
+ candidate.sdpMid = c.sdp_mid
239
+ candidate.sdpMLineIndex = c.sdp_mline_index
240
+ await peer_connection.add_ice_candidate(candidate)
241
+
242
+ async def close(self):
243
+ """Clear the connection map."""
244
+ coros = [pc.disconnect() for pc in self._pcs_map.values()]
245
+ await asyncio.gather(*coros)
246
+ self._pcs_map.clear()