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.

Files changed (106) hide show
  1. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/METADATA +8 -3
  2. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/RECORD +106 -79
  3. pipecat/adapters/base_llm_adapter.py +44 -6
  4. pipecat/adapters/services/anthropic_adapter.py +302 -2
  5. pipecat/adapters/services/aws_nova_sonic_adapter.py +40 -2
  6. pipecat/adapters/services/bedrock_adapter.py +40 -2
  7. pipecat/adapters/services/gemini_adapter.py +276 -6
  8. pipecat/adapters/services/open_ai_adapter.py +88 -7
  9. pipecat/adapters/services/open_ai_realtime_adapter.py +39 -1
  10. pipecat/audio/dtmf/__init__.py +0 -0
  11. pipecat/audio/dtmf/types.py +47 -0
  12. pipecat/audio/dtmf/utils.py +70 -0
  13. pipecat/audio/filters/aic_filter.py +199 -0
  14. pipecat/audio/utils.py +9 -7
  15. pipecat/extensions/ivr/__init__.py +0 -0
  16. pipecat/extensions/ivr/ivr_navigator.py +452 -0
  17. pipecat/frames/frames.py +156 -43
  18. pipecat/pipeline/llm_switcher.py +76 -0
  19. pipecat/pipeline/parallel_pipeline.py +3 -3
  20. pipecat/pipeline/service_switcher.py +144 -0
  21. pipecat/pipeline/task.py +68 -28
  22. pipecat/pipeline/task_observer.py +10 -0
  23. pipecat/processors/aggregators/dtmf_aggregator.py +2 -2
  24. pipecat/processors/aggregators/llm_context.py +277 -0
  25. pipecat/processors/aggregators/llm_response.py +48 -15
  26. pipecat/processors/aggregators/llm_response_universal.py +840 -0
  27. pipecat/processors/aggregators/openai_llm_context.py +3 -3
  28. pipecat/processors/dtmf_aggregator.py +0 -2
  29. pipecat/processors/filters/stt_mute_filter.py +0 -2
  30. pipecat/processors/frame_processor.py +18 -11
  31. pipecat/processors/frameworks/rtvi.py +17 -10
  32. pipecat/processors/metrics/sentry.py +2 -0
  33. pipecat/runner/daily.py +137 -36
  34. pipecat/runner/run.py +1 -1
  35. pipecat/runner/utils.py +7 -7
  36. pipecat/serializers/asterisk.py +20 -4
  37. pipecat/serializers/exotel.py +1 -1
  38. pipecat/serializers/plivo.py +1 -1
  39. pipecat/serializers/telnyx.py +1 -1
  40. pipecat/serializers/twilio.py +1 -1
  41. pipecat/services/__init__.py +2 -2
  42. pipecat/services/anthropic/llm.py +113 -28
  43. pipecat/services/asyncai/tts.py +4 -0
  44. pipecat/services/aws/llm.py +82 -8
  45. pipecat/services/aws/tts.py +0 -10
  46. pipecat/services/aws_nova_sonic/aws.py +5 -0
  47. pipecat/services/cartesia/tts.py +28 -16
  48. pipecat/services/cerebras/llm.py +15 -10
  49. pipecat/services/deepgram/stt.py +8 -0
  50. pipecat/services/deepseek/llm.py +13 -8
  51. pipecat/services/fireworks/llm.py +13 -8
  52. pipecat/services/fish/tts.py +8 -6
  53. pipecat/services/gemini_multimodal_live/gemini.py +5 -0
  54. pipecat/services/gladia/config.py +7 -1
  55. pipecat/services/gladia/stt.py +23 -15
  56. pipecat/services/google/llm.py +159 -59
  57. pipecat/services/google/llm_openai.py +18 -3
  58. pipecat/services/grok/llm.py +2 -1
  59. pipecat/services/llm_service.py +38 -3
  60. pipecat/services/mem0/memory.py +2 -1
  61. pipecat/services/mistral/llm.py +5 -6
  62. pipecat/services/nim/llm.py +2 -1
  63. pipecat/services/openai/base_llm.py +88 -26
  64. pipecat/services/openai/image.py +6 -1
  65. pipecat/services/openai_realtime_beta/openai.py +5 -2
  66. pipecat/services/openpipe/llm.py +6 -8
  67. pipecat/services/perplexity/llm.py +13 -8
  68. pipecat/services/playht/tts.py +9 -6
  69. pipecat/services/rime/tts.py +1 -1
  70. pipecat/services/sambanova/llm.py +18 -13
  71. pipecat/services/sarvam/tts.py +415 -10
  72. pipecat/services/speechmatics/stt.py +2 -2
  73. pipecat/services/tavus/video.py +1 -1
  74. pipecat/services/tts_service.py +15 -5
  75. pipecat/services/vistaar/llm.py +2 -5
  76. pipecat/transports/base_input.py +32 -19
  77. pipecat/transports/base_output.py +39 -5
  78. pipecat/transports/daily/__init__.py +0 -0
  79. pipecat/transports/daily/transport.py +2371 -0
  80. pipecat/transports/daily/utils.py +410 -0
  81. pipecat/transports/livekit/__init__.py +0 -0
  82. pipecat/transports/livekit/transport.py +1042 -0
  83. pipecat/transports/network/fastapi_websocket.py +12 -546
  84. pipecat/transports/network/small_webrtc.py +12 -922
  85. pipecat/transports/network/webrtc_connection.py +9 -595
  86. pipecat/transports/network/websocket_client.py +12 -481
  87. pipecat/transports/network/websocket_server.py +12 -487
  88. pipecat/transports/services/daily.py +9 -2334
  89. pipecat/transports/services/helpers/daily_rest.py +12 -396
  90. pipecat/transports/services/livekit.py +12 -975
  91. pipecat/transports/services/tavus.py +12 -757
  92. pipecat/transports/smallwebrtc/__init__.py +0 -0
  93. pipecat/transports/smallwebrtc/connection.py +612 -0
  94. pipecat/transports/smallwebrtc/transport.py +936 -0
  95. pipecat/transports/tavus/__init__.py +0 -0
  96. pipecat/transports/tavus/transport.py +770 -0
  97. pipecat/transports/websocket/__init__.py +0 -0
  98. pipecat/transports/websocket/client.py +494 -0
  99. pipecat/transports/websocket/fastapi.py +559 -0
  100. pipecat/transports/websocket/server.py +500 -0
  101. pipecat/transports/whatsapp/__init__.py +0 -0
  102. pipecat/transports/whatsapp/api.py +345 -0
  103. pipecat/transports/whatsapp/client.py +364 -0
  104. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/WHEEL +0 -0
  105. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/licenses/LICENSE +0 -0
  106. {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
- JSON string with sanitized message content for logging.
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.warn(
458
- "`FrameProcessor.wait_for_task()` is deprecated. "
459
- "Use `await task` or `await asyncio.wait_for(task, timeout)` instead.",
460
- DeprecationWarning,
461
- stacklevel=2,
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
- await self.cancel_task(self.__input_frame_task)
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 isinstance(frame, OpenAILLMContextFrame) and self._params.user_llm_enabled:
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
- messages = frame.context.messages
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
- self._client_version = []
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 "unknown"
1436
+ version = data.version if data else None
1427
1437
  logger.debug(f"Received client-ready: version {version}")
1428
- if version == "unknown":
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,
@@ -6,6 +6,8 @@
6
6
 
7
7
  """Sentry integration for frame processor metrics."""
8
8
 
9
+ import asyncio
10
+
9
11
  from loguru import logger
10
12
 
11
13
  from pipecat.utils.asyncio.task_manager import BaseTaskManager
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
- # Use room_url and token with DailyTransport
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 Tuple
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.services.helpers.daily_rest import (
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
- async def configure(aiohttp_session: aiohttp.ClientSession) -> Tuple[str, str]:
44
- """Configure Daily room URL and token from environment variables.
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
- Tuple containing the room URL and authentication token.
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
- # Check for existing room URL
68
- existing_room_url = os.getenv("DAILY_SAMPLE_ROOM_URL")
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
- if existing_room_url:
77
- # Use existing room
78
- print(f"Using existing Daily room: {existing_room_url}")
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
- else:
81
- # Create a new temporary room
82
- room_name = f"pipecat-{uuid.uuid4().hex[:8]}"
83
- print(f"Creating new Daily room: {room_name}")
84
-
85
- # Calculate expiration time: current time + 2 hours
86
- expiration_time = time.time() + (2 * 60 * 60) # 2 hours from now
87
-
88
- # Create room properties with absolute timestamp
89
- room_properties = DailyRoomProperties(
90
- exp=expiration_time, # Absolute Unix timestamp
91
- eject_at_room_exp=True,
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
- # Create room parameters
95
- room_params = DailyRoomParams(name=room_name, properties=room_properties)
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
- print(f"Created Daily room: {room_url}")
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
- # Create a meeting token for the room with an expiration 2 hours in the future
102
- expiry_time: float = 2 * 60 * 60
103
- token = await daily_rest_helper.get_token(room_url, expiry_time)
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
- return (room_url, token)
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.warn(
126
- "configure_with_args is deprecated. Use configure() instead.",
127
- DeprecationWarning,
128
- stacklevel=2,
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.network.webrtc_connection import SmallWebRTCConnection
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.network.small_webrtc import SmallWebRTCTransport
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.services.daily import DailyTransport
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.services.daily import DailyTransport
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.services.daily import DailyTransport
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.network.fastapi_websocket import FastAPIWebsocketTransport
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.services.daily import DailyTransport
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.network.small_webrtc import SmallWebRTCTransport
497
+ from pipecat.transports.smallwebrtc.transport import SmallWebRTCTransport
498
498
 
499
499
  return SmallWebRTCTransport(
500
500
  params=params,
@@ -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
- if enc == "pcmu":
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 enc == "pcm16":
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:
@@ -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,
@@ -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,
@@ -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
  )
@@ -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,
@@ -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(