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
@@ -26,13 +26,13 @@ from pipecat.frames.frames import (
26
26
  EndFrame,
27
27
  Frame,
28
28
  InputAudioRawFrame,
29
- InputTransportMessageUrgentFrame,
29
+ InputTransportMessageFrame,
30
30
  OutputAudioRawFrame,
31
31
  OutputImageRawFrame,
32
+ OutputTransportMessageFrame,
33
+ OutputTransportMessageUrgentFrame,
32
34
  SpriteFrame,
33
35
  StartFrame,
34
- TransportMessageFrame,
35
- TransportMessageUrgentFrame,
36
36
  UserImageRawFrame,
37
37
  UserImageRequestFrame,
38
38
  )
@@ -66,7 +66,7 @@ class SmallWebRTCCallbacks(BaseModel):
66
66
  on_client_disconnected: Called when a client disconnects.
67
67
  """
68
68
 
69
- on_app_message: Callable[[Any], Awaitable[None]]
69
+ on_app_message: Callable[[Any, str], Awaitable[None]]
70
70
  on_client_connected: Callable[[SmallWebRTCConnection], Awaitable[None]]
71
71
  on_client_disconnected: Callable[[SmallWebRTCConnection], Awaitable[None]]
72
72
 
@@ -254,7 +254,7 @@ class SmallWebRTCClient:
254
254
 
255
255
  @self._webrtc_connection.event_handler("app-message")
256
256
  async def on_app_message(connection: SmallWebRTCConnection, message: Any):
257
- await self._handle_app_message(message)
257
+ await self._handle_app_message(message, connection.pc_id)
258
258
 
259
259
  def _convert_frame(self, frame_array: np.ndarray, format_name: str) -> np.ndarray:
260
260
  """Convert a video frame to RGB format based on the input format.
@@ -309,7 +309,7 @@ class SmallWebRTCClient:
309
309
  # self._webrtc_connection.ask_to_renegotiate()
310
310
  frame = None
311
311
  except MediaStreamError:
312
- logger.warning("Received an unexpected media stream error while reading the audio.")
312
+ logger.warning("Received an unexpected media stream error while reading the video.")
313
313
  frame = None
314
314
 
315
315
  if frame is None or not isinstance(frame, VideoFrame):
@@ -321,15 +321,21 @@ class SmallWebRTCClient:
321
321
  # Convert frame to NumPy array in its native format
322
322
  frame_array = frame.to_ndarray(format=format_name)
323
323
  frame_rgb = self._convert_frame(frame_array, format_name)
324
+ del frame_array # free intermediate array immediately
325
+ image_bytes = frame_rgb.tobytes()
326
+ del frame_rgb # free RGB array immediately
324
327
 
325
328
  image_frame = UserImageRawFrame(
326
329
  user_id=self._webrtc_connection.pc_id,
327
- image=frame_rgb.tobytes(),
330
+ image=image_bytes,
328
331
  size=(frame.width, frame.height),
329
332
  format="RGB",
330
333
  )
331
334
  image_frame.transport_source = video_source
332
335
 
336
+ del frame # free original VideoFrame
337
+ del image_bytes # reference kept in image_frame
338
+
333
339
  yield image_frame
334
340
 
335
341
  async def read_audio_frame(self):
@@ -364,40 +370,62 @@ class SmallWebRTCClient:
364
370
  resampled_frames = self._pipecat_resampler.resample(frame)
365
371
  for resampled_frame in resampled_frames:
366
372
  # 16-bit PCM bytes
367
- pcm_bytes = resampled_frame.to_ndarray().astype(np.int16).tobytes()
373
+ pcm_array = resampled_frame.to_ndarray().astype(np.int16)
374
+ pcm_bytes = pcm_array.tobytes()
375
+ del pcm_array # free NumPy array immediately
376
+
368
377
  audio_frame = InputAudioRawFrame(
369
378
  audio=pcm_bytes,
370
379
  sample_rate=resampled_frame.sample_rate,
371
380
  num_channels=self._audio_in_channels,
372
381
  )
