dv-pipecat-ai 0.0.85.dev7__py3-none-any.whl → 0.0.85.dev699__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.85.dev7.dist-info → dv_pipecat_ai-0.0.85.dev699.dist-info}/METADATA +78 -117
- {dv_pipecat_ai-0.0.85.dev7.dist-info → dv_pipecat_ai-0.0.85.dev699.dist-info}/RECORD +158 -122
- pipecat/adapters/base_llm_adapter.py +38 -1
- pipecat/adapters/services/anthropic_adapter.py +9 -14
- pipecat/adapters/services/aws_nova_sonic_adapter.py +5 -0
- 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/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/vad_analyzer.py +13 -1
- pipecat/extensions/voicemail/voicemail_detector.py +5 -5
- pipecat/frames/frames.py +120 -87
- 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 +12 -4
- pipecat/pipeline/service_switcher.py +64 -36
- pipecat/pipeline/task.py +85 -24
- pipecat/processors/aggregators/dtmf_aggregator.py +28 -22
- 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_response.py +6 -7
- 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/filters/stt_mute_filter.py +2 -0
- pipecat/processors/frame_processor.py +103 -17
- pipecat/processors/frameworks/langchain.py +8 -2
- pipecat/processors/frameworks/rtvi.py +209 -68
- pipecat/processors/frameworks/strands_agents.py +170 -0
- pipecat/processors/logger.py +2 -2
- pipecat/processors/transcript_processor.py +4 -4
- pipecat/processors/user_idle_processor.py +3 -6
- pipecat/runner/run.py +270 -50
- pipecat/runner/types.py +2 -0
- pipecat/runner/utils.py +51 -10
- pipecat/serializers/exotel.py +5 -5
- pipecat/serializers/livekit.py +20 -0
- pipecat/serializers/plivo.py +6 -9
- pipecat/serializers/protobuf.py +6 -5
- pipecat/serializers/telnyx.py +2 -2
- pipecat/serializers/twilio.py +43 -23
- pipecat/services/ai_service.py +2 -6
- pipecat/services/anthropic/llm.py +2 -25
- pipecat/services/asyncai/tts.py +2 -3
- pipecat/services/aws/__init__.py +1 -0
- pipecat/services/aws/llm.py +122 -97
- pipecat/services/aws/nova_sonic/__init__.py +0 -0
- pipecat/services/aws/nova_sonic/context.py +367 -0
- pipecat/services/aws/nova_sonic/frames.py +25 -0
- pipecat/services/aws/nova_sonic/llm.py +1155 -0
- pipecat/services/aws/stt.py +1 -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 +13 -355
- pipecat/services/aws_nova_sonic/frames.py +13 -17
- 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/tts.py +2 -2
- pipecat/services/deepgram/__init__.py +1 -0
- pipecat/services/deepgram/flux/__init__.py +0 -0
- pipecat/services/deepgram/flux/stt.py +636 -0
- pipecat/services/elevenlabs/__init__.py +2 -1
- pipecat/services/elevenlabs/stt.py +254 -276
- pipecat/services/elevenlabs/tts.py +5 -5
- pipecat/services/fish/tts.py +2 -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/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 +2 -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 +2 -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 +16 -8
- 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/playht/tts.py +31 -4
- pipecat/services/rime/tts.py +3 -4
- pipecat/services/salesforce/__init__.py +9 -0
- pipecat/services/salesforce/llm.py +465 -0
- pipecat/services/sarvam/tts.py +2 -6
- pipecat/services/simli/video.py +2 -2
- pipecat/services/speechmatics/stt.py +1 -7
- pipecat/services/stt_service.py +34 -0
- pipecat/services/tavus/video.py +2 -2
- pipecat/services/tts_service.py +9 -9
- pipecat/services/vision_service.py +7 -6
- pipecat/tests/utils.py +4 -4
- pipecat/transcriptions/language.py +41 -1
- pipecat/transports/base_input.py +17 -42
- pipecat/transports/base_output.py +42 -26
- 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 +98 -19
- pipecat/transports/smallwebrtc/request_handler.py +204 -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/string.py +12 -1
- pipecat/utils/tracing/service_decorators.py +21 -21
- {dv_pipecat_ai-0.0.85.dev7.dist-info → dv_pipecat_ai-0.0.85.dev699.dist-info}/WHEEL +0 -0
- {dv_pipecat_ai-0.0.85.dev7.dist-info → dv_pipecat_ai-0.0.85.dev699.dist-info}/licenses/LICENSE +0 -0
- {dv_pipecat_ai-0.0.85.dev7.dist-info → dv_pipecat_ai-0.0.85.dev699.dist-info}/top_level.txt +0 -0
- /pipecat/services/{aws_nova_sonic → aws/nova_sonic}/ready.wav +0 -0
|
@@ -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
|
|
|
@@ -12,6 +12,8 @@ WhatsApp call events.
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import asyncio
|
|
15
|
+
import hashlib
|
|
16
|
+
import hmac
|
|
15
17
|
from typing import Awaitable, Callable, Dict, List, Optional
|
|
16
18
|
|
|
17
19
|
import aiohttp
|
|
@@ -47,6 +49,7 @@ class WhatsAppClient:
|
|
|
47
49
|
phone_number_id: str,
|
|
48
50
|
session: aiohttp.ClientSession,
|
|
49
51
|
ice_servers: Optional[List[IceServer]] = None,
|
|
52
|
+
whatsapp_secret: Optional[str] = None,
|
|
50
53
|
) -> None:
|
|
51
54
|
"""Initialize the WhatsApp client.
|
|
52
55
|
|
|
@@ -56,10 +59,12 @@ class WhatsAppClient:
|
|
|
56
59
|
session: aiohttp session for making HTTP requests
|
|
57
60
|
ice_servers: List of ICE servers for WebRTC connections. If None,
|
|
58
61
|
defaults to Google's public STUN server
|
|
62
|
+
whatsapp_secret: WhatsApp APP secret for validating that the webhook request came from WhatsApp.
|
|
59
63
|
"""
|
|
60
64
|
self._whatsapp_api = WhatsAppApi(
|
|
61
65
|
whatsapp_token=whatsapp_token, phone_number_id=phone_number_id, session=session
|
|
62
66
|
)
|
|
67
|
+
self._whatsapp_secret = whatsapp_secret
|
|
63
68
|
self._ongoing_calls_map: Dict[str, SmallWebRTCConnection] = {}
|
|
64
69
|
|
|
65
70
|
# Set default ICE servers if none provided
|
|
@@ -68,6 +73,22 @@ class WhatsAppClient:
|
|
|
68
73
|
else:
|
|
69
74
|
self._ice_servers = ice_servers
|
|
70
75
|
|
|
76
|
+
def update_ice_servers(self, ice_servers: Optional[List[IceServer]] = None):
|
|
77
|
+
"""Update the list of ICE servers used for WebRTC connections."""
|
|
78
|
+
self._ice_servers = ice_servers
|
|
79
|
+
|
|
80
|
+
def update_whatsapp_secret(self, whatsapp_secret: Optional[str] = None):
|
|
81
|
+
"""Update the WhatsApp APP secret for validating that the webhook request came from WhatsApp."""
|
|
82
|
+
self._whatsapp_secret = whatsapp_secret
|
|
83
|
+
|
|
84
|
+
def update_whatsapp_token(self, whatsapp_token: str):
|
|
85
|
+
"""Update the WhatsApp API access token."""
|
|
86
|
+
self._whatsapp_api.update_whatsapp_token(whatsapp_token)
|
|
87
|
+
|
|
88
|
+
def update_whatsapp_phone_number_id(self, phone_number_id: str):
|
|
89
|
+
"""Update the WhatsApp phone number ID for authentication."""
|
|
90
|
+
self._whatsapp_api.update_whatsapp_phone_number_id(phone_number_id)
|
|
91
|
+
|
|
71
92
|
async def terminate_all_calls(self) -> None:
|
|
72
93
|
"""Terminate all ongoing WhatsApp calls.
|
|
73
94
|
|
|
@@ -133,10 +154,32 @@ class WhatsAppClient:
|
|
|
133
154
|
|
|
134
155
|
return int(challenge)
|
|
135
156
|
|
|
157
|
+
async def _validate_whatsapp_webhook_request(self, raw_body: bytes, sha256_signature: str):
|
|
158
|
+
"""Common handler for both /start and /connect endpoints."""
|
|
159
|
+
# Compute HMAC SHA256 using your App Secret
|
|
160
|
+
expected_signature = hmac.new(
|
|
161
|
+
key=self._whatsapp_secret.encode("utf-8"),
|
|
162
|
+
msg=raw_body,
|
|
163
|
+
digestmod=hashlib.sha256,
|
|
164
|
+
).hexdigest()
|
|
165
|
+
|
|
166
|
+
# Extract signature from header (strip 'sha256=' prefix)
|
|
167
|
+
if not sha256_signature:
|
|
168
|
+
raise Exception("Missing X-Hub-Signature-256 header")
|
|
169
|
+
received_signature = sha256_signature.split("sha256=")[-1]
|
|
170
|
+
|
|
171
|
+
# Compare signatures securely
|
|
172
|
+
if not hmac.compare_digest(expected_signature, received_signature):
|
|
173
|
+
raise Exception("Invalid webhook signature")
|
|
174
|
+
|
|
175
|
+
logger.debug(f"Webhook signature verified!")
|
|
176
|
+
|
|
136
177
|
async def handle_webhook_request(
|
|
137
178
|
self,
|
|
138
179
|
request: WhatsAppWebhookRequest,
|
|
139
180
|
connection_callback: Optional[Callable[[SmallWebRTCConnection], Awaitable[None]]] = None,
|
|
181
|
+
raw_body: Optional[bytes] = None,
|
|
182
|
+
sha256_signature: Optional[str] = None,
|
|
140
183
|
) -> bool:
|
|
141
184
|
"""Handle a webhook request from WhatsApp.
|
|
142
185
|
|
|
@@ -150,6 +193,8 @@ class WhatsAppClient:
|
|
|
150
193
|
connection_callback: Optional callback function to invoke when a new
|
|
151
194
|
WebRTC connection is established. The callback
|
|
152
195
|
receives the SmallWebRTCConnection instance.
|
|
196
|
+
raw_body: Optional bytes containing the raw request body.
|
|
197
|
+
sha256_signature: Optional X-Hub-Signature-256 header value from the request.
|
|
153
198
|
|
|
154
199
|
Returns:
|
|
155
200
|
bool: True if the webhook request was handled successfully, False otherwise
|
|
@@ -159,6 +204,8 @@ class WhatsAppClient:
|
|
|
159
204
|
Exception: If connection establishment or API calls fail
|
|
160
205
|
"""
|
|
161
206
|
try:
|
|
207
|
+
if self._whatsapp_secret:
|
|
208
|
+
await self._validate_whatsapp_webhook_request(raw_body, sha256_signature)
|
|
162
209
|
for entry in request.entry:
|
|
163
210
|
for change in entry.changes:
|
|
164
211
|
# Handle connect events
|
pipecat/utils/base_object.py
CHANGED
|
@@ -14,13 +14,33 @@ and async cleanup for all Pipecat components.
|
|
|
14
14
|
import asyncio
|
|
15
15
|
import inspect
|
|
16
16
|
from abc import ABC
|
|
17
|
-
from
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
18
19
|
|
|
19
20
|
from loguru import logger
|
|
20
21
|
|
|
21
22
|
from pipecat.utils.utils import obj_count, obj_id
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
@dataclass
|
|
26
|
+
class EventHandler:
|
|
27
|
+
"""Data class to store event handlers information.
|
|
28
|
+
|
|
29
|
+
This data class stores the event name, a list of handlers to run for this
|
|
30
|
+
event, and whether these handlers will be executed in a task.
|
|
31
|
+
|
|
32
|
+
Parameters:
|
|
33
|
+
name (str): The name of the event handler.
|
|
34
|
+
handlers (List[Any]): A list of functions to be called when this event is triggered.
|
|
35
|
+
is_sync (bool): Indicates whether the functions are executed in a task.
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
name: str
|
|
40
|
+
handlers: List[Any]
|
|
41
|
+
is_sync: bool
|
|
42
|
+
|
|
43
|
+
|
|
24
44
|
class BaseObject(ABC):
|
|
25
45
|
"""Abstract base class providing common functionality for Pipecat objects.
|
|
26
46
|
|
|
@@ -41,7 +61,7 @@ class BaseObject(ABC):
|
|
|
41
61
|
self._name = name or f"{self.__class__.__name__}#{obj_count(self)}"
|
|
42
62
|
|
|
43
63
|
# Registered event handlers.
|
|
44
|
-
self._event_handlers:
|
|
64
|
+
self._event_handlers: Dict[str, EventHandler] = {}
|
|
45
65
|
|
|
46
66
|
# Set of tasks being executed. When a task finishes running it gets
|
|
47
67
|
# automatically removed from the set. When we cleanup we wait for all
|
|
@@ -103,20 +123,23 @@ class BaseObject(ABC):
|
|
|
103
123
|
Can be sync or async.
|
|
104
124
|
"""
|
|
105
125
|
if event_name in self._event_handlers:
|
|
106
|
-
self._event_handlers[event_name].append(handler)
|
|
126
|
+
self._event_handlers[event_name].handlers.append(handler)
|
|
107
127
|
else:
|
|
108
128
|
logger.warning(f"Event handler {event_name} not registered")
|
|
109
129
|
|
|
110
|
-
def _register_event_handler(self, event_name: str):
|
|
130
|
+
def _register_event_handler(self, event_name: str, sync: bool = False):
|
|
111
131
|
"""Register an event handler type.
|
|
112
132
|
|
|
113
133
|
Args:
|
|
114
134
|
event_name: The name of the event type to register.
|
|
135
|
+
sync: Whether this event handler will be executed in a task.
|
|
115
136
|
"""
|
|
116
137
|
if event_name not in self._event_handlers:
|
|
117
|
-
self._event_handlers[event_name] =
|
|
138
|
+
self._event_handlers[event_name] = EventHandler(
|
|
139
|
+
name=event_name, handlers=[], is_sync=sync
|
|
140
|
+
)
|
|
118
141
|
else:
|
|
119
|
-
logger.warning(f"Event handler {event_name}
|
|
142
|
+
logger.warning(f"Event handler {event_name} already registered")
|
|
120
143
|
|
|
121
144
|
async def _call_event_handler(self, event_name: str, *args, **kwargs):
|
|
122
145
|
"""Call all registered handlers for the specified event.
|
|
@@ -126,34 +149,43 @@ class BaseObject(ABC):
|
|
|
126
149
|
*args: Positional arguments to pass to event handlers.
|
|
127
150
|
**kwargs: Keyword arguments to pass to event handlers.
|
|
128
151
|
"""
|
|
129
|
-
|
|
130
|
-
# anything.
|
|
131
|
-
if not self._event_handlers.get(event_name):
|
|
152
|
+
if event_name not in self._event_handlers:
|
|
132
153
|
return
|
|
133
154
|
|
|
134
|
-
|
|
135
|
-
|
|
155
|
+
event_handler = self._event_handlers[event_name]
|
|
156
|
+
|
|
157
|
+
for handler in event_handler.handlers:
|
|
158
|
+
if event_handler.is_sync:
|
|
159
|
+
# Just run the handler.
|
|
160
|
+
await self._run_handler(event_handler.name, handler, *args, **kwargs)
|
|
161
|
+
else:
|
|
162
|
+
# Create the task. Note that this is a task per each function
|
|
163
|
+
# handler. Users can register to an event handler multiple
|
|
164
|
+
# times.
|
|
165
|
+
task = asyncio.create_task(
|
|
166
|
+
self._run_handler(event_handler.name, handler, *args, **kwargs)
|
|
167
|
+
)
|
|
136
168
|
|
|
137
|
-
|
|
138
|
-
|
|
169
|
+
# Add it to our list of event tasks.
|
|
170
|
+
self._event_tasks.add((event_name, task))
|
|
139
171
|
|
|
140
|
-
|
|
141
|
-
|
|
172
|
+
# Remove the task from the event tasks list when the task completes.
|
|
173
|
+
task.add_done_callback(self._event_task_finished)
|
|
142
174
|
|
|
143
|
-
async def
|
|
175
|
+
async def _run_handler(self, event_name: str, handler, *args, **kwargs):
|
|
144
176
|
"""Execute all handlers for an event.
|
|
145
177
|
|
|
146
178
|
Args:
|
|
147
|
-
event_name: The name
|
|
179
|
+
event_name: The event name for this handler.
|
|
180
|
+
handler: The handler function to run.
|
|
148
181
|
*args: Positional arguments to pass to handlers.
|
|
149
182
|
**kwargs: Keyword arguments to pass to handlers.
|
|
150
183
|
"""
|
|
151
184
|
try:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
handler(self, *args, **kwargs)
|
|
185
|
+
if inspect.iscoroutinefunction(handler):
|
|
186
|
+
await handler(self, *args, **kwargs)
|
|
187
|
+
else:
|
|
188
|
+
handler(self, *args, **kwargs)
|
|
157
189
|
except Exception as e:
|
|
158
190
|
logger.exception(f"Exception in event handler {event_name}: {e}")
|
|
159
191
|
|
pipecat/utils/string.py
CHANGED
|
@@ -21,13 +21,24 @@ import re
|
|
|
21
21
|
from typing import FrozenSet, Optional, Sequence, Tuple
|
|
22
22
|
|
|
23
23
|
import nltk
|
|
24
|
+
from loguru import logger
|
|
24
25
|
from nltk.tokenize import sent_tokenize
|
|
25
26
|
|
|
26
27
|
# Ensure punkt_tab tokenizer data is available
|
|
27
28
|
try:
|
|
28
29
|
nltk.data.find("tokenizers/punkt_tab")
|
|
29
30
|
except LookupError:
|
|
30
|
-
|
|
31
|
+
try:
|
|
32
|
+
nltk.download("punkt_tab", quiet=True)
|
|
33
|
+
except (OSError, PermissionError) as e:
|
|
34
|
+
logger.error(
|
|
35
|
+
f"Failed to download NLTK 'punkt_tab' tokenizer data: {e}. "
|
|
36
|
+
"This data is required for sentence tokenization features. "
|
|
37
|
+
"The download failed due to filesystem permissions. "
|
|
38
|
+
"To resolve: pre-install the data in a location with appropriate read permissions, "
|
|
39
|
+
"or set the NLTK_DATA environment variable to point to a writable directory. "
|
|
40
|
+
"See https://www.nltk.org/data.html for more information."
|
|
41
|
+
)
|
|
31
42
|
|
|
32
43
|
SENTENCE_ENDING_PUNCTUATION: FrozenSet[str] = frozenset(
|
|
33
44
|
{
|