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.
- {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/METADATA +98 -130
- {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/RECORD +192 -140
- pipecat/adapters/base_llm_adapter.py +38 -1
- pipecat/adapters/services/anthropic_adapter.py +9 -14
- pipecat/adapters/services/aws_nova_sonic_adapter.py +120 -5
- pipecat/adapters/services/bedrock_adapter.py +236 -13
- pipecat/adapters/services/gemini_adapter.py +12 -8
- pipecat/adapters/services/open_ai_adapter.py +19 -7
- pipecat/adapters/services/open_ai_realtime_adapter.py +5 -0
- pipecat/audio/dtmf/dtmf-0.wav +0 -0
- pipecat/audio/dtmf/dtmf-1.wav +0 -0
- pipecat/audio/dtmf/dtmf-2.wav +0 -0
- pipecat/audio/dtmf/dtmf-3.wav +0 -0
- pipecat/audio/dtmf/dtmf-4.wav +0 -0
- pipecat/audio/dtmf/dtmf-5.wav +0 -0
- pipecat/audio/dtmf/dtmf-6.wav +0 -0
- pipecat/audio/dtmf/dtmf-7.wav +0 -0
- pipecat/audio/dtmf/dtmf-8.wav +0 -0
- pipecat/audio/dtmf/dtmf-9.wav +0 -0
- pipecat/audio/dtmf/dtmf-pound.wav +0 -0
- pipecat/audio/dtmf/dtmf-star.wav +0 -0
- pipecat/audio/filters/krisp_viva_filter.py +193 -0
- pipecat/audio/filters/noisereduce_filter.py +15 -0
- pipecat/audio/turn/base_turn_analyzer.py +9 -1
- pipecat/audio/turn/smart_turn/base_smart_turn.py +14 -8
- pipecat/audio/turn/smart_turn/data/__init__.py +0 -0
- pipecat/audio/turn/smart_turn/data/smart-turn-v3.0.onnx +0 -0
- pipecat/audio/turn/smart_turn/http_smart_turn.py +6 -2
- pipecat/audio/turn/smart_turn/local_smart_turn.py +1 -1
- pipecat/audio/turn/smart_turn/local_smart_turn_v2.py +1 -1
- pipecat/audio/turn/smart_turn/local_smart_turn_v3.py +124 -0
- pipecat/audio/vad/data/README.md +10 -0
- pipecat/audio/vad/data/silero_vad_v2.onnx +0 -0
- pipecat/audio/vad/silero.py +9 -3
- pipecat/audio/vad/vad_analyzer.py +13 -1
- pipecat/extensions/voicemail/voicemail_detector.py +5 -5
- pipecat/frames/frames.py +277 -86
- pipecat/observers/loggers/debug_log_observer.py +3 -3
- pipecat/observers/loggers/llm_log_observer.py +7 -3
- pipecat/observers/loggers/user_bot_latency_log_observer.py +22 -10
- pipecat/pipeline/runner.py +18 -6
- pipecat/pipeline/service_switcher.py +64 -36
- pipecat/pipeline/task.py +125 -79
- pipecat/pipeline/tts_switcher.py +30 -0
- pipecat/processors/aggregators/dtmf_aggregator.py +2 -3
- pipecat/processors/aggregators/{gated_openai_llm_context.py → gated_llm_context.py} +9 -9
- pipecat/processors/aggregators/gated_open_ai_llm_context.py +12 -0
- pipecat/processors/aggregators/llm_context.py +40 -2
- pipecat/processors/aggregators/llm_response.py +32 -15
- pipecat/processors/aggregators/llm_response_universal.py +19 -15
- pipecat/processors/aggregators/user_response.py +6 -6
- pipecat/processors/aggregators/vision_image_frame.py +24 -2
- pipecat/processors/audio/audio_buffer_processor.py +43 -8
- pipecat/processors/dtmf_aggregator.py +174 -77
- pipecat/processors/filters/stt_mute_filter.py +17 -0
- pipecat/processors/frame_processor.py +110 -24
- pipecat/processors/frameworks/langchain.py +8 -2
- pipecat/processors/frameworks/rtvi.py +210 -68
- pipecat/processors/frameworks/strands_agents.py +170 -0
- pipecat/processors/logger.py +2 -2
- pipecat/processors/transcript_processor.py +26 -5
- pipecat/processors/user_idle_processor.py +35 -11
- pipecat/runner/daily.py +59 -20
- pipecat/runner/run.py +395 -93
- pipecat/runner/types.py +6 -4
- pipecat/runner/utils.py +51 -10
- pipecat/serializers/__init__.py +5 -1
- pipecat/serializers/asterisk.py +16 -2
- pipecat/serializers/convox.py +41 -4
- pipecat/serializers/custom.py +257 -0
- pipecat/serializers/exotel.py +5 -5
- pipecat/serializers/livekit.py +20 -0
- pipecat/serializers/plivo.py +5 -5
- pipecat/serializers/protobuf.py +6 -5
- pipecat/serializers/telnyx.py +2 -2
- pipecat/serializers/twilio.py +43 -23
- pipecat/serializers/vi.py +324 -0
- pipecat/services/ai_service.py +2 -6
- pipecat/services/anthropic/llm.py +2 -25
- pipecat/services/assemblyai/models.py +6 -0
- pipecat/services/assemblyai/stt.py +13 -5
- pipecat/services/asyncai/tts.py +5 -3
- pipecat/services/aws/__init__.py +1 -0
- pipecat/services/aws/llm.py +147 -105
- pipecat/services/aws/nova_sonic/__init__.py +0 -0
- pipecat/services/aws/nova_sonic/context.py +436 -0
- pipecat/services/aws/nova_sonic/frames.py +25 -0
- pipecat/services/aws/nova_sonic/llm.py +1265 -0
- pipecat/services/aws/stt.py +3 -3
- pipecat/services/aws_nova_sonic/__init__.py +19 -1
- pipecat/services/aws_nova_sonic/aws.py +11 -1151
- pipecat/services/aws_nova_sonic/context.py +8 -354
- pipecat/services/aws_nova_sonic/frames.py +13 -17
- pipecat/services/azure/llm.py +51 -1
- pipecat/services/azure/realtime/__init__.py +0 -0
- pipecat/services/azure/realtime/llm.py +65 -0
- pipecat/services/azure/stt.py +15 -0
- pipecat/services/cartesia/stt.py +77 -70
- pipecat/services/cartesia/tts.py +80 -13
- pipecat/services/deepgram/__init__.py +1 -0
- pipecat/services/deepgram/flux/__init__.py +0 -0
- pipecat/services/deepgram/flux/stt.py +640 -0
- pipecat/services/elevenlabs/__init__.py +4 -1
- pipecat/services/elevenlabs/stt.py +339 -0
- pipecat/services/elevenlabs/tts.py +87 -46
- pipecat/services/fish/tts.py +5 -2
- pipecat/services/gemini_multimodal_live/events.py +38 -524
- pipecat/services/gemini_multimodal_live/file_api.py +23 -173
- pipecat/services/gemini_multimodal_live/gemini.py +41 -1403
- pipecat/services/gladia/stt.py +56 -72
- pipecat/services/google/__init__.py +1 -0
- pipecat/services/google/gemini_live/__init__.py +3 -0
- pipecat/services/google/gemini_live/file_api.py +189 -0
- pipecat/services/google/gemini_live/llm.py +1582 -0
- pipecat/services/google/gemini_live/llm_vertex.py +184 -0
- pipecat/services/google/llm.py +15 -11
- pipecat/services/google/llm_openai.py +3 -3
- pipecat/services/google/llm_vertex.py +86 -16
- pipecat/services/google/stt.py +4 -0
- pipecat/services/google/tts.py +7 -3
- pipecat/services/heygen/api.py +2 -0
- pipecat/services/heygen/client.py +8 -4
- pipecat/services/heygen/video.py +2 -0
- pipecat/services/hume/__init__.py +5 -0
- pipecat/services/hume/tts.py +220 -0
- pipecat/services/inworld/tts.py +6 -6
- pipecat/services/llm_service.py +15 -5
- pipecat/services/lmnt/tts.py +4 -2
- pipecat/services/mcp_service.py +4 -2
- pipecat/services/mem0/memory.py +6 -5
- pipecat/services/mistral/llm.py +29 -8
- pipecat/services/moondream/vision.py +42 -16
- pipecat/services/neuphonic/tts.py +5 -2
- pipecat/services/openai/__init__.py +1 -0
- pipecat/services/openai/base_llm.py +27 -20
- pipecat/services/openai/realtime/__init__.py +0 -0
- pipecat/services/openai/realtime/context.py +272 -0
- pipecat/services/openai/realtime/events.py +1106 -0
- pipecat/services/openai/realtime/frames.py +37 -0
- pipecat/services/openai/realtime/llm.py +829 -0
- pipecat/services/openai/tts.py +49 -10
- pipecat/services/openai_realtime/__init__.py +27 -0
- pipecat/services/openai_realtime/azure.py +21 -0
- pipecat/services/openai_realtime/context.py +21 -0
- pipecat/services/openai_realtime/events.py +21 -0
- pipecat/services/openai_realtime/frames.py +21 -0
- pipecat/services/openai_realtime_beta/azure.py +16 -0
- pipecat/services/openai_realtime_beta/openai.py +17 -5
- pipecat/services/piper/tts.py +7 -9
- pipecat/services/playht/tts.py +34 -4
- pipecat/services/rime/tts.py +12 -12
- pipecat/services/riva/stt.py +3 -1
- pipecat/services/salesforce/__init__.py +9 -0
- pipecat/services/salesforce/llm.py +700 -0
- pipecat/services/sarvam/__init__.py +7 -0
- pipecat/services/sarvam/stt.py +540 -0
- pipecat/services/sarvam/tts.py +97 -13
- pipecat/services/simli/video.py +2 -2
- pipecat/services/speechmatics/stt.py +22 -10
- pipecat/services/stt_service.py +47 -0
- pipecat/services/tavus/video.py +2 -2
- pipecat/services/tts_service.py +75 -22
- pipecat/services/vision_service.py +7 -6
- pipecat/services/vistaar/llm.py +51 -9
- pipecat/tests/utils.py +4 -4
- pipecat/transcriptions/language.py +41 -1
- pipecat/transports/base_input.py +13 -34
- pipecat/transports/base_output.py +140 -104
- pipecat/transports/daily/transport.py +199 -26
- pipecat/transports/heygen/__init__.py +0 -0
- pipecat/transports/heygen/transport.py +381 -0
- pipecat/transports/livekit/transport.py +228 -63
- pipecat/transports/local/audio.py +6 -1
- pipecat/transports/local/tk.py +11 -2
- pipecat/transports/network/fastapi_websocket.py +1 -1
- pipecat/transports/smallwebrtc/connection.py +103 -19
- pipecat/transports/smallwebrtc/request_handler.py +246 -0
- pipecat/transports/smallwebrtc/transport.py +65 -23
- pipecat/transports/tavus/transport.py +23 -12
- pipecat/transports/websocket/client.py +41 -5
- pipecat/transports/websocket/fastapi.py +21 -11
- pipecat/transports/websocket/server.py +14 -7
- pipecat/transports/whatsapp/api.py +8 -0
- pipecat/transports/whatsapp/client.py +47 -0
- pipecat/utils/base_object.py +54 -22
- pipecat/utils/redis.py +58 -0
- pipecat/utils/string.py +13 -1
- pipecat/utils/tracing/service_decorators.py +21 -21
- pipecat/serializers/genesys.py +0 -95
- pipecat/services/google/test-google-chirp.py +0 -45
- pipecat/services/openai.py +0 -698
- {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/WHEEL +0 -0
- {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/licenses/LICENSE +0 -0
- {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/top_level.txt +0 -0
- /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
|
-
|
|
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
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
|