dv-pipecat-ai 0.0.82.dev857__py3-none-any.whl → 0.0.85.dev837__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dv-pipecat-ai might be problematic. Click here for more details.

Files changed (195) hide show
  1. {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/METADATA +98 -130
  2. {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/RECORD +192 -140
  3. pipecat/adapters/base_llm_adapter.py +38 -1
  4. pipecat/adapters/services/anthropic_adapter.py +9 -14
  5. pipecat/adapters/services/aws_nova_sonic_adapter.py +120 -5
  6. pipecat/adapters/services/bedrock_adapter.py +236 -13
  7. pipecat/adapters/services/gemini_adapter.py +12 -8
  8. pipecat/adapters/services/open_ai_adapter.py +19 -7
  9. pipecat/adapters/services/open_ai_realtime_adapter.py +5 -0
  10. pipecat/audio/dtmf/dtmf-0.wav +0 -0
  11. pipecat/audio/dtmf/dtmf-1.wav +0 -0
  12. pipecat/audio/dtmf/dtmf-2.wav +0 -0
  13. pipecat/audio/dtmf/dtmf-3.wav +0 -0
  14. pipecat/audio/dtmf/dtmf-4.wav +0 -0
  15. pipecat/audio/dtmf/dtmf-5.wav +0 -0
  16. pipecat/audio/dtmf/dtmf-6.wav +0 -0
  17. pipecat/audio/dtmf/dtmf-7.wav +0 -0
  18. pipecat/audio/dtmf/dtmf-8.wav +0 -0
  19. pipecat/audio/dtmf/dtmf-9.wav +0 -0
  20. pipecat/audio/dtmf/dtmf-pound.wav +0 -0
  21. pipecat/audio/dtmf/dtmf-star.wav +0 -0
  22. pipecat/audio/filters/krisp_viva_filter.py +193 -0
  23. pipecat/audio/filters/noisereduce_filter.py +15 -0
  24. pipecat/audio/turn/base_turn_analyzer.py +9 -1
  25. pipecat/audio/turn/smart_turn/base_smart_turn.py +14 -8
  26. pipecat/audio/turn/smart_turn/data/__init__.py +0 -0
  27. pipecat/audio/turn/smart_turn/data/smart-turn-v3.0.onnx +0 -0
  28. pipecat/audio/turn/smart_turn/http_smart_turn.py +6 -2
  29. pipecat/audio/turn/smart_turn/local_smart_turn.py +1 -1
  30. pipecat/audio/turn/smart_turn/local_smart_turn_v2.py +1 -1
  31. pipecat/audio/turn/smart_turn/local_smart_turn_v3.py +124 -0
  32. pipecat/audio/vad/data/README.md +10 -0
  33. pipecat/audio/vad/data/silero_vad_v2.onnx +0 -0
  34. pipecat/audio/vad/silero.py +9 -3
  35. pipecat/audio/vad/vad_analyzer.py +13 -1
  36. pipecat/extensions/voicemail/voicemail_detector.py +5 -5
  37. pipecat/frames/frames.py +277 -86
  38. pipecat/observers/loggers/debug_log_observer.py +3 -3
  39. pipecat/observers/loggers/llm_log_observer.py +7 -3
  40. pipecat/observers/loggers/user_bot_latency_log_observer.py +22 -10
  41. pipecat/pipeline/runner.py +18 -6
  42. pipecat/pipeline/service_switcher.py +64 -36
  43. pipecat/pipeline/task.py +125 -79
  44. pipecat/pipeline/tts_switcher.py +30 -0
  45. pipecat/processors/aggregators/dtmf_aggregator.py +2 -3
  46. pipecat/processors/aggregators/{gated_openai_llm_context.py → gated_llm_context.py} +9 -9
  47. pipecat/processors/aggregators/gated_open_ai_llm_context.py +12 -0
  48. pipecat/processors/aggregators/llm_context.py +40 -2
  49. pipecat/processors/aggregators/llm_response.py +32 -15
  50. pipecat/processors/aggregators/llm_response_universal.py +19 -15
  51. pipecat/processors/aggregators/user_response.py +6 -6
  52. pipecat/processors/aggregators/vision_image_frame.py +24 -2
  53. pipecat/processors/audio/audio_buffer_processor.py +43 -8
  54. pipecat/processors/dtmf_aggregator.py +174 -77
  55. pipecat/processors/filters/stt_mute_filter.py +17 -0
  56. pipecat/processors/frame_processor.py +110 -24
  57. pipecat/processors/frameworks/langchain.py +8 -2
  58. pipecat/processors/frameworks/rtvi.py +210 -68
  59. pipecat/processors/frameworks/strands_agents.py +170 -0
  60. pipecat/processors/logger.py +2 -2
  61. pipecat/processors/transcript_processor.py +26 -5
  62. pipecat/processors/user_idle_processor.py +35 -11
  63. pipecat/runner/daily.py +59 -20
  64. pipecat/runner/run.py +395 -93
  65. pipecat/runner/types.py +6 -4
  66. pipecat/runner/utils.py +51 -10
  67. pipecat/serializers/__init__.py +5 -1
  68. pipecat/serializers/asterisk.py +16 -2
  69. pipecat/serializers/convox.py +41 -4
  70. pipecat/serializers/custom.py +257 -0
  71. pipecat/serializers/exotel.py +5 -5
  72. pipecat/serializers/livekit.py +20 -0
  73. pipecat/serializers/plivo.py +5 -5
  74. pipecat/serializers/protobuf.py +6 -5
  75. pipecat/serializers/telnyx.py +2 -2
  76. pipecat/serializers/twilio.py +43 -23
  77. pipecat/serializers/vi.py +324 -0
  78. pipecat/services/ai_service.py +2 -6
  79. pipecat/services/anthropic/llm.py +2 -25
  80. pipecat/services/assemblyai/models.py +6 -0
  81. pipecat/services/assemblyai/stt.py +13 -5
  82. pipecat/services/asyncai/tts.py +5 -3
  83. pipecat/services/aws/__init__.py +1 -0
  84. pipecat/services/aws/llm.py +147 -105
  85. pipecat/services/aws/nova_sonic/__init__.py +0 -0
  86. pipecat/services/aws/nova_sonic/context.py +436 -0
  87. pipecat/services/aws/nova_sonic/frames.py +25 -0
  88. pipecat/services/aws/nova_sonic/llm.py +1265 -0
  89. pipecat/services/aws/stt.py +3 -3
  90. pipecat/services/aws_nova_sonic/__init__.py +19 -1
  91. pipecat/services/aws_nova_sonic/aws.py +11 -1151
  92. pipecat/services/aws_nova_sonic/context.py +8 -354
  93. pipecat/services/aws_nova_sonic/frames.py +13 -17
  94. pipecat/services/azure/llm.py +51 -1
  95. pipecat/services/azure/realtime/__init__.py +0 -0
  96. pipecat/services/azure/realtime/llm.py +65 -0
  97. pipecat/services/azure/stt.py +15 -0
  98. pipecat/services/cartesia/stt.py +77 -70
  99. pipecat/services/cartesia/tts.py +80 -13
  100. pipecat/services/deepgram/__init__.py +1 -0
  101. pipecat/services/deepgram/flux/__init__.py +0 -0
  102. pipecat/services/deepgram/flux/stt.py +640 -0
  103. pipecat/services/elevenlabs/__init__.py +4 -1
  104. pipecat/services/elevenlabs/stt.py +339 -0
  105. pipecat/services/elevenlabs/tts.py +87 -46
  106. pipecat/services/fish/tts.py +5 -2
  107. pipecat/services/gemini_multimodal_live/events.py +38 -524
  108. pipecat/services/gemini_multimodal_live/file_api.py +23 -173
  109. pipecat/services/gemini_multimodal_live/gemini.py +41 -1403
  110. pipecat/services/gladia/stt.py +56 -72
  111. pipecat/services/google/__init__.py +1 -0
  112. pipecat/services/google/gemini_live/__init__.py +3 -0
  113. pipecat/services/google/gemini_live/file_api.py +189 -0
  114. pipecat/services/google/gemini_live/llm.py +1582 -0
  115. pipecat/services/google/gemini_live/llm_vertex.py +184 -0
  116. pipecat/services/google/llm.py +15 -11
  117. pipecat/services/google/llm_openai.py +3 -3
  118. pipecat/services/google/llm_vertex.py +86 -16
  119. pipecat/services/google/stt.py +4 -0
  120. pipecat/services/google/tts.py +7 -3
  121. pipecat/services/heygen/api.py +2 -0
  122. pipecat/services/heygen/client.py +8 -4
  123. pipecat/services/heygen/video.py +2 -0
  124. pipecat/services/hume/__init__.py +5 -0
  125. pipecat/services/hume/tts.py +220 -0
  126. pipecat/services/inworld/tts.py +6 -6
  127. pipecat/services/llm_service.py +15 -5
  128. pipecat/services/lmnt/tts.py +4 -2
  129. pipecat/services/mcp_service.py +4 -2
  130. pipecat/services/mem0/memory.py +6 -5
  131. pipecat/services/mistral/llm.py +29 -8
  132. pipecat/services/moondream/vision.py +42 -16
  133. pipecat/services/neuphonic/tts.py +5 -2
  134. pipecat/services/openai/__init__.py +1 -0
  135. pipecat/services/openai/base_llm.py +27 -20
  136. pipecat/services/openai/realtime/__init__.py +0 -0
  137. pipecat/services/openai/realtime/context.py +272 -0
  138. pipecat/services/openai/realtime/events.py +1106 -0
  139. pipecat/services/openai/realtime/frames.py +37 -0
  140. pipecat/services/openai/realtime/llm.py +829 -0
  141. pipecat/services/openai/tts.py +49 -10
  142. pipecat/services/openai_realtime/__init__.py +27 -0
  143. pipecat/services/openai_realtime/azure.py +21 -0
  144. pipecat/services/openai_realtime/context.py +21 -0
  145. pipecat/services/openai_realtime/events.py +21 -0
  146. pipecat/services/openai_realtime/frames.py +21 -0
  147. pipecat/services/openai_realtime_beta/azure.py +16 -0
  148. pipecat/services/openai_realtime_beta/openai.py +17 -5
  149. pipecat/services/piper/tts.py +7 -9
  150. pipecat/services/playht/tts.py +34 -4
  151. pipecat/services/rime/tts.py +12 -12
  152. pipecat/services/riva/stt.py +3 -1
  153. pipecat/services/salesforce/__init__.py +9 -0
  154. pipecat/services/salesforce/llm.py +700 -0
  155. pipecat/services/sarvam/__init__.py +7 -0
  156. pipecat/services/sarvam/stt.py +540 -0
  157. pipecat/services/sarvam/tts.py +97 -13
  158. pipecat/services/simli/video.py +2 -2
  159. pipecat/services/speechmatics/stt.py +22 -10
  160. pipecat/services/stt_service.py +47 -0
  161. pipecat/services/tavus/video.py +2 -2
  162. pipecat/services/tts_service.py +75 -22
  163. pipecat/services/vision_service.py +7 -6
  164. pipecat/services/vistaar/llm.py +51 -9
  165. pipecat/tests/utils.py +4 -4
  166. pipecat/transcriptions/language.py +41 -1
  167. pipecat/transports/base_input.py +13 -34
  168. pipecat/transports/base_output.py +140 -104
  169. pipecat/transports/daily/transport.py +199 -26
  170. pipecat/transports/heygen/__init__.py +0 -0
  171. pipecat/transports/heygen/transport.py +381 -0
  172. pipecat/transports/livekit/transport.py +228 -63
  173. pipecat/transports/local/audio.py +6 -1
  174. pipecat/transports/local/tk.py +11 -2
  175. pipecat/transports/network/fastapi_websocket.py +1 -1
  176. pipecat/transports/smallwebrtc/connection.py +103 -19
  177. pipecat/transports/smallwebrtc/request_handler.py +246 -0
  178. pipecat/transports/smallwebrtc/transport.py +65 -23
  179. pipecat/transports/tavus/transport.py +23 -12
  180. pipecat/transports/websocket/client.py +41 -5
  181. pipecat/transports/websocket/fastapi.py +21 -11
  182. pipecat/transports/websocket/server.py +14 -7
  183. pipecat/transports/whatsapp/api.py +8 -0
  184. pipecat/transports/whatsapp/client.py +47 -0
  185. pipecat/utils/base_object.py +54 -22
  186. pipecat/utils/redis.py +58 -0
  187. pipecat/utils/string.py +13 -1
  188. pipecat/utils/tracing/service_decorators.py +21 -21
  189. pipecat/serializers/genesys.py +0 -95
  190. pipecat/services/google/test-google-chirp.py +0 -45
  191. pipecat/services/openai.py +0 -698
  192. {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/WHEEL +0 -0
  193. {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/licenses/LICENSE +0 -0
  194. {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/top_level.txt +0 -0
  195. /pipecat/services/{aws_nova_sonic → aws/nova_sonic}/ready.wav +0 -0
@@ -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
@@ -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 typing import Optional
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: dict = {}
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} not registered")
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
- # If we haven't registered an event handler, we don't need to do
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
- # Create the task.
135
- task = asyncio.create_task(self._run_task(event_name, *args, **kwargs))
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
- # Add it to our list of event tasks.
138
- self._event_tasks.add((event_name, task))
169
+ # Add it to our list of event tasks.
170
+ self._event_tasks.add((event_name, task))
139
171
 
140
- # Remove the task from the event tasks list when the task completes.
141
- task.add_done_callback(self._event_task_finished)
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 _run_task(self, event_name: str, *args, **kwargs):
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 of the event being handled.
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
- for handler in self._event_handlers[event_name]:
153
- if inspect.iscoroutinefunction(handler):
154
- await handler(self, *args, **kwargs)
155
- else:
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
- nltk.download("punkt_tab", quiet=True)
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
- evt = args[0] if args else None
655
- if evt and hasattr(evt, "toolCall") and evt.toolCall.functionCalls:
656
- function_calls = evt.toolCall.functionCalls
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
- evt = args[0] if args else None
726
- if evt and hasattr(evt, "usageMetadata") and evt.usageMetadata:
727
- usage = evt.usageMetadata
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, "promptTokenCount"):
731
- operation_attrs["tokens.prompt"] = usage.promptTokenCount or 0
732
- if hasattr(usage, "responseTokenCount"):
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.responseTokenCount or 0
734
+ usage.response_token_count or 0
735
735
  )
736
- if hasattr(usage, "totalTokenCount"):
737
- operation_attrs["tokens.total"] = usage.totalTokenCount or 0
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
- evt
755
- and hasattr(evt, "serverContent")
756
- and evt.serverContent.turnComplete
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
- evt = args[0] if args else None
776
- if evt and hasattr(evt, "usageMetadata") and evt.usageMetadata:
777
- usage = evt.usageMetadata
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.promptTokenCount or 0,
783
- completion_tokens=usage.responseTokenCount or 0,
784
- total_tokens=usage.totalTokenCount or 0,
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
 
@@ -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())