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
|
@@ -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/redis.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Async Redis helper utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import redis.asyncio as redis
|
|
11
|
+
except ImportError: # pragma: no cover - Redis is optional
|
|
12
|
+
redis = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING: # pragma: no cover - typing aid
|
|
16
|
+
from redis.asyncio import Redis
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_async_redis_client(
|
|
20
|
+
url: Optional[str],
|
|
21
|
+
*,
|
|
22
|
+
decode_responses: bool = True,
|
|
23
|
+
encoding: str = "utf-8",
|
|
24
|
+
logger: Optional[Any] = None,
|
|
25
|
+
**kwargs,
|
|
26
|
+
) -> Optional["Redis"]:
|
|
27
|
+
"""Return a configured async Redis client or None if unavailable.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
url: Redis connection URL.
|
|
31
|
+
decode_responses: Whether to decode responses to str.
|
|
32
|
+
encoding: Character encoding to use with decoded responses.
|
|
33
|
+
logger: Optional logger supporting .warning() for diagnostics.
|
|
34
|
+
**kwargs: Additional keyword arguments forwarded to Redis.from_url.
|
|
35
|
+
"""
|
|
36
|
+
if redis is None:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
if not url or url in {"redis_url", "REDIS_URL"}:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
parsed = urlparse(url)
|
|
43
|
+
connection_kwargs = {
|
|
44
|
+
"decode_responses": decode_responses,
|
|
45
|
+
"encoding": encoding,
|
|
46
|
+
}
|
|
47
|
+
connection_kwargs.update(kwargs)
|
|
48
|
+
|
|
49
|
+
if parsed.scheme == "rediss":
|
|
50
|
+
connection_kwargs.setdefault("ssl_cert_reqs", "none")
|
|
51
|
+
connection_kwargs.setdefault("ssl_check_hostname", False)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
return redis.Redis.from_url(url, **connection_kwargs)
|
|
55
|
+
except Exception as exc: # pragma: no cover - best effort logging
|
|
56
|
+
if logger is not None:
|
|
57
|
+
logger.warning(f"Failed to create Redis client: {exc}")
|
|
58
|
+
return None
|
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
|
{
|
|
@@ -36,6 +47,7 @@ SENTENCE_ENDING_PUNCTUATION: FrozenSet[str] = frozenset(
|
|
|
36
47
|
"!",
|
|
37
48
|
"?",
|
|
38
49
|
";",
|
|
50
|
+
"…",
|
|
39
51
|
# East Asian punctuation (Chinese (Traditional & Simplified), Japanese, Korean)
|
|
40
52
|
"。", # Ideographic full stop
|
|
41
53
|
"?", # Full-width question mark
|
|
@@ -651,9 +651,9 @@ def traced_gemini_live(operation: str) -> Callable:
|
|
|
651
651
|
|
|
652
652
|
elif operation == "llm_tool_call" and args:
|
|
653
653
|
# Extract tool call information
|
|
654
|
-
|
|
655
|
-
if
|
|
656
|
-
function_calls =
|
|
654
|
+
msg = args[0] if args else None
|
|
655
|
+
if msg and hasattr(msg, "tool_call") and msg.tool_call.function_calls:
|
|
656
|
+
function_calls = msg.tool_call.function_calls
|
|
657
657
|
if function_calls:
|
|
658
658
|
# Add information about the first function call
|
|
659
659
|
call = function_calls[0]
|
|
@@ -722,19 +722,19 @@ def traced_gemini_live(operation: str) -> Callable:
|
|
|
722
722
|
|
|
723
723
|
elif operation == "llm_response" and args:
|
|
724
724
|
# Extract usage and response metadata from turn complete event
|
|
725
|
-
|
|
726
|
-
if
|
|
727
|
-
usage =
|
|
725
|
+
msg = args[0] if args else None
|
|
726
|
+
if msg and hasattr(msg, "usage_metadata") and msg.usage_metadata:
|
|
727
|
+
usage = msg.usage_metadata
|
|
728
728
|
|
|
729
729
|
# Token usage - basic attributes for span visibility
|
|
730
|
-
if hasattr(usage, "
|
|
731
|
-
operation_attrs["tokens.prompt"] = usage.
|
|
732
|
-
if hasattr(usage, "
|
|
730
|
+
if hasattr(usage, "prompt_token_count"):
|
|
731
|
+
operation_attrs["tokens.prompt"] = usage.prompt_token_count or 0
|
|
732
|
+
if hasattr(usage, "response_token_count"):
|
|
733
733
|
operation_attrs["tokens.completion"] = (
|
|
734
|
-
usage.
|
|
734
|
+
usage.response_token_count or 0
|
|
735
735
|
)
|
|
736
|
-
if hasattr(usage, "
|
|
737
|
-
operation_attrs["tokens.total"] = usage.
|
|
736
|
+
if hasattr(usage, "total_token_count"):
|
|
737
|
+
operation_attrs["tokens.total"] = usage.total_token_count or 0
|
|
738
738
|
|
|
739
739
|
# Get output text and modality from service state
|
|
740
740
|
text = getattr(self, "_bot_text_buffer", "")
|
|
@@ -751,9 +751,9 @@ def traced_gemini_live(operation: str) -> Callable:
|
|
|
751
751
|
|
|
752
752
|
# Add turn completion status
|
|
753
753
|
if (
|
|
754
|
-
|
|
755
|
-
and hasattr(
|
|
756
|
-
and
|
|
754
|
+
msg
|
|
755
|
+
and hasattr(msg, "server_content")
|
|
756
|
+
and msg.server_content.turn_complete
|
|
757
757
|
):
|
|
758
758
|
operation_attrs["turn_complete"] = True
|
|
759
759
|
|
|
@@ -772,16 +772,16 @@ def traced_gemini_live(operation: str) -> Callable:
|
|
|
772
772
|
|
|
773
773
|
# For llm_response operation, also handle token usage metrics
|
|
774
774
|
if operation == "llm_response" and hasattr(self, "start_llm_usage_metrics"):
|
|
775
|
-
|
|
776
|
-
if
|
|
777
|
-
usage =
|
|
775
|
+
msg = args[0] if args else None
|
|
776
|
+
if msg and hasattr(msg, "usage_metadata") and msg.usage_metadata:
|
|
777
|
+
usage = msg.usage_metadata
|
|
778
778
|
# Create LLMTokenUsage object
|
|
779
779
|
from pipecat.metrics.metrics import LLMTokenUsage
|
|
780
780
|
|
|
781
781
|
tokens = LLMTokenUsage(
|
|
782
|
-
prompt_tokens=usage.
|
|
783
|
-
completion_tokens=usage.
|
|
784
|
-
total_tokens=usage.
|
|
782
|
+
prompt_tokens=usage.prompt_token_count or 0,
|
|
783
|
+
completion_tokens=usage.response_token_count or 0,
|
|
784
|
+
total_tokens=usage.total_token_count or 0,
|
|
785
785
|
)
|
|
786
786
|
_add_token_usage_to_span(current_span, tokens)
|
|
787
787
|
|
pipecat/serializers/genesys.py
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import json
|
|
3
|
-
from typing import Optional
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel
|
|
6
|
-
|
|
7
|
-
from pipecat.audio.utils import create_default_resampler, pcm_to_ulaw, ulaw_to_pcm
|
|
8
|
-
from pipecat.frames.frames import (
|
|
9
|
-
AudioRawFrame,
|
|
10
|
-
Frame,
|
|
11
|
-
InputAudioRawFrame,
|
|
12
|
-
InputDTMFFrame,
|
|
13
|
-
KeypadEntry,
|
|
14
|
-
StartFrame,
|
|
15
|
-
StartInterruptionFrame,
|
|
16
|
-
TransportMessageFrame,
|
|
17
|
-
TransportMessageUrgentFrame,
|
|
18
|
-
)
|
|
19
|
-
from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializerType
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class GenesysFrameSerializer(FrameSerializer):
|
|
23
|
-
class InputParams(BaseModel):
|
|
24
|
-
genesys_sample_rate: int = 8000 # Default Genesys rate (8kHz)
|
|
25
|
-
sample_rate: Optional[int] = None # Pipeline input rate
|
|
26
|
-
|
|
27
|
-
def __init__(self, session_id: str, params: InputParams = InputParams()):
|
|
28
|
-
self._session_id = session_id
|
|
29
|
-
self._params = params
|
|
30
|
-
self._genesys_sample_rate = self._params.genesys_sample_rate
|
|
31
|
-
self._sample_rate = 0 # Pipeline input rate
|
|
32
|
-
self._resampler = create_default_resampler()
|
|
33
|
-
self._seq = 1 # Sequence number for outgoing messages
|
|
34
|
-
|
|
35
|
-
@property
|
|
36
|
-
def type(self) -> FrameSerializerType:
|
|
37
|
-
return FrameSerializerType.TEXT
|
|
38
|
-
|
|
39
|
-
async def setup(self, frame: StartFrame):
|
|
40
|
-
self._sample_rate = self._params.sample_rate or frame.audio_in_sample_rate
|
|
41
|
-
|
|
42
|
-
async def serialize(self, frame: Frame) -> str | bytes | None:
|
|
43
|
-
if isinstance(frame, StartInterruptionFrame):
|
|
44
|
-
answer = {
|
|
45
|
-
"version": "2",
|
|
46
|
-
"type": "clearAudio", # Or appropriate event for interruption
|
|
47
|
-
"seq": self._seq,
|
|
48
|
-
"id": self._session_id,
|
|
49
|
-
}
|
|
50
|
-
self._seq += 1
|
|
51
|
-
return json.dumps(answer)
|
|
52
|
-
elif isinstance(frame, AudioRawFrame):
|
|
53
|
-
data = frame.audio
|
|
54
|
-
# Convert PCM to 8kHz μ-law for Genesys
|
|
55
|
-
serialized_data = await pcm_to_ulaw(
|
|
56
|
-
data, frame.sample_rate, self._genesys_sample_rate, self._resampler
|
|
57
|
-
)
|
|
58
|
-
payload = base64.b64encode(serialized_data).decode("utf-8")
|
|
59
|
-
answer = {
|
|
60
|
-
"version": "2",
|
|
61
|
-
"type": "audio",
|
|
62
|
-
"seq": self._seq,
|
|
63
|
-
"id": self._session_id,
|
|
64
|
-
"media": {
|
|
65
|
-
"payload": payload,
|
|
66
|
-
"format": "PCMU",
|
|
67
|
-
"rate": self._genesys_sample_rate,
|
|
68
|
-
},
|
|
69
|
-
}
|
|
70
|
-
self._seq += 1
|
|
71
|
-
return json.dumps(answer)
|
|
72
|
-
elif isinstance(frame, (TransportMessageFrame, TransportMessageUrgentFrame)):
|
|
73
|
-
return json.dumps(frame.message)
|
|
74
|
-
|
|
75
|
-
async def deserialize(self, data: str | bytes) -> Frame | None:
|
|
76
|
-
message = json.loads(data)
|
|
77
|
-
if message.get("type") == "audio":
|
|
78
|
-
payload_base64 = message["media"]["payload"]
|
|
79
|
-
payload = base64.b64decode(payload_base64)
|
|
80
|
-
# Convert Genesys 8kHz μ-law to PCM at pipeline input rate
|
|
81
|
-
deserialized_data = await ulaw_to_pcm(
|
|
82
|
-
payload, self._genesys_sample_rate, self._sample_rate, self._resampler
|
|
83
|
-
)
|
|
84
|
-
audio_frame = InputAudioRawFrame(
|
|
85
|
-
audio=deserialized_data, num_channels=1, sample_rate=self._sample_rate
|
|
86
|
-
)
|
|
87
|
-
return audio_frame
|
|
88
|
-
elif message.get("type") == "dtmf":
|
|
89
|
-
digit = message.get("dtmf", {}).get("digit")
|
|
90
|
-
try:
|
|
91
|
-
return InputDTMFFrame(KeypadEntry(digit))
|
|
92
|
-
except ValueError:
|
|
93
|
-
return None
|
|
94
|
-
else:
|
|
95
|
-
return None
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import os
|
|
3
|
-
|
|
4
|
-
from pipecat.frames.frames import TTSAudioRawFrame
|
|
5
|
-
from pipecat.services.google.tts import GoogleTTSService
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
async def test_chirp_tts():
|
|
9
|
-
# Get credentials from environment variable
|
|
10
|
-
credentials_path = (
|
|
11
|
-
"/Users/kalicharanvemuru/Documents/Code/pipecat/examples/ringg-chatbot/creds.json"
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
if not credentials_path or not os.path.exists(credentials_path):
|
|
15
|
-
raise ValueError(
|
|
16
|
-
"Please set GOOGLE_APPLICATION_CREDENTIALS environment variable to your service account key file"
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
# Initialize the TTS service with Chirp voice
|
|
20
|
-
tts = GoogleTTSService(
|
|
21
|
-
credentials_path=credentials_path,
|
|
22
|
-
voice_id="en-US-Chirp3-HD-Charon", # Using Chirp3 HD Charon voice
|
|
23
|
-
sample_rate=24000,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
# Test text
|
|
27
|
-
test_text = "Hello, this is a test of the Google TTS service with Chirp voice."
|
|
28
|
-
|
|
29
|
-
print(f"Testing TTS with text: {test_text}")
|
|
30
|
-
|
|
31
|
-
# Generate speech
|
|
32
|
-
try:
|
|
33
|
-
async for frame in tts.run_tts(test_text):
|
|
34
|
-
if isinstance(frame, TTSAudioRawFrame):
|
|
35
|
-
print(f"Received audio chunk of size: {len(frame.audio)} bytes")
|
|
36
|
-
else:
|
|
37
|
-
print(f"Received frame: {frame.__class__.__name__}")
|
|
38
|
-
|
|
39
|
-
print("TTS generation completed successfully!")
|
|
40
|
-
except Exception as e:
|
|
41
|
-
print(f"Error during TTS generation: {str(e)}")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if __name__ == "__main__":
|
|
45
|
-
asyncio.run(test_chirp_tts())
|