dv-pipecat-ai 0.0.82.dev815__py3-none-any.whl → 0.0.82.dev857__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.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/METADATA +8 -3
- {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/RECORD +106 -79
- pipecat/adapters/base_llm_adapter.py +44 -6
- pipecat/adapters/services/anthropic_adapter.py +302 -2
- pipecat/adapters/services/aws_nova_sonic_adapter.py +40 -2
- pipecat/adapters/services/bedrock_adapter.py +40 -2
- pipecat/adapters/services/gemini_adapter.py +276 -6
- pipecat/adapters/services/open_ai_adapter.py +88 -7
- pipecat/adapters/services/open_ai_realtime_adapter.py +39 -1
- pipecat/audio/dtmf/__init__.py +0 -0
- pipecat/audio/dtmf/types.py +47 -0
- pipecat/audio/dtmf/utils.py +70 -0
- pipecat/audio/filters/aic_filter.py +199 -0
- pipecat/audio/utils.py +9 -7
- pipecat/extensions/ivr/__init__.py +0 -0
- pipecat/extensions/ivr/ivr_navigator.py +452 -0
- pipecat/frames/frames.py +156 -43
- pipecat/pipeline/llm_switcher.py +76 -0
- pipecat/pipeline/parallel_pipeline.py +3 -3
- pipecat/pipeline/service_switcher.py +144 -0
- pipecat/pipeline/task.py +68 -28
- pipecat/pipeline/task_observer.py +10 -0
- pipecat/processors/aggregators/dtmf_aggregator.py +2 -2
- pipecat/processors/aggregators/llm_context.py +277 -0
- pipecat/processors/aggregators/llm_response.py +48 -15
- pipecat/processors/aggregators/llm_response_universal.py +840 -0
- pipecat/processors/aggregators/openai_llm_context.py +3 -3
- pipecat/processors/dtmf_aggregator.py +0 -2
- pipecat/processors/filters/stt_mute_filter.py +0 -2
- pipecat/processors/frame_processor.py +18 -11
- pipecat/processors/frameworks/rtvi.py +17 -10
- pipecat/processors/metrics/sentry.py +2 -0
- pipecat/runner/daily.py +137 -36
- pipecat/runner/run.py +1 -1
- pipecat/runner/utils.py +7 -7
- pipecat/serializers/asterisk.py +20 -4
- pipecat/serializers/exotel.py +1 -1
- pipecat/serializers/plivo.py +1 -1
- pipecat/serializers/telnyx.py +1 -1
- pipecat/serializers/twilio.py +1 -1
- pipecat/services/__init__.py +2 -2
- pipecat/services/anthropic/llm.py +113 -28
- pipecat/services/asyncai/tts.py +4 -0
- pipecat/services/aws/llm.py +82 -8
- pipecat/services/aws/tts.py +0 -10
- pipecat/services/aws_nova_sonic/aws.py +5 -0
- pipecat/services/cartesia/tts.py +28 -16
- pipecat/services/cerebras/llm.py +15 -10
- pipecat/services/deepgram/stt.py +8 -0
- pipecat/services/deepseek/llm.py +13 -8
- pipecat/services/fireworks/llm.py +13 -8
- pipecat/services/fish/tts.py +8 -6
- pipecat/services/gemini_multimodal_live/gemini.py +5 -0
- pipecat/services/gladia/config.py +7 -1
- pipecat/services/gladia/stt.py +23 -15
- pipecat/services/google/llm.py +159 -59
- pipecat/services/google/llm_openai.py +18 -3
- pipecat/services/grok/llm.py +2 -1
- pipecat/services/llm_service.py +38 -3
- pipecat/services/mem0/memory.py +2 -1
- pipecat/services/mistral/llm.py +5 -6
- pipecat/services/nim/llm.py +2 -1
- pipecat/services/openai/base_llm.py +88 -26
- pipecat/services/openai/image.py +6 -1
- pipecat/services/openai_realtime_beta/openai.py +5 -2
- pipecat/services/openpipe/llm.py +6 -8
- pipecat/services/perplexity/llm.py +13 -8
- pipecat/services/playht/tts.py +9 -6
- pipecat/services/rime/tts.py +1 -1
- pipecat/services/sambanova/llm.py +18 -13
- pipecat/services/sarvam/tts.py +415 -10
- pipecat/services/speechmatics/stt.py +2 -2
- pipecat/services/tavus/video.py +1 -1
- pipecat/services/tts_service.py +15 -5
- pipecat/services/vistaar/llm.py +2 -5
- pipecat/transports/base_input.py +32 -19
- pipecat/transports/base_output.py +39 -5
- pipecat/transports/daily/__init__.py +0 -0
- pipecat/transports/daily/transport.py +2371 -0
- pipecat/transports/daily/utils.py +410 -0
- pipecat/transports/livekit/__init__.py +0 -0
- pipecat/transports/livekit/transport.py +1042 -0
- pipecat/transports/network/fastapi_websocket.py +12 -546
- pipecat/transports/network/small_webrtc.py +12 -922
- pipecat/transports/network/webrtc_connection.py +9 -595
- pipecat/transports/network/websocket_client.py +12 -481
- pipecat/transports/network/websocket_server.py +12 -487
- pipecat/transports/services/daily.py +9 -2334
- pipecat/transports/services/helpers/daily_rest.py +12 -396
- pipecat/transports/services/livekit.py +12 -975
- pipecat/transports/services/tavus.py +12 -757
- pipecat/transports/smallwebrtc/__init__.py +0 -0
- pipecat/transports/smallwebrtc/connection.py +612 -0
- pipecat/transports/smallwebrtc/transport.py +936 -0
- pipecat/transports/tavus/__init__.py +0 -0
- pipecat/transports/tavus/transport.py +770 -0
- pipecat/transports/websocket/__init__.py +0 -0
- pipecat/transports/websocket/client.py +494 -0
- pipecat/transports/websocket/fastapi.py +559 -0
- pipecat/transports/websocket/server.py +500 -0
- pipecat/transports/whatsapp/__init__.py +0 -0
- pipecat/transports/whatsapp/api.py +345 -0
- pipecat/transports/whatsapp/client.py +364 -0
- {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/WHEEL +0 -0
- {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/licenses/LICENSE +0 -0
- {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/top_level.txt +0 -0
|
@@ -15,7 +15,7 @@ import copy
|
|
|
15
15
|
import io
|
|
16
16
|
import json
|
|
17
17
|
from dataclasses import dataclass
|
|
18
|
-
from typing import Any, List, Optional
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
19
|
|
|
20
20
|
from openai._types import NOT_GIVEN, NotGiven
|
|
21
21
|
from openai.types.chat import (
|
|
@@ -183,13 +183,13 @@ class OpenAILLMContext:
|
|
|
183
183
|
"""
|
|
184
184
|
return json.dumps(self._messages, cls=CustomEncoder, ensure_ascii=False, indent=2)
|
|
185
185
|
|
|
186
|
-
def get_messages_for_logging(self) -> str:
|
|
186
|
+
def get_messages_for_logging(self) -> List[Dict[str, Any]]:
|
|
187
187
|
"""Get sanitized messages suitable for logging.
|
|
188
188
|
|
|
189
189
|
Removes or truncates sensitive data like image content for safe logging.
|
|
190
190
|
|
|
191
191
|
Returns:
|
|
192
|
-
|
|
192
|
+
List of messages in a format ready for logging.
|
|
193
193
|
"""
|
|
194
194
|
msgs = []
|
|
195
195
|
for message in self.messages:
|
|
@@ -8,7 +8,6 @@ from pipecat.frames.frames import (
|
|
|
8
8
|
InputDTMFFrame,
|
|
9
9
|
StartInterruptionFrame,
|
|
10
10
|
StartUserIdleProcessorFrame,
|
|
11
|
-
StopInterruptionFrame,
|
|
12
11
|
StopUserIdleProcessorFrame,
|
|
13
12
|
TranscriptionFrame,
|
|
14
13
|
UserStartedSpeakingFrame,
|
|
@@ -132,7 +131,6 @@ class DTMFAggregator(FrameProcessor):
|
|
|
132
131
|
aggregated_frame.metadata["push_aggregation"] = True
|
|
133
132
|
await self.push_frame(StartInterruptionFrame())
|
|
134
133
|
await self.push_frame(aggregated_frame, direction)
|
|
135
|
-
await self.push_frame(StopInterruptionFrame())
|
|
136
134
|
self._aggregation = ""
|
|
137
135
|
elif raise_timeout and self._stopped_idle_processor:
|
|
138
136
|
transcript_frame = TranscriptionFrame(
|
|
@@ -27,7 +27,6 @@ from pipecat.frames.frames import (
|
|
|
27
27
|
InterimTranscriptionFrame,
|
|
28
28
|
StartFrame,
|
|
29
29
|
StartInterruptionFrame,
|
|
30
|
-
StopInterruptionFrame,
|
|
31
30
|
STTMuteFrame,
|
|
32
31
|
TranscriptionFrame,
|
|
33
32
|
UserStartedSpeakingFrame,
|
|
@@ -213,7 +212,6 @@ class STTMuteFilter(FrameProcessor):
|
|
|
213
212
|
# Conditionally include InputAudioRawFrame in suppression tuple based on voicemail_detection_enabled
|
|
214
213
|
suppression_types = (
|
|
215
214
|
StartInterruptionFrame,
|
|
216
|
-
StopInterruptionFrame,
|
|
217
215
|
VADUserStartedSpeakingFrame,
|
|
218
216
|
VADUserStoppedSpeakingFrame,
|
|
219
217
|
UserStartedSpeakingFrame,
|
|
@@ -31,7 +31,6 @@ from pipecat.frames.frames import (
|
|
|
31
31
|
FrameProcessorResumeUrgentFrame,
|
|
32
32
|
StartFrame,
|
|
33
33
|
StartInterruptionFrame,
|
|
34
|
-
StopInterruptionFrame,
|
|
35
34
|
SystemFrame,
|
|
36
35
|
)
|
|
37
36
|
from pipecat.metrics.metrics import LLMTokenUsage, MetricsData
|
|
@@ -127,6 +126,11 @@ class FrameProcessorQueue(asyncio.PriorityQueue):
|
|
|
127
126
|
return item
|
|
128
127
|
|
|
129
128
|
|
|
129
|
+
# Timeout in seconds for cancelling the input frame processing task.
|
|
130
|
+
# This prevents hanging if a library swallows asyncio.CancelledError.
|
|
131
|
+
INPUT_TASK_CANCEL_TIMEOUT_SECS = 3
|
|
132
|
+
|
|
133
|
+
|
|
130
134
|
class FrameProcessor(BaseObject):
|
|
131
135
|
"""Base class for all frame processors in the pipeline.
|
|
132
136
|
|
|
@@ -446,7 +450,7 @@ class FrameProcessor(BaseObject):
|
|
|
446
450
|
|
|
447
451
|
.. deprecated:: 0.0.81
|
|
448
452
|
This function is deprecated, use `await task` or
|
|
449
|
-
`await asyncio.wait_for(task, timeout) instead.
|
|
453
|
+
`await asyncio.wait_for(task, timeout)` instead.
|
|
450
454
|
|
|
451
455
|
Args:
|
|
452
456
|
task: The task to wait for.
|
|
@@ -454,12 +458,14 @@ class FrameProcessor(BaseObject):
|
|
|
454
458
|
"""
|
|
455
459
|
import warnings
|
|
456
460
|
|
|
457
|
-
warnings.
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
461
|
+
with warnings.catch_warnings():
|
|
462
|
+
warnings.simplefilter("always")
|
|
463
|
+
warnings.warn(
|
|
464
|
+
"`FrameProcessor.wait_for_task()` is deprecated. "
|
|
465
|
+
"Use `await task` or `await asyncio.wait_for(task, timeout)` instead.",
|
|
466
|
+
DeprecationWarning,
|
|
467
|
+
stacklevel=2,
|
|
468
|
+
)
|
|
463
469
|
|
|
464
470
|
if timeout:
|
|
465
471
|
await asyncio.wait_for(task, timeout)
|
|
@@ -587,8 +593,6 @@ class FrameProcessor(BaseObject):
|
|
|
587
593
|
elif isinstance(frame, StartInterruptionFrame):
|
|
588
594
|
await self._start_interruption()
|
|
589
595
|
await self.stop_all_metrics()
|
|
590
|
-
elif isinstance(frame, StopInterruptionFrame):
|
|
591
|
-
self._should_report_ttfb = True
|
|
592
596
|
elif isinstance(frame, CancelFrame):
|
|
593
597
|
await self.__cancel(frame)
|
|
594
598
|
elif isinstance(frame, (FrameProcessorPauseFrame, FrameProcessorPauseUrgentFrame)):
|
|
@@ -753,7 +757,10 @@ class FrameProcessor(BaseObject):
|
|
|
753
757
|
async def __cancel_input_task(self):
|
|
754
758
|
"""Cancel the frame input processing task."""
|
|
755
759
|
if self.__input_frame_task:
|
|
756
|
-
|
|
760
|
+
# Apply a timeout as a safeguard: if a library swallows asyncio.CancelledError,
|
|
761
|
+
# the task would otherwise never be cancelled. With a timeout, we can detect this
|
|
762
|
+
# situation and surface it in the logs instead of hanging indefinitely.
|
|
763
|
+
await self.cancel_task(self.__input_frame_task, INPUT_TASK_CANCEL_TIMEOUT_SECS)
|
|
757
764
|
self.__input_frame_task = None
|
|
758
765
|
|
|
759
766
|
def __create_process_task(self):
|
|
@@ -42,6 +42,7 @@ from pipecat.frames.frames import (
|
|
|
42
42
|
FunctionCallResultFrame,
|
|
43
43
|
InputAudioRawFrame,
|
|
44
44
|
InterimTranscriptionFrame,
|
|
45
|
+
LLMContextFrame,
|
|
45
46
|
LLMFullResponseEndFrame,
|
|
46
47
|
LLMFullResponseStartFrame,
|
|
47
48
|
LLMMessagesAppendFrame,
|
|
@@ -916,7 +917,10 @@ class RTVIObserver(BaseObserver):
|
|
|
916
917
|
and self._params.user_transcription_enabled
|
|
917
918
|
):
|
|
918
919
|
await self._handle_user_transcriptions(frame)
|
|
919
|
-
elif
|
|
920
|
+
elif (
|
|
921
|
+
isinstance(frame, (OpenAILLMContextFrame, LLMContextFrame))
|
|
922
|
+
and self._params.user_llm_enabled
|
|
923
|
+
):
|
|
920
924
|
await self._handle_context(frame)
|
|
921
925
|
elif isinstance(frame, LLMFullResponseStartFrame) and self._params.bot_llm_enabled:
|
|
922
926
|
await self.push_transport_message_urgent(RTVIBotLLMStartedMessage())
|
|
@@ -1017,16 +1021,20 @@ class RTVIObserver(BaseObserver):
|
|
|
1017
1021
|
if message:
|
|
1018
1022
|
await self.push_transport_message_urgent(message)
|
|
1019
1023
|
|
|
1020
|
-
async def _handle_context(self, frame: OpenAILLMContextFrame):
|
|
1024
|
+
async def _handle_context(self, frame: OpenAILLMContextFrame | LLMContextFrame):
|
|
1021
1025
|
"""Process LLM context frames to extract user messages for the RTVI client."""
|
|
1022
1026
|
try:
|
|
1023
|
-
|
|
1027
|
+
if isinstance(frame, OpenAILLMContextFrame):
|
|
1028
|
+
messages = frame.context.messages
|
|
1029
|
+
else:
|
|
1030
|
+
messages = frame.context.get_messages()
|
|
1024
1031
|
if not messages:
|
|
1025
1032
|
return
|
|
1026
1033
|
|
|
1027
1034
|
message = messages[-1]
|
|
1028
1035
|
|
|
1029
1036
|
# Handle Google LLM format (protobuf objects with attributes)
|
|
1037
|
+
# Note: not possible if frame is a universal LLMContextFrame
|
|
1030
1038
|
if hasattr(message, "role") and message.role == "user" and hasattr(message, "parts"):
|
|
1031
1039
|
text = "".join(part.text for part in message.parts if hasattr(part, "text"))
|
|
1032
1040
|
if text:
|
|
@@ -1116,7 +1124,9 @@ class RTVIProcessor(FrameProcessor):
|
|
|
1116
1124
|
self._bot_ready = False
|
|
1117
1125
|
self._client_ready = False
|
|
1118
1126
|
self._client_ready_id = ""
|
|
1119
|
-
|
|
1127
|
+
# Default to 0.3.0 which is the last version before actually having a
|
|
1128
|
+
# "client-version".
|
|
1129
|
+
self._client_version = [0, 3, 0]
|
|
1120
1130
|
self._errors_enabled = True
|
|
1121
1131
|
|
|
1122
1132
|
self._registered_actions: Dict[str, RTVIAction] = {}
|
|
@@ -1423,16 +1433,13 @@ class RTVIProcessor(FrameProcessor):
|
|
|
1423
1433
|
|
|
1424
1434
|
async def _handle_client_ready(self, request_id: str, data: RTVIClientReadyData | None):
|
|
1425
1435
|
"""Handle the client-ready message from the client."""
|
|
1426
|
-
version = data.version if data else
|
|
1436
|
+
version = data.version if data else None
|
|
1427
1437
|
logger.debug(f"Received client-ready: version {version}")
|
|
1428
|
-
if version
|
|
1429
|
-
self._client_version = [0, 3, 0] # Default to 0.3.0 if unknown
|
|
1430
|
-
else:
|
|
1438
|
+
if version:
|
|
1431
1439
|
try:
|
|
1432
1440
|
self._client_version = [int(v) for v in version.split(".")]
|
|
1433
1441
|
except ValueError:
|
|
1434
1442
|
logger.warning(f"Invalid client version format: {version}")
|
|
1435
|
-
self._client_version = [0, 3, 0]
|
|
1436
1443
|
about = data.about if data else {"library": "unknown"}
|
|
1437
1444
|
logger.debug(f"Client Details: {about}")
|
|
1438
1445
|
if self._input_transport:
|
|
@@ -1615,7 +1622,7 @@ class RTVIProcessor(FrameProcessor):
|
|
|
1615
1622
|
async def _send_bot_ready(self):
|
|
1616
1623
|
"""Send the bot-ready message to the client."""
|
|
1617
1624
|
config = None
|
|
1618
|
-
if self._client_version[0] < 1:
|
|
1625
|
+
if self._client_version and self._client_version[0] < 1:
|
|
1619
1626
|
config = self._config.config
|
|
1620
1627
|
message = RTVIBotReady(
|
|
1621
1628
|
id=self._client_ready_id,
|
pipecat/runner/daily.py
CHANGED
|
@@ -10,6 +10,10 @@ This module provides helper functions for creating and configuring Daily rooms
|
|
|
10
10
|
and authentication tokens. It automatically creates temporary rooms for
|
|
11
11
|
development or uses existing rooms specified via environment variables.
|
|
12
12
|
|
|
13
|
+
Functions:
|
|
14
|
+
|
|
15
|
+
- configure(): Create a standard or SIP-enabled Daily room, returning a DailyRoomConfig object.
|
|
16
|
+
|
|
13
17
|
Environment variables:
|
|
14
18
|
|
|
15
19
|
- DAILY_API_KEY - Daily API key for room/token creation (required)
|
|
@@ -22,39 +26,95 @@ Example::
|
|
|
22
26
|
from pipecat.runner.daily import configure
|
|
23
27
|
|
|
24
28
|
async with aiohttp.ClientSession() as session:
|
|
29
|
+
# Standard room
|
|
25
30
|
room_url, token = await configure(session)
|
|
26
|
-
|
|
31
|
+
|
|
32
|
+
# SIP-enabled room for phone calls
|
|
33
|
+
config = await configure(session, sip_caller_phone="+15551234567")
|
|
34
|
+
# config contains: room_url, token, sip_endpoint
|
|
27
35
|
"""
|
|
28
36
|
|
|
29
37
|
import os
|
|
30
38
|
import time
|
|
31
39
|
import uuid
|
|
32
|
-
from typing import
|
|
40
|
+
from typing import Dict, List, Optional
|
|
33
41
|
|
|
34
42
|
import aiohttp
|
|
43
|
+
from loguru import logger
|
|
44
|
+
from pydantic import BaseModel
|
|
35
45
|
|
|
36
|
-
from pipecat.transports.
|
|
46
|
+
from pipecat.transports.daily.utils import (
|
|
37
47
|
DailyRESTHelper,
|
|
38
48
|
DailyRoomParams,
|
|
39
49
|
DailyRoomProperties,
|
|
50
|
+
DailyRoomSipParams,
|
|
40
51
|
)
|
|
41
52
|
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
"""
|
|
54
|
+
class DailyRoomConfig(BaseModel):
|
|
55
|
+
"""Configuration returned when creating a Daily room.
|
|
56
|
+
|
|
57
|
+
Parameters:
|
|
58
|
+
room_url: The Daily room URL for joining the meeting.
|
|
59
|
+
token: Authentication token for the bot to join the room.
|
|
60
|
+
sip_endpoint: SIP endpoint URI for phone connections (None for standard rooms).
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
room_url: str
|
|
64
|
+
token: str
|
|
65
|
+
sip_endpoint: Optional[str] = None
|
|
66
|
+
|
|
67
|
+
def __iter__(self):
|
|
68
|
+
"""Enable tuple unpacking for backward compatibility.
|
|
69
|
+
|
|
70
|
+
Allows: room_url, token = await configure(session)
|
|
71
|
+
"""
|
|
72
|
+
yield self.room_url
|
|
73
|
+
yield self.token
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def configure(
|
|
77
|
+
aiohttp_session: aiohttp.ClientSession,
|
|
78
|
+
*,
|
|
79
|
+
room_exp_duration: Optional[float] = 2.0,
|
|
80
|
+
token_exp_duration: Optional[float] = 2.0,
|
|
81
|
+
sip_caller_phone: Optional[str] = None,
|
|
82
|
+
sip_enable_video: Optional[bool] = False,
|
|
83
|
+
sip_num_endpoints: Optional[int] = 1,
|
|
84
|
+
sip_codecs: Optional[Dict[str, List[str]]] = None,
|
|
85
|
+
) -> DailyRoomConfig:
|
|
86
|
+
"""Configure Daily room URL and token with optional SIP capabilities.
|
|
45
87
|
|
|
46
88
|
This function will either:
|
|
47
|
-
1. Use an existing room URL from DAILY_SAMPLE_ROOM_URL environment variable
|
|
89
|
+
1. Use an existing room URL from DAILY_SAMPLE_ROOM_URL environment variable (standard mode only)
|
|
48
90
|
2. Create a new temporary room automatically if no URL is provided
|
|
49
91
|
|
|
50
92
|
Args:
|
|
51
93
|
aiohttp_session: HTTP session for making API requests.
|
|
94
|
+
room_exp_duration: Room expiration time in hours.
|
|
95
|
+
token_exp_duration: Token expiration time in hours.
|
|
96
|
+
sip_caller_phone: Phone number or identifier for SIP display name.
|
|
97
|
+
When provided, enables SIP functionality and returns SipRoomConfig.
|
|
98
|
+
sip_enable_video: Whether video is enabled for SIP.
|
|
99
|
+
sip_num_endpoints: Number of allowed SIP endpoints.
|
|
100
|
+
sip_codecs: Codecs to support for audio and video. If None, uses Daily defaults.
|
|
101
|
+
Example: {"audio": ["OPUS"], "video": ["H264"]}
|
|
52
102
|
|
|
53
103
|
Returns:
|
|
54
|
-
|
|
104
|
+
DailyRoomConfig: Object with room_url, token, and optional sip_endpoint.
|
|
105
|
+
Supports tuple unpacking for backward compatibility: room_url, token = await configure(session)
|
|
55
106
|
|
|
56
107
|
Raises:
|
|
57
108
|
Exception: If DAILY_API_KEY is not provided in environment variables.
|
|
109
|
+
|
|
110
|
+
Examples::
|
|
111
|
+
|
|
112
|
+
# Standard room
|
|
113
|
+
room_url, token = await configure(session)
|
|
114
|
+
|
|
115
|
+
# SIP-enabled room
|
|
116
|
+
sip_config = await configure(session, sip_caller_phone="+15551234567")
|
|
117
|
+
print(f"SIP endpoint: {sip_config.sip_endpoint}")
|
|
58
118
|
"""
|
|
59
119
|
# Check for required API key
|
|
60
120
|
api_key = os.getenv("DAILY_API_KEY")
|
|
@@ -64,8 +124,8 @@ async def configure(aiohttp_session: aiohttp.ClientSession) -> Tuple[str, str]:
|
|
|
64
124
|
"Get your API key from https://dashboard.daily.co/developers"
|
|
65
125
|
)
|
|
66
126
|
|
|
67
|
-
#
|
|
68
|
-
|
|
127
|
+
# Determine if SIP mode is enabled
|
|
128
|
+
sip_enabled = sip_caller_phone is not None
|
|
69
129
|
|
|
70
130
|
daily_rest_helper = DailyRESTHelper(
|
|
71
131
|
daily_api_key=api_key,
|
|
@@ -73,36 +133,75 @@ async def configure(aiohttp_session: aiohttp.ClientSession) -> Tuple[str, str]:
|
|
|
73
133
|
aiohttp_session=aiohttp_session,
|
|
74
134
|
)
|
|
75
135
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
136
|
+
# Check for existing room URL (only in standard mode)
|
|
137
|
+
existing_room_url = os.getenv("DAILY_SAMPLE_ROOM_URL")
|
|
138
|
+
if existing_room_url and not sip_enabled:
|
|
139
|
+
# Use existing room (standard mode only)
|
|
140
|
+
logger.info(f"Using existing Daily room: {existing_room_url}")
|
|
79
141
|
room_url = existing_room_url
|
|
80
|
-
|
|
81
|
-
# Create
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
142
|
+
|
|
143
|
+
# Create token and return standard format
|
|
144
|
+
expiry_time: float = token_exp_duration * 60 * 60
|
|
145
|
+
token = await daily_rest_helper.get_token(room_url, expiry_time)
|
|
146
|
+
return DailyRoomConfig(room_url=room_url, token=token)
|
|
147
|
+
|
|
148
|
+
# Create a new room
|
|
149
|
+
room_prefix = "pipecat-sip" if sip_enabled else "pipecat"
|
|
150
|
+
room_name = f"{room_prefix}-{uuid.uuid4().hex[:8]}"
|
|
151
|
+
logger.info(f"Creating new Daily room: {room_name}")
|
|
152
|
+
|
|
153
|
+
# Calculate expiration time
|
|
154
|
+
expiration_time = time.time() + (room_exp_duration * 60 * 60)
|
|
155
|
+
|
|
156
|
+
# Create room properties
|
|
157
|
+
room_properties = DailyRoomProperties(
|
|
158
|
+
exp=expiration_time,
|
|
159
|
+
eject_at_room_exp=True,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Add SIP configuration if enabled
|
|
163
|
+
if sip_enabled:
|
|
164
|
+
sip_params = DailyRoomSipParams(
|
|
165
|
+
display_name=sip_caller_phone,
|
|
166
|
+
video=sip_enable_video,
|
|
167
|
+
sip_mode="dial-in",
|
|
168
|
+
num_endpoints=sip_num_endpoints,
|
|
169
|
+
codecs=sip_codecs,
|
|
92
170
|
)
|
|
171
|
+
room_properties.sip = sip_params
|
|
172
|
+
room_properties.enable_dialout = True # Enable outbound calls if needed
|
|
173
|
+
room_properties.start_video_off = not sip_enable_video # Voice-only by default
|
|
93
174
|
|
|
94
|
-
|
|
95
|
-
|
|
175
|
+
# Create room parameters
|
|
176
|
+
room_params = DailyRoomParams(name=room_name, properties=room_properties)
|
|
96
177
|
|
|
178
|
+
try:
|
|
97
179
|
room_response = await daily_rest_helper.create_room(room_params)
|
|
98
180
|
room_url = room_response.url
|
|
99
|
-
|
|
181
|
+
logger.info(f"Created Daily room: {room_url}")
|
|
182
|
+
|
|
183
|
+
# Create meeting token
|
|
184
|
+
token_expiry_seconds = token_exp_duration * 60 * 60
|
|
185
|
+
token = await daily_rest_helper.get_token(room_url, token_expiry_seconds)
|
|
100
186
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
187
|
+
if sip_enabled:
|
|
188
|
+
# Return SIP configuration object
|
|
189
|
+
sip_endpoint = room_response.config.sip_endpoint
|
|
190
|
+
logger.info(f"SIP endpoint: {sip_endpoint}")
|
|
104
191
|
|
|
105
|
-
|
|
192
|
+
return DailyRoomConfig(
|
|
193
|
+
room_url=room_url,
|
|
194
|
+
token=token,
|
|
195
|
+
sip_endpoint=sip_endpoint,
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
# Return standard configuration
|
|
199
|
+
return DailyRoomConfig(room_url=room_url, token=token)
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
error_msg = f"Error creating Daily room: {e}"
|
|
203
|
+
logger.error(error_msg)
|
|
204
|
+
raise
|
|
106
205
|
|
|
107
206
|
|
|
108
207
|
# Keep this for backwards compatibility, but mark as deprecated
|
|
@@ -122,11 +221,13 @@ async def configure_with_args(aiohttp_session: aiohttp.ClientSession, parser=Non
|
|
|
122
221
|
"""
|
|
123
222
|
import warnings
|
|
124
223
|
|
|
125
|
-
warnings.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
224
|
+
with warnings.catch_warnings():
|
|
225
|
+
warnings.simplefilter("always")
|
|
226
|
+
warnings.warn(
|
|
227
|
+
"configure_with_args is deprecated. Use configure() instead.",
|
|
228
|
+
DeprecationWarning,
|
|
229
|
+
stacklevel=2,
|
|
230
|
+
)
|
|
130
231
|
|
|
131
232
|
room_url, token = await configure(aiohttp_session)
|
|
132
233
|
return (room_url, token, None)
|
pipecat/runner/run.py
CHANGED
|
@@ -182,7 +182,7 @@ def _setup_webrtc_routes(app: FastAPI, esp32_mode: bool = False, host: str = "lo
|
|
|
182
182
|
try:
|
|
183
183
|
from pipecat_ai_small_webrtc_prebuilt.frontend import SmallWebRTCPrebuiltUI
|
|
184
184
|
|
|
185
|
-
from pipecat.transports.
|
|
185
|
+
from pipecat.transports.smallwebrtc.connection import SmallWebRTCConnection
|
|
186
186
|
except ImportError as e:
|
|
187
187
|
logger.error(f"WebRTC transport dependencies not installed: {e}")
|
|
188
188
|
return
|
pipecat/runner/utils.py
CHANGED
|
@@ -203,7 +203,7 @@ def get_transport_client_id(transport: BaseTransport, client: Any) -> str:
|
|
|
203
203
|
"""
|
|
204
204
|
# Import conditionally to avoid dependency issues
|
|
205
205
|
try:
|
|
206
|
-
from pipecat.transports.
|
|
206
|
+
from pipecat.transports.smallwebrtc.transport import SmallWebRTCTransport
|
|
207
207
|
|
|
208
208
|
if isinstance(transport, SmallWebRTCTransport):
|
|
209
209
|
return client.pc_id
|
|
@@ -211,7 +211,7 @@ def get_transport_client_id(transport: BaseTransport, client: Any) -> str:
|
|
|
211
211
|
pass
|
|
212
212
|
|
|
213
213
|
try:
|
|
214
|
-
from pipecat.transports.
|
|
214
|
+
from pipecat.transports.daily.transport import DailyTransport
|
|
215
215
|
|
|
216
216
|
if isinstance(transport, DailyTransport):
|
|
217
217
|
return client["id"]
|
|
@@ -233,7 +233,7 @@ async def maybe_capture_participant_camera(
|
|
|
233
233
|
framerate: Video capture framerate. Defaults to 0 (auto).
|
|
234
234
|
"""
|
|
235
235
|
try:
|
|
236
|
-
from pipecat.transports.
|
|
236
|
+
from pipecat.transports.daily.transport import DailyTransport
|
|
237
237
|
|
|
238
238
|
if isinstance(transport, DailyTransport):
|
|
239
239
|
await transport.capture_participant_video(
|
|
@@ -254,7 +254,7 @@ async def maybe_capture_participant_screen(
|
|
|
254
254
|
framerate: Video capture framerate. Defaults to 0 (auto).
|
|
255
255
|
"""
|
|
256
256
|
try:
|
|
257
|
-
from pipecat.transports.
|
|
257
|
+
from pipecat.transports.daily.transport import DailyTransport
|
|
258
258
|
|
|
259
259
|
if isinstance(transport, DailyTransport):
|
|
260
260
|
await transport.capture_participant_video(
|
|
@@ -359,7 +359,7 @@ async def _create_telephony_transport(
|
|
|
359
359
|
Returns:
|
|
360
360
|
Configured FastAPIWebsocketTransport ready for telephony use.
|
|
361
361
|
"""
|
|
362
|
-
from pipecat.transports.
|
|
362
|
+
from pipecat.transports.websocket.fastapi import FastAPIWebsocketTransport
|
|
363
363
|
|
|
364
364
|
if params is None:
|
|
365
365
|
raise ValueError(
|
|
@@ -482,7 +482,7 @@ async def create_transport(
|
|
|
482
482
|
if isinstance(runner_args, DailyRunnerArguments):
|
|
483
483
|
params = _get_transport_params("daily", transport_params)
|
|
484
484
|
|
|
485
|
-
from pipecat.transports.
|
|
485
|
+
from pipecat.transports.daily.transport import DailyTransport
|
|
486
486
|
|
|
487
487
|
return DailyTransport(
|
|
488
488
|
runner_args.room_url,
|
|
@@ -494,7 +494,7 @@ async def create_transport(
|
|
|
494
494
|
elif isinstance(runner_args, SmallWebRTCRunnerArguments):
|
|
495
495
|
params = _get_transport_params("webrtc", transport_params)
|
|
496
496
|
|
|
497
|
-
from pipecat.transports.
|
|
497
|
+
from pipecat.transports.smallwebrtc.transport import SmallWebRTCTransport
|
|
498
498
|
|
|
499
499
|
return SmallWebRTCTransport(
|
|
500
500
|
params=params,
|
pipecat/serializers/asterisk.py
CHANGED
|
@@ -33,7 +33,7 @@ class AsteriskFrameSerializer(FrameSerializer):
|
|
|
33
33
|
|
|
34
34
|
# What the ADAPTER/Asterisk is sending/expecting on the wire:
|
|
35
35
|
# "pcmu" -> μ-law @ 8k; "pcm16" -> signed 16-bit @ 8k
|
|
36
|
-
telephony_encoding: Literal["pcmu", "pcm16"] = "pcmu"
|
|
36
|
+
telephony_encoding: Literal["pcmu", "pcma", "pcm16"] = "pcmu"
|
|
37
37
|
telephony_sample_rate: int = 8000
|
|
38
38
|
sample_rate: Optional[int] = None # pipeline input rate
|
|
39
39
|
auto_hang_up: bool = False # no-op here; adapter handles hangup
|
|
@@ -45,6 +45,7 @@ class AsteriskFrameSerializer(FrameSerializer):
|
|
|
45
45
|
self._sample_rate = 0
|
|
46
46
|
self._in_resampler = create_stream_resampler()
|
|
47
47
|
self._out_resampler = create_stream_resampler()
|
|
48
|
+
self._hangup_sent = False
|
|
48
49
|
|
|
49
50
|
@property
|
|
50
51
|
def type(self) -> FrameSerializerType:
|
|
@@ -80,6 +81,19 @@ class AsteriskFrameSerializer(FrameSerializer):
|
|
|
80
81
|
"payload": payload,
|
|
81
82
|
}
|
|
82
83
|
)
|
|
84
|
+
elif self._params.telephony_encoding == "pcma":
|
|
85
|
+
al = await pcm_to_alaw(pcm, frame.sample_rate, self._tel_rate, self._out_resampler)
|
|
86
|
+
if not al:
|
|
87
|
+
return None
|
|
88
|
+
payload = base64.b64encode(al).decode("utf-8")
|
|
89
|
+
return json.dumps(
|
|
90
|
+
{
|
|
91
|
+
"event": "media",
|
|
92
|
+
"encoding": "pcma",
|
|
93
|
+
"sampleRate": self._tel_rate,
|
|
94
|
+
"payload": payload,
|
|
95
|
+
}
|
|
96
|
+
)
|
|
83
97
|
else: # "pcm16"
|
|
84
98
|
# resample to 8k if needed, but data stays PCM16 bytes
|
|
85
99
|
pcm8 = await self._out_resampler.resample(pcm, frame.sample_rate, self._tel_rate)
|
|
@@ -105,14 +119,16 @@ class AsteriskFrameSerializer(FrameSerializer):
|
|
|
105
119
|
except Exception:
|
|
106
120
|
return None
|
|
107
121
|
if msg.get("event") == "media":
|
|
108
|
-
enc = msg.get("encoding")
|
|
109
122
|
sr = int(msg.get("sampleRate", self._tel_rate))
|
|
110
123
|
raw = base64.b64decode(msg.get("payload", ""))
|
|
111
124
|
if not raw:
|
|
112
125
|
return None
|
|
113
|
-
|
|
126
|
+
# Use our configured telephony_encoding instead of trusting the message
|
|
127
|
+
if self._params.telephony_encoding == "pcmu":
|
|
114
128
|
pcm = await ulaw_to_pcm(raw, sr, self._sample_rate, self._in_resampler)
|
|
115
|
-
elif
|
|
129
|
+
elif self._params.telephony_encoding == "pcma":
|
|
130
|
+
pcm = await alaw_to_pcm(raw, sr, self._sample_rate, self._in_resampler)
|
|
131
|
+
elif self._params.telephony_encoding == "pcm16":
|
|
116
132
|
# resample if pipeline rate != 8k
|
|
117
133
|
pcm = await self._in_resampler.resample(raw, sr, self._sample_rate)
|
|
118
134
|
else:
|
pipecat/serializers/exotel.py
CHANGED
|
@@ -13,13 +13,13 @@ from typing import Optional
|
|
|
13
13
|
from loguru import logger
|
|
14
14
|
from pydantic import BaseModel
|
|
15
15
|
|
|
16
|
+
from pipecat.audio.dtmf.types import KeypadEntry
|
|
16
17
|
from pipecat.audio.utils import create_stream_resampler
|
|
17
18
|
from pipecat.frames.frames import (
|
|
18
19
|
AudioRawFrame,
|
|
19
20
|
Frame,
|
|
20
21
|
InputAudioRawFrame,
|
|
21
22
|
InputDTMFFrame,
|
|
22
|
-
KeypadEntry,
|
|
23
23
|
StartFrame,
|
|
24
24
|
StartInterruptionFrame,
|
|
25
25
|
TransportMessageFrame,
|
pipecat/serializers/plivo.py
CHANGED
|
@@ -13,6 +13,7 @@ from typing import Optional
|
|
|
13
13
|
from loguru import logger
|
|
14
14
|
from pydantic import BaseModel
|
|
15
15
|
|
|
16
|
+
from pipecat.audio.dtmf.types import KeypadEntry
|
|
16
17
|
from pipecat.audio.utils import create_stream_resampler, pcm_to_ulaw, ulaw_to_pcm
|
|
17
18
|
from pipecat.frames.frames import (
|
|
18
19
|
AudioRawFrame,
|
|
@@ -21,7 +22,6 @@ from pipecat.frames.frames import (
|
|
|
21
22
|
Frame,
|
|
22
23
|
InputAudioRawFrame,
|
|
23
24
|
InputDTMFFrame,
|
|
24
|
-
KeypadEntry,
|
|
25
25
|
StartFrame,
|
|
26
26
|
StartInterruptionFrame,
|
|
27
27
|
TransportMessageFrame,
|
pipecat/serializers/telnyx.py
CHANGED
|
@@ -14,6 +14,7 @@ import aiohttp
|
|
|
14
14
|
from loguru import logger
|
|
15
15
|
from pydantic import BaseModel
|
|
16
16
|
|
|
17
|
+
from pipecat.audio.dtmf.types import KeypadEntry
|
|
17
18
|
from pipecat.audio.utils import (
|
|
18
19
|
alaw_to_pcm,
|
|
19
20
|
create_stream_resampler,
|
|
@@ -28,7 +29,6 @@ from pipecat.frames.frames import (
|
|
|
28
29
|
Frame,
|
|
29
30
|
InputAudioRawFrame,
|
|
30
31
|
InputDTMFFrame,
|
|
31
|
-
KeypadEntry,
|
|
32
32
|
StartFrame,
|
|
33
33
|
StartInterruptionFrame,
|
|
34
34
|
)
|
pipecat/serializers/twilio.py
CHANGED
|
@@ -13,6 +13,7 @@ from typing import Optional
|
|
|
13
13
|
from loguru import logger
|
|
14
14
|
from pydantic import BaseModel
|
|
15
15
|
|
|
16
|
+
from pipecat.audio.dtmf.types import KeypadEntry
|
|
16
17
|
from pipecat.audio.utils import create_stream_resampler, pcm_to_ulaw, ulaw_to_pcm
|
|
17
18
|
from pipecat.frames.frames import (
|
|
18
19
|
AudioRawFrame,
|
|
@@ -21,7 +22,6 @@ from pipecat.frames.frames import (
|
|
|
21
22
|
Frame,
|
|
22
23
|
InputAudioRawFrame,
|
|
23
24
|
InputDTMFFrame,
|
|
24
|
-
KeypadEntry,
|
|
25
25
|
StartFrame,
|
|
26
26
|
StartInterruptionFrame,
|
|
27
27
|
TransportMessageFrame,
|
pipecat/services/__init__.py
CHANGED
|
@@ -11,11 +11,11 @@ _warned_modules = set()
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def _warn_deprecated_access(globals: Dict[str, Any], attr, old: str, new: str):
|
|
14
|
-
import warnings
|
|
15
|
-
|
|
16
14
|
# Only warn once per old->new module pair
|
|
17
15
|
module_key = (old, new)
|
|
18
16
|
if module_key not in _warned_modules:
|
|
17
|
+
import warnings
|
|
18
|
+
|
|
19
19
|
with warnings.catch_warnings():
|
|
20
20
|
warnings.simplefilter("always")
|
|
21
21
|
warnings.warn(
|