382
+ del pcm_bytes # reference kept in audio_frame
383
+
373
384
  yield audio_frame
374
385
  else:
375
386
  # 16-bit PCM bytes
376
- pcm_bytes = frame.to_ndarray().astype(np.int16).tobytes()
387
+ pcm_array = frame.to_ndarray().astype(np.int16)
388
+ pcm_bytes = pcm_array.tobytes()
389
+ del pcm_array # free NumPy array immediately
390
+
377
391
  audio_frame = InputAudioRawFrame(
378
392
  audio=pcm_bytes,
379
393
  sample_rate=frame.sample_rate,
380
394
  num_channels=self._audio_in_channels,
381
395
  )
396
+ del pcm_bytes # reference kept in audio_frame
397
+
382
398
  yield audio_frame
383
399
 
384
- async def write_audio_frame(self, frame: OutputAudioRawFrame):
400
+ del frame # free original AudioFrame
401
+
402
+ async def write_audio_frame(self, frame: OutputAudioRawFrame) -> bool:
385
403
  """Write an audio frame to the WebRTC connection.
386
404
 
387
405
  Args:
388
406
  frame: The audio frame to transmit.
407
+
408
+ Returns:
409
+ True if the audio frame was written successfully, False otherwise.
389
410
  """
390
411
  if self._can_send() and self._audio_output_track:
391
412
  await self._audio_output_track.add_audio_bytes(frame.audio)
413
+ return True
414
+ return False
392
415
 
393
- async def write_video_frame(self, frame: OutputImageRawFrame):
416
+ async def write_video_frame(self, frame: OutputImageRawFrame) -> bool:
394
417
  """Write a video frame to the WebRTC connection.
395
418
 
396
419
  Args:
397
420
  frame: The video frame to transmit.
421
+
422
+ Returns:
423
+ True if the video frame was written successfully, False otherwise.
398
424
  """
399
425
  if self._can_send() and self._video_output_track:
400
426
  self._video_output_track.add_video_frame(frame)
427
+ return True
428
+ return False
401
429
 
402
430
  async def setup(self, _params: TransportParams, frame):
403
431
  """Set up the client with transport parameters.
@@ -433,7 +461,9 @@ class SmallWebRTCClient:
433
461
  await self._webrtc_connection.disconnect()
434
462
  await self._handle_peer_disconnected()
435
463
 
436
- async def send_message(self, frame: TransportMessageFrame | TransportMessageUrgentFrame):
464
+ async def send_message(
465
+ self, frame: OutputTransportMessageFrame | OutputTransportMessageUrgentFrame
466
+ ):
437
467
  """Send an application message through the WebRTC connection.
438
468
 
439
469
  Args:
@@ -478,11 +508,15 @@ class SmallWebRTCClient:
478
508
  self._screen_video_track = None
479
509
  self._audio_output_track = None
480
510
  self._video_output_track = None
481
- await self._callbacks.on_client_disconnected(self._webrtc_connection)
482
511
 
483
- async def _handle_app_message(self, message: Any):
512
+ # Trigger `on_client_disconnected` if the client actually disconnects,
513
+ # that is, we are not the ones disconnecting.
514
+ if not self._closing:
515
+ await self._callbacks.on_client_disconnected(self._webrtc_connection)
516
+
517
+ async def _handle_app_message(self, message: Any, sender: str):
484
518
  """Handle incoming application messages."""
485
- await self._callbacks.on_app_message(message)
519
+ await self._callbacks.on_app_message(message, sender)
486
520
 
487
521
  def _can_send(self):
488
522
  """Check if the connection is ready for sending data."""
@@ -651,7 +685,7 @@ class SmallWebRTCInputTransport(BaseInputTransport):
651
685
  message: The application message to process.
652
686
  """
653
687
  logger.debug(f"Received app message inside SmallWebRTCInputTransport {message}")
654
- frame = InputTransportMessageUrgentFrame(message=message)
688
+ frame = InputTransportMessageFrame(message=message)
655
689
  await self.push_frame(frame)
656
690
 
657
691
  # Add this method similar to DailyInputTransport.request_participant_image
@@ -788,7 +822,9 @@ class SmallWebRTCOutputTransport(BaseOutputTransport):
788
822
  await super().cancel(frame)
789
823
  await self._client.disconnect()
790
824
 
791
- async def send_message(self, frame: TransportMessageFrame | TransportMessageUrgentFrame):
825
+ async def send_message(
826
+ self, frame: OutputTransportMessageFrame | OutputTransportMessageUrgentFrame
827
+ ):
792
828
  """Send a transport message through the WebRTC connection.
793
829
 
794
830
  Args:
@@ -796,21 +832,27 @@ class SmallWebRTCOutputTransport(BaseOutputTransport):
796
832
  """
797
833
  await self._client.send_message(frame)
798
834
 
799
- async def write_audio_frame(self, frame: OutputAudioRawFrame):
835
+ async def write_audio_frame(self, frame: OutputAudioRawFrame) -> bool:
800
836
  """Write an audio frame to the WebRTC connection.
801
837
 
802
838
  Args:
803
839
  frame: The output audio frame to transmit.
840
+
841
+ Returns:
842
+ True if the audio frame was written successfully, False otherwise.
804
843
  """
805
- await self._client.write_audio_frame(frame)
844
+ return await self._client.write_audio_frame(frame)
806
845
 
807
- async def write_video_frame(self, frame: OutputImageRawFrame):
846
+ async def write_video_frame(self, frame: OutputImageRawFrame) -> bool:
808
847
  """Write a video frame to the WebRTC connection.
809
848
 
810
849
  Args:
811
850
  frame: The output video frame to transmit.
851
+
852
+ Returns:
853
+ True if the video frame was written successfully, False otherwise.
812
854
  """
813
- await self._client.write_video_frame(frame)
855
+ return await self._client.write_video_frame(frame)
814
856
 
815
857
 
816
858
  class SmallWebRTCTransport(BaseTransport):
@@ -897,11 +939,11 @@ class SmallWebRTCTransport(BaseTransport):
897
939
  if self._output:
898
940
  await self._output.queue_frame(frame, FrameDirection.DOWNSTREAM)
899
941
 
900
- async def _on_app_message(self, message: Any):
942
+ async def _on_app_message(self, message: Any, sender: str):
901
943
  """Handle incoming application messages."""
902
944
  if self._input:
903
945
  await self._input.push_app_message(message)
904
- await self._call_event_handler("on_app_message", message)
946
+ await self._call_event_handler("on_app_message", message, sender)
905
947
 
906
948
  async def _on_client_connected(self, webrtc_connection):
907
949
  """Handle client connection events."""
@@ -25,11 +25,11 @@ from pipecat.frames.frames import (
25
25
  EndFrame,
26
26
  Frame,
27
27
  InputAudioRawFrame,
28
+ InterruptionFrame,
28
29
  OutputAudioRawFrame,
30
+ OutputTransportMessageFrame,
31
+ OutputTransportMessageUrgentFrame,
29
32
  StartFrame,
30
- StartInterruptionFrame,
31
- TransportMessageFrame,
32
- TransportMessageUrgentFrame,
33
33
  )
34
34
  from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, FrameProcessorSetup
35
35
  from pipecat.transports.base_input import BaseInputTransport
@@ -221,6 +221,7 @@ class TavusTransportClient:
221
221
  ),
222
222
  on_joined=self._on_joined,
223
223
  on_left=self._on_left,
224
+ on_before_leave=partial(self._on_handle_callback, "on_before_leave"),
224
225
  on_error=partial(self._on_handle_callback, "on_error"),
225
226
  on_app_message=partial(self._on_handle_callback, "on_app_message"),
226
227
  on_call_state_updated=partial(self._on_handle_callback, "on_call_state_updated"),
@@ -344,7 +345,9 @@ class TavusTransportClient:
344
345
  participant_id, callback, audio_source, sample_rate, callback_interval_ms
345
346
  )
346
347
 
347
- async def send_message(self, frame: TransportMessageFrame | TransportMessageUrgentFrame):
348
+ async def send_message(
349
+ self, frame: OutputTransportMessageFrame | OutputTransportMessageUrgentFrame
350
+ ):
348
351
  """Send a message to participants.
349
352
 
350
353
  Args:
@@ -372,7 +375,7 @@ class TavusTransportClient:
372
375
 
373
376
  async def send_interrupt_message(self) -> None:
374
377
  """Send an interrupt message to the conversation."""
375
- transport_frame = TransportMessageUrgentFrame(
378
+ transport_frame = OutputTransportMessageUrgentFrame(
376
379
  message={
377
380
  "message_type": "conversation",
378
381
  "event_type": "conversation.interrupt",
@@ -395,15 +398,18 @@ class TavusTransportClient:
395
398
  participant_settings=participant_settings, profile_settings=profile_settings
396
399
  )
397
400
 
398
- async def write_audio_frame(self, frame: OutputAudioRawFrame):
401
+ async def write_audio_frame(self, frame: OutputAudioRawFrame) -> bool:
399
402
  """Write an audio frame to the transport.
400
403
 
401
404
  Args:
402
405
  frame: The audio frame to write.
406
+
407
+ Returns:
408
+ True if the audio frame was written successfully, False otherwise.
403
409
  """
404
410
  if not self._client:
405
- return
406
- await self._client.write_audio_frame(frame)
411
+ return False
412
+ return await self._client.write_audio_frame(frame)
407
413
 
408
414
  async def register_audio_destination(self, destination: str):
409
415
  """Register an audio destination for output.
@@ -601,7 +607,9 @@ class TavusOutputTransport(BaseOutputTransport):
601
607
  await super().cancel(frame)
602
608
  await self._client.stop()
603
609
 
604
- async def send_message(self, frame: TransportMessageFrame | TransportMessageUrgentFrame):
610
+ async def send_message(
611
+ self, frame: OutputTransportMessageFrame | OutputTransportMessageUrgentFrame
612
+ ):
605
613
  """Send a message to participants.
606
614
 
607
615
  Args:
@@ -618,22 +626,25 @@ class TavusOutputTransport(BaseOutputTransport):
618
626
  direction: The direction of frame flow in the pipeline.
619
627
  """
620
628
  await super().process_frame(frame, direction)
621
- if isinstance(frame, StartInterruptionFrame):
629
+ if isinstance(frame, InterruptionFrame):
622
630
  await self._handle_interruptions()
623
631
 
624
632
  async def _handle_interruptions(self):
625
633
  """Handle interruption events by sending interrupt message."""
626
634
  await self._client.send_interrupt_message()
627
635
 
628
- async def write_audio_frame(self, frame: OutputAudioRawFrame):
636
+ async def write_audio_frame(self, frame: OutputAudioRawFrame) -> bool:
629
637
  """Write an audio frame to the Tavus transport.
630
638
 
631
639
  Args:
632
640
  frame: The audio frame to write.
641
+
642
+ Returns:
643
+ True if the audio frame was written successfully, False otherwise.
633
644
  """
634
645
  # This is the custom track destination expected by Tavus
635
646
  frame.transport_destination = self._transport_destination
636
- await self._client.write_audio_frame(frame)
647
+ return await self._client.write_audio_frame(frame)
637
648
 
638
649
  async def register_audio_destination(self, destination: str):
639
650
  """Register an audio destination.
@@ -28,9 +28,9 @@ from pipecat.frames.frames import (
28
28
  Frame,
29
29
  InputAudioRawFrame,
30
30
  OutputAudioRawFrame,
31
+ OutputTransportMessageFrame,
32
+ OutputTransportMessageUrgentFrame,
31
33
  StartFrame,
32
- TransportMessageFrame,
33
- TransportMessageUrgentFrame,
34
34
  )
35
35
  from pipecat.processors.frame_processor import FrameProcessorSetup
36
36
  from pipecat.serializers.base_serializer import FrameSerializer
@@ -150,17 +150,39 @@ class WebsocketClientSession:
150
150
  await self._websocket.close()
151
151
  self._websocket = None
152
152
 
153
- async def send(self, message: websockets.Data):
153
+ async def send(self, message: websockets.Data) -> bool:
154
154
  """Send a message through the WebSocket connection.
155
155
 
156
156
  Args:
157
157
  message: The message data to send.
158
158
  """
159
+ result = False
159
160
  try:
160
161
  if self._websocket:
161
162
  await self._websocket.send(message)
163
+ result = True
162
164
  except Exception as e:
163
165
  logger.error(f"{self} exception sending data: {e.__class__.__name__} ({e})")
166
+ finally:
167
+ return result
168
+
169
+ @property
170
+ def is_connected(self) -> bool:
171
+ """Check if the WebSocket is currently connected.
172
+
173
+ Returns:
174
+ True if the WebSocket is in connected state.
175
+ """
176
+ return self._websocket.state == websockets.State.OPEN if self._websocket else False
177
+
178
+ @property
179
+ def is_closing(self) -> bool:
180
+ """Check if the WebSocket is currently closing.
181
+
182
+ Returns:
183
+ True if the WebSocket is in the process of closing.
184
+ """
185
+ return self._websocket.state == websockets.State.CLOSING if self._websocket else False
164
186
 
165
187
  async def _client_task_handler(self):
166
188
  """Handle incoming messages from the WebSocket connection."""
@@ -363,7 +385,9 @@ class WebsocketClientOutputTransport(BaseOutputTransport):
363
385
  await super().cleanup()
364
386
  await self._transport.cleanup()
365
387
 
366
- async def send_message(self, frame: TransportMessageFrame | TransportMessageUrgentFrame):
388
+ async def send_message(
389
+ self, frame: OutputTransportMessageFrame | OutputTransportMessageUrgentFrame
390
+ ):
367
391
  """Send a transport message through the WebSocket.
368
392
 
369
393
  Args:
@@ -371,12 +395,18 @@ class WebsocketClientOutputTransport(BaseOutputTransport):
371
395
  """
372
396
  await self._write_frame(frame)
373
397
 
374
- async def write_audio_frame(self, frame: OutputAudioRawFrame):
398
+ async def write_audio_frame(self, frame: OutputAudioRawFrame) -> bool:
375
399
  """Write an audio frame to the WebSocket with optional WAV header.
376
400
 
377
401
  Args:
378
402
  frame: The output audio frame to write.
403
+
404
+ Returns:
405
+ True if the audio frame was written successfully, False otherwise.
379
406
  """
407
+ if self._session.is_closing or not self._session.is_connected:
408
+ return False
409
+
380
410
  frame = OutputAudioRawFrame(
381
411
  audio=frame.audio,
382
412
  sample_rate=self.sample_rate,
@@ -402,10 +432,16 @@ class WebsocketClientOutputTransport(BaseOutputTransport):
402
432
  # Simulate audio playback with a sleep.
403
433
  await self._write_audio_sleep()
404
434
 
435
+ return True
436
+
405
437
  async def _write_frame(self, frame: Frame):
406
438
  """Write a frame to the WebSocket after serialization."""
439
+ if self._session.is_closing or not self._session.is_connected:
440
+ return
441
+
407
442
  if not self._params.serializer:
408
443
  return
444
+
409
445
  payload = await self._params.serializer.serialize(frame)
410
446
  if payload:
411
447
  await self._session.send(payload)
@@ -26,11 +26,11 @@ from pipecat.frames.frames import (
26
26
  EndFrame,
27
27
  Frame,
28
28
  InputAudioRawFrame,
29
+ InterruptionFrame,
29
30
  OutputAudioRawFrame,
31
+ OutputTransportMessageFrame,
32
+ OutputTransportMessageUrgentFrame,
30
33
  StartFrame,
31
- StartInterruptionFrame,
32
- TransportMessageFrame,
33
- TransportMessageUrgentFrame,
34
34
  )
35
35
  from pipecat.processors.frame_processor import FrameDirection
36
36
  from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializerType
@@ -150,7 +150,6 @@ class FastAPIWebsocketClient:
150
150
  "Closing already disconnected websocket!", call_id=self._conversation_id
151
151
  )
152
152
  self._closing = True
153
- await self.trigger_client_disconnected()
154
153
 
155
154
  async def disconnect(self):
156
155
  """Disconnect the WebSocket client."""
@@ -164,8 +163,6 @@ class FastAPIWebsocketClient:
164
163
  await self._websocket.close()
165
164
  except Exception as e:
166
165
  logger.error(f"{self} exception while closing the websocket: {e}")
167
- finally:
168
- await self.trigger_client_disconnected()
169
166
 
170
167
  async def trigger_client_disconnected(self):
171
168
  """Trigger the client disconnected callback."""
@@ -310,7 +307,10 @@ class FastAPIWebsocketInputTransport(BaseInputTransport):
310
307
  except Exception as e:
311
308
  logger.error(f"{self} exception receiving data: {e.__class__.__name__} ({e})")
312
309
 
313
- await self._client.trigger_client_disconnected()
310
+ # Trigger `on_client_disconnected` if the client actually disconnects,
311
+ # that is, we are not the ones disconnecting.
312
+ if not self._client.is_closing:
313
+ await self._client.trigger_client_disconnected()
314
314
 
315
315
  async def _monitor_websocket(self):
316
316
  """Wait for self._params.session_timeout seconds, if the websocket is still open, trigger timeout event."""
@@ -410,11 +410,13 @@ class FastAPIWebsocketOutputTransport(BaseOutputTransport):
410
410
  """
411
411
  await super().process_frame(frame, direction)
412
412
 
413
- if isinstance(frame, StartInterruptionFrame):
413
+ if isinstance(frame, InterruptionFrame):
414
414
  await self._write_frame(frame)
415
415
  self._next_send_time = 0
416
416
 
417
- async def send_message(self, frame: TransportMessageFrame | TransportMessageUrgentFrame):
417
+ async def send_message(
418
+ self, frame: OutputTransportMessageFrame | OutputTransportMessageUrgentFrame
419
+ ):
418
420
  """Send a transport message frame.
419
421
 
420
422
  Args:
@@ -422,14 +424,17 @@ class FastAPIWebsocketOutputTransport(BaseOutputTransport):
422
424
  """
423
425
  await self._write_frame(frame)
424
426
 
425
- async def write_audio_frame(self, frame: OutputAudioRawFrame):
427
+ async def write_audio_frame(self, frame: OutputAudioRawFrame) -> bool:
426
428
  """Write an audio frame to the WebSocket with timing simulation.
427
429
 
428
430
  Args:
429
431
  frame: The output audio frame to write.
432
+
433
+ Returns:
434
+ True if the audio frame was written successfully, False otherwise.
430
435
  """
431
436
  if self._client.is_closing or not self._client.is_connected:
432
- return
437
+ return False
433
438
 
434
439
  frame = OutputAudioRawFrame(
435
440
  audio=frame.audio,
@@ -456,8 +461,13 @@ class FastAPIWebsocketOutputTransport(BaseOutputTransport):
456
461
  # Simulate audio playback with a sleep.
457
462
  await self._write_audio_sleep()
458
463
 
464
+ return True
465
+
459
466
  async def _write_frame(self, frame: Frame):
460
467
  """Serialize and send a frame through the WebSocket."""
468
+ if self._client.is_closing or not self._client.is_connected:
469
+ return
470
+
461
471
  if not self._params.serializer:
462
472
  return
463
473
 
@@ -25,11 +25,11 @@ from pipecat.frames.frames import (
25
25
  EndFrame,
26
26
  Frame,
27
27
  InputAudioRawFrame,
28
+ InterruptionFrame,
28
29
  OutputAudioRawFrame,
30
+ OutputTransportMessageFrame,
31
+ OutputTransportMessageUrgentFrame,
29
32
  StartFrame,
30
- StartInterruptionFrame,
31
- TransportMessageFrame,
32
- TransportMessageUrgentFrame,
33
33
  )
34
34
  from pipecat.processors.frame_processor import FrameDirection
35
35
  from pipecat.serializers.base_serializer import FrameSerializer
@@ -334,11 +334,13 @@ class WebsocketServerOutputTransport(BaseOutputTransport):
334
334
  """
335
335
  await super().process_frame(frame, direction)
336
336
 
337
- if isinstance(frame, StartInterruptionFrame):
337
+ if isinstance(frame, InterruptionFrame):
338
338
  await self._write_frame(frame)
339
339
  self._next_send_time = 0
340
340
 
341
- async def send_message(self, frame: TransportMessageFrame | TransportMessageUrgentFrame):
341
+ async def send_message(
342
+ self, frame: OutputTransportMessageFrame | OutputTransportMessageUrgentFrame
343
+ ):
342
344
  """Send a transport message frame to the client.
343
345
 
344
346
  Args:
@@ -346,14 +348,17 @@ class WebsocketServerOutputTransport(BaseOutputTransport):
346
348
  """
347
349
  await self._write_frame(frame)
348
350
 
349
- async def write_audio_frame(self, frame: OutputAudioRawFrame):
351
+ async def write_audio_frame(self, frame: OutputAudioRawFrame) -> bool:
350
352
  """Write an audio frame to the WebSocket client with timing control.
351
353
 
352
354
  Args:
353
355
  frame: The output audio frame to write.
356
+
357
+ Returns:
358
+ True if the audio frame was written successfully, False otherwise.
354
359
  """
355
360
  if not self._websocket:
356
- return
361
+ return False
357
362
 
358
363
  frame = OutputAudioRawFrame(
359
364
  audio=frame.audio,
@@ -380,6 +385,8 @@ class WebsocketServerOutputTransport(BaseOutputTransport):
380
385
  # Simulate audio playback with a sleep.
381
386
  await self._write_audio_sleep()
382
387
 
388
+ return True
389
+
383
390
  async def _write_frame(self, frame: Frame):
384
391
  """Serialize and send a frame to the WebSocket client."""
385
392
  if not self._params.serializer:
@@ -241,6 +241,14 @@ class WhatsAppApi:
241
241
  self._whatsapp_url = f"{self.BASE_URL}{phone_number_id}/calls"
242
242
  self._whatsapp_token = whatsapp_token
243
243
 
244
+ def update_whatsapp_token(self, whatsapp_token: str):
245
+ """Update the WhatsApp access token for authentication."""
246
+ self._whatsapp_token = whatsapp_token
247
+
248
+ def update_whatsapp_phone_number_id(self, phone_number_id: str):
249
+ """Update the WhatsApp phone number ID for authentication."""
250
+ self._phone_number_id = phone_number_id
251
+
244
252
  async def answer_call_to_whatsapp(self, call_id: str, action: str, sdp: str, from_: str):
245
253
  """Answer an incoming WhatsApp call.
246
254