From a5c5e069babb833eaf3ebb594e926d50f8f0c926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Thu, 28 Aug 2025 15:29:20 -0700 Subject: [PATCH 1/6] move pipecat.frames.frames.KeypadEntry to pipecat.audio.dtmf.types.KeypadEntry --- CHANGELOG.md | 12 +++ src/pipecat/audio/dtmf/__init__.py | 0 src/pipecat/audio/dtmf/types.py | 47 ++++++++++++ src/pipecat/frames/frames.py | 73 +++++++++++++------ .../processors/aggregators/dtmf_aggregator.py | 2 +- src/pipecat/serializers/exotel.py | 2 +- src/pipecat/serializers/plivo.py | 2 +- src/pipecat/serializers/telnyx.py | 2 +- src/pipecat/serializers/twilio.py | 2 +- tests/test_dtmf_aggregator.py | 2 +- 10 files changed, 114 insertions(+), 30 deletions(-) create mode 100644 src/pipecat/audio/dtmf/__init__.py create mode 100644 src/pipecat/audio/dtmf/types.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 230e9345a..3cdaea355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to **Pipecat** will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +## Changed + +- `pipecat.frames.frames.KeypadEntry` is deprecated and has been moved to + `pipecat.audio.dtmf.types.KeypadEntry`. + +## Deprecated + +- `pipecat.frames.frames.KeypadEntry` is deprecated use + `pipecat.audio.dtmf.types.KeypadEntry` instead. + ## [0.0.82] - 2025-08-28 ### Added diff --git a/src/pipecat/audio/dtmf/__init__.py b/src/pipecat/audio/dtmf/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/pipecat/audio/dtmf/types.py b/src/pipecat/audio/dtmf/types.py new file mode 100644 index 000000000..2de5ae917 --- /dev/null +++ b/src/pipecat/audio/dtmf/types.py @@ -0,0 +1,47 @@ +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""This module defines generic type for DTMS. + +It defines the `KeypadEntry` enumeration, representing dual-tone multi-frequency +(DTMF) keypad entries for phone system integration. Each entry corresponds to a +key on the telephone keypad, facilitating the handling of input in +telecommunication applications. +""" + +from enum import Enum + + +class KeypadEntry(str, Enum): + """DTMF keypad entries for phone system integration. + + Parameters: + ONE: Number key 1. + TWO: Number key 2. + THREE: Number key 3. + FOUR: Number key 4. + FIVE: Number key 5. + SIX: Number key 6. + SEVEN: Number key 7. + EIGHT: Number key 8. + NINE: Number key 9. + ZERO: Number key 0. + POUND: Pound/hash key (#). + STAR: Star/asterisk key (*). + """ + + ONE = "1" + TWO = "2" + THREE = "3" + FOUR = "4" + FIVE = "5" + SIX = "6" + SEVEN = "7" + EIGHT = "8" + NINE = "9" + ZERO = "0" + + POUND = "#" + STAR = "*" diff --git a/src/pipecat/frames/frames.py b/src/pipecat/frames/frames.py index 484ef36b5..69e79a573 100644 --- a/src/pipecat/frames/frames.py +++ b/src/pipecat/frames/frames.py @@ -12,7 +12,6 @@ and LLM processing. """ from dataclasses import dataclass, field -from enum import Enum from typing import ( TYPE_CHECKING, Any, @@ -28,6 +27,7 @@ from typing import ( ) from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.audio.dtmf.types import KeypadEntry as NewKeypadEntry from pipecat.audio.interruptions.base_interruption_strategy import BaseInterruptionStrategy from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams from pipecat.audio.vad.vad_analyzer import VADParams @@ -41,9 +41,13 @@ if TYPE_CHECKING: from pipecat.processors.frame_processor import FrameProcessor -class KeypadEntry(str, Enum): +class DeprecatedKeypadEntry: """DTMF keypad entries for phone system integration. + .. deprecated:: 0.0.82 + This class is deprecated and will be removed in a future version. + Instead, use `audio.dtmf.types.KeypadEntry`. + Parameters: ONE: Number key 1. TWO: Number key 2. @@ -59,18 +63,38 @@ class KeypadEntry(str, Enum): STAR: Star/asterisk key (*). """ - ONE = "1" - TWO = "2" - THREE = "3" - FOUR = "4" - FIVE = "5" - SIX = "6" - SEVEN = "7" - EIGHT = "8" - NINE = "9" - ZERO = "0" - POUND = "#" - STAR = "*" + _enum = NewKeypadEntry + + @classmethod + def _warn(cls): + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "`pipecat.frames.frames.KeypadEntry` is deprecated and will be removed in a future version. " + "Use `pipecat.audio.dtmf.types.KeypadEntry` instead.", + DeprecationWarning, + stacklevel=2, + ) + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + """Allow the instance to be called as a function.""" + self._warn() + return self._enum(*args, **kwargs) + + def __getattr__(self, name): + """Retrieve an attribute from the underlying enum.""" + self._warn() + return getattr(self._enum, name) + + def __getitem__(self, name): + """Retrieve an item from the underlying enum.""" + self._warn() + return self._enum[name] + + +KeypadEntry = DeprecatedKeypadEntry() def format_pts(pts: Optional[int]): @@ -526,15 +550,16 @@ class LLMMessagesFrame(DataFrame): super().__post_init__() import warnings - warnings.simplefilter("always") - warnings.warn( - "LLMMessagesFrame is deprecated and will be removed in a future version. " - "Instead, use either " - "`LLMMessagesUpdateFrame` with `run_llm=True`, or " - "`OpenAILLMContextFrame` with desired messages in a new context", - DeprecationWarning, - stacklevel=2, - ) + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "LLMMessagesFrame is deprecated and will be removed in a future version. " + "Instead, use either " + "`LLMMessagesUpdateFrame` with `run_llm=True`, or " + "`OpenAILLMContextFrame` with desired messages in a new context", + DeprecationWarning, + stacklevel=2, + ) @dataclass @@ -668,7 +693,7 @@ class DTMFFrame: button: The DTMF keypad entry that was pressed. """ - button: KeypadEntry + button: NewKeypadEntry @dataclass diff --git a/src/pipecat/processors/aggregators/dtmf_aggregator.py b/src/pipecat/processors/aggregators/dtmf_aggregator.py index 38e1296f6..c50b0d8a8 100644 --- a/src/pipecat/processors/aggregators/dtmf_aggregator.py +++ b/src/pipecat/processors/aggregators/dtmf_aggregator.py @@ -14,13 +14,13 @@ for downstream processing by LLM context aggregators. import asyncio from typing import Optional +from pipecat.audio.dtmf.types import KeypadEntry from pipecat.frames.frames import ( BotInterruptionFrame, CancelFrame, EndFrame, Frame, InputDTMFFrame, - KeypadEntry, StartFrame, TranscriptionFrame, ) diff --git a/src/pipecat/serializers/exotel.py b/src/pipecat/serializers/exotel.py index 3fd2fb92d..9ed342631 100644 --- a/src/pipecat/serializers/exotel.py +++ b/src/pipecat/serializers/exotel.py @@ -13,13 +13,13 @@ from typing import Optional from loguru import logger from pydantic import BaseModel +from pipecat.audio.dtmf.types import KeypadEntry from pipecat.audio.utils import create_stream_resampler from pipecat.frames.frames import ( AudioRawFrame, Frame, InputAudioRawFrame, InputDTMFFrame, - KeypadEntry, StartFrame, StartInterruptionFrame, TransportMessageFrame, diff --git a/src/pipecat/serializers/plivo.py b/src/pipecat/serializers/plivo.py index 11855d850..aa8b4b27e 100644 --- a/src/pipecat/serializers/plivo.py +++ b/src/pipecat/serializers/plivo.py @@ -13,6 +13,7 @@ from typing import Optional from loguru import logger from pydantic import BaseModel +from pipecat.audio.dtmf.types import KeypadEntry from pipecat.audio.utils import create_stream_resampler, pcm_to_ulaw, ulaw_to_pcm from pipecat.frames.frames import ( AudioRawFrame, @@ -21,7 +22,6 @@ from pipecat.frames.frames import ( Frame, InputAudioRawFrame, InputDTMFFrame, - KeypadEntry, StartFrame, StartInterruptionFrame, TransportMessageFrame, diff --git a/src/pipecat/serializers/telnyx.py b/src/pipecat/serializers/telnyx.py index bf8d5d69f..467c01ba2 100644 --- a/src/pipecat/serializers/telnyx.py +++ b/src/pipecat/serializers/telnyx.py @@ -14,6 +14,7 @@ import aiohttp from loguru import logger from pydantic import BaseModel +from pipecat.audio.dtmf.types import KeypadEntry from pipecat.audio.utils import ( alaw_to_pcm, create_stream_resampler, @@ -28,7 +29,6 @@ from pipecat.frames.frames import ( Frame, InputAudioRawFrame, InputDTMFFrame, - KeypadEntry, StartFrame, StartInterruptionFrame, ) diff --git a/src/pipecat/serializers/twilio.py b/src/pipecat/serializers/twilio.py index 7679e8721..57e7c8dba 100644 --- a/src/pipecat/serializers/twilio.py +++ b/src/pipecat/serializers/twilio.py @@ -13,6 +13,7 @@ from typing import Optional from loguru import logger from pydantic import BaseModel +from pipecat.audio.dtmf.types import KeypadEntry from pipecat.audio.utils import create_stream_resampler, pcm_to_ulaw, ulaw_to_pcm from pipecat.frames.frames import ( AudioRawFrame, @@ -21,7 +22,6 @@ from pipecat.frames.frames import ( Frame, InputAudioRawFrame, InputDTMFFrame, - KeypadEntry, StartFrame, StartInterruptionFrame, TransportMessageFrame, diff --git a/tests/test_dtmf_aggregator.py b/tests/test_dtmf_aggregator.py index 697d50d27..5d9d1346a 100644 --- a/tests/test_dtmf_aggregator.py +++ b/tests/test_dtmf_aggregator.py @@ -6,10 +6,10 @@ import unittest +from pipecat.audio.dtmf.types import KeypadEntry from pipecat.frames.frames import ( EndFrame, InputDTMFFrame, - KeypadEntry, TranscriptionFrame, ) from pipecat.processors.aggregators.dtmf_aggregator import DTMFAggregator From 79be0695ddcb6b71d888344ed5efb1e52ae5e0f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Thu, 28 Aug 2025 15:30:23 -0700 Subject: [PATCH 2/6] make sure warnings are always displayed --- src/pipecat/audio/utils.py | 16 ++++--- .../processors/aggregators/llm_response.py | 47 ++++++++++++------- src/pipecat/processors/frame_processor.py | 14 +++--- src/pipecat/runner/daily.py | 12 +++-- src/pipecat/services/__init__.py | 4 +- src/pipecat/services/cartesia/tts.py | 24 ++++++---- src/pipecat/services/fish/tts.py | 14 +++--- src/pipecat/services/gladia/stt.py | 31 +++++++----- src/pipecat/services/google/llm_openai.py | 14 +++--- src/pipecat/services/playht/tts.py | 15 +++--- src/pipecat/services/sarvam/tts.py | 15 +++--- src/pipecat/services/speechmatics/stt.py | 3 +- src/pipecat/services/tts_service.py | 12 +++-- 13 files changed, 130 insertions(+), 91 deletions(-) diff --git a/src/pipecat/audio/utils.py b/src/pipecat/audio/utils.py index 84f73ebad..41aecf907 100644 --- a/src/pipecat/audio/utils.py +++ b/src/pipecat/audio/utils.py @@ -41,13 +41,15 @@ def create_default_resampler(**kwargs) -> BaseAudioResampler: """ import warnings - warnings.warn( - "`create_default_resampler` is deprecated. " - "Use `create_stream_resampler` for real-time processing scenarios or " - "`create_file_resampler` for batch processing of complete audio files.", - DeprecationWarning, - stacklevel=2, - ) + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "`create_default_resampler` is deprecated. " + "Use `create_stream_resampler` for real-time processing scenarios or " + "`create_file_resampler` for batch processing of complete audio files.", + DeprecationWarning, + stacklevel=2, + ) return SOXRAudioResampler(**kwargs) diff --git a/src/pipecat/processors/aggregators/llm_response.py b/src/pipecat/processors/aggregators/llm_response.py index 485ffe6aa..d058a4334 100644 --- a/src/pipecat/processors/aggregators/llm_response.py +++ b/src/pipecat/processors/aggregators/llm_response.py @@ -12,7 +12,6 @@ LLM processing, and text-to-speech components in conversational AI pipelines. """ import asyncio -import warnings from abc import abstractmethod from dataclasses import dataclass from typing import Dict, List, Literal, Optional, Set @@ -326,11 +325,15 @@ class LLMContextResponseAggregator(BaseLLMResponseAggregator): Returns: LLMContextFrame containing the current context. """ - warnings.warn( - "get_context_frame() is deprecated and will be removed in a future version. To trigger an LLM response, use LLMRunFrame instead.", - DeprecationWarning, - stacklevel=2, - ) + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "get_context_frame() is deprecated and will be removed in a future version. To trigger an LLM response, use LLMRunFrame instead.", + DeprecationWarning, + stacklevel=2, + ) return self._get_context_frame() def _get_context_frame(self) -> OpenAILLMContextFrame: @@ -1035,12 +1038,16 @@ class LLMUserResponseAggregator(LLMUserContextAggregator): params: Configuration parameters for aggregation behavior. **kwargs: Additional arguments passed to parent class. """ - warnings.warn( - "LLMUserResponseAggregator is deprecated and will be removed in a future version. " - "Use LLMUserContextAggregator or another LLM-specific subclass instead.", - DeprecationWarning, - stacklevel=2, - ) + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "LLMUserResponseAggregator is deprecated and will be removed in a future version. " + "Use LLMUserContextAggregator or another LLM-specific subclass instead.", + DeprecationWarning, + stacklevel=2, + ) super().__init__(context=OpenAILLMContext(messages), params=params, **kwargs) async def _process_aggregation(self): @@ -1078,12 +1085,16 @@ class LLMAssistantResponseAggregator(LLMAssistantContextAggregator): params: Configuration parameters for aggregation behavior. **kwargs: Additional arguments passed to parent class. """ - warnings.warn( - "LLMAssistantResponseAggregator is deprecated and will be removed in a future version. " - "Use LLMAssistantContextAggregator or another LLM-specific subclass instead.", - DeprecationWarning, - stacklevel=2, - ) + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "LLMAssistantResponseAggregator is deprecated and will be removed in a future version. " + "Use LLMAssistantContextAggregator or another LLM-specific subclass instead.", + DeprecationWarning, + stacklevel=2, + ) super().__init__(context=OpenAILLMContext(messages), params=params, **kwargs) async def push_aggregation(self): diff --git a/src/pipecat/processors/frame_processor.py b/src/pipecat/processors/frame_processor.py index ab82f7aa1..b20b655f7 100644 --- a/src/pipecat/processors/frame_processor.py +++ b/src/pipecat/processors/frame_processor.py @@ -452,12 +452,14 @@ class FrameProcessor(BaseObject): """ import warnings - warnings.warn( - "`FrameProcessor.wait_for_task()` is deprecated. " - "Use `await task` or `await asyncio.wait_for(task, timeout)` instead.", - DeprecationWarning, - stacklevel=2, - ) + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "`FrameProcessor.wait_for_task()` is deprecated. " + "Use `await task` or `await asyncio.wait_for(task, timeout)` instead.", + DeprecationWarning, + stacklevel=2, + ) if timeout: await asyncio.wait_for(task, timeout) diff --git a/src/pipecat/runner/daily.py b/src/pipecat/runner/daily.py index 33e3d8066..397a08cc2 100644 --- a/src/pipecat/runner/daily.py +++ b/src/pipecat/runner/daily.py @@ -122,11 +122,13 @@ async def configure_with_args(aiohttp_session: aiohttp.ClientSession, parser=Non """ import warnings - warnings.warn( - "configure_with_args is deprecated. Use configure() instead.", - DeprecationWarning, - stacklevel=2, - ) + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "configure_with_args is deprecated. Use configure() instead.", + DeprecationWarning, + stacklevel=2, + ) room_url, token = await configure(aiohttp_session) return (room_url, token, None) diff --git a/src/pipecat/services/__init__.py b/src/pipecat/services/__init__.py index 0df8d028f..c28257891 100644 --- a/src/pipecat/services/__init__.py +++ b/src/pipecat/services/__init__.py @@ -11,11 +11,11 @@ _warned_modules = set() def _warn_deprecated_access(globals: Dict[str, Any], attr, old: str, new: str): - import warnings - # Only warn once per old->new module pair module_key = (old, new) if module_key not in _warned_modules: + import warnings + with warnings.catch_warnings(): warnings.simplefilter("always") warnings.warn( diff --git a/src/pipecat/services/cartesia/tts.py b/src/pipecat/services/cartesia/tts.py index f8f148a38..5efda600c 100644 --- a/src/pipecat/services/cartesia/tts.py +++ b/src/pipecat/services/cartesia/tts.py @@ -271,11 +271,13 @@ class CartesiaTTSService(AudioContextWordTTSService): voice_config["id"] = self._voice_id if self._settings["emotion"]: - warnings.warn( - "The 'emotion' parameter in __experimental_controls is deprecated and will be removed in a future version.", - DeprecationWarning, - stacklevel=2, - ) + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "The 'emotion' parameter in __experimental_controls is deprecated and will be removed in a future version.", + DeprecationWarning, + stacklevel=2, + ) voice_config["__experimental_controls"] = {} if self._settings["emotion"]: voice_config["__experimental_controls"]["emotion"] = self._settings["emotion"] @@ -606,11 +608,13 @@ class CartesiaHttpTTSService(TTSService): voice_config = {"mode": "id", "id": self._voice_id} if self._settings["emotion"]: - warnings.warn( - "The 'emotion' parameter in voice.__experimental_controls is deprecated and will be removed in a future version.", - DeprecationWarning, - stacklevel=2, - ) + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "The 'emotion' parameter in voice.__experimental_controls is deprecated and will be removed in a future version.", + DeprecationWarning, + stacklevel=2, + ) voice_config["__experimental_controls"] = {"emotion": self._settings["emotion"]} await self.start_ttfb_metrics() diff --git a/src/pipecat/services/fish/tts.py b/src/pipecat/services/fish/tts.py index 53f6cb171..305c14884 100644 --- a/src/pipecat/services/fish/tts.py +++ b/src/pipecat/services/fish/tts.py @@ -120,12 +120,14 @@ class FishAudioTTSService(InterruptibleTTSService): if model: import warnings - warnings.warn( - "Parameter 'model' is deprecated and will be removed in a future version. " - "Use 'reference_id' instead.", - DeprecationWarning, - stacklevel=2, - ) + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "Parameter 'model' is deprecated and will be removed in a future version. " + "Use 'reference_id' instead.", + DeprecationWarning, + stacklevel=2, + ) reference_id = model self._api_key = api_key diff --git a/src/pipecat/services/gladia/stt.py b/src/pipecat/services/gladia/stt.py index 316edd91c..ca3600643 100644 --- a/src/pipecat/services/gladia/stt.py +++ b/src/pipecat/services/gladia/stt.py @@ -13,7 +13,6 @@ supporting multiple languages, custom vocabulary, and various audio processing o import asyncio import base64 import json -import warnings from typing import Any, AsyncGenerator, Dict, Literal, Optional import aiohttp @@ -173,12 +172,16 @@ class _InputParamsDescriptor: """Descriptor for backward compatibility with deprecation warning.""" def __get__(self, obj, objtype=None): - warnings.warn( - "GladiaSTTService.InputParams is deprecated and will be removed in a future version. " - "Import and use GladiaInputParams directly instead.", - DeprecationWarning, - stacklevel=2, - ) + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "GladiaSTTService.InputParams is deprecated and will be removed in a future version. " + "Import and use GladiaInputParams directly instead.", + DeprecationWarning, + stacklevel=2, + ) return GladiaInputParams @@ -234,12 +237,14 @@ class GladiaSTTService(STTService): # Warn about deprecated language parameter if it's used if params.language is not None: - warnings.warn( - "The 'language' parameter is deprecated and will be removed in a future version. " - "Use 'language_config' instead.", - DeprecationWarning, - stacklevel=2, - ) + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "The 'language' parameter is deprecated and will be removed in a future version. " + "Use 'language_config' instead.", + DeprecationWarning, + stacklevel=2, + ) self._api_key = api_key self._region = region diff --git a/src/pipecat/services/google/llm_openai.py b/src/pipecat/services/google/llm_openai.py index 2c64f050f..83d90e4ac 100644 --- a/src/pipecat/services/google/llm_openai.py +++ b/src/pipecat/services/google/llm_openai.py @@ -65,12 +65,14 @@ class GoogleLLMOpenAIBetaService(OpenAILLMService): """ import warnings - warnings.warn( - "GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version. " - "Use GoogleLLMService instead for better integration with Google's native API.", - DeprecationWarning, - stacklevel=2, - ) + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version. " + "Use GoogleLLMService instead for better integration with Google's native API.", + DeprecationWarning, + stacklevel=2, + ) super().__init__(api_key=api_key, base_url=base_url, model=model, **kwargs) diff --git a/src/pipecat/services/playht/tts.py b/src/pipecat/services/playht/tts.py index 3c03e8335..aa92df055 100644 --- a/src/pipecat/services/playht/tts.py +++ b/src/pipecat/services/playht/tts.py @@ -14,7 +14,6 @@ import io import json import struct import uuid -import warnings from typing import AsyncGenerator, Optional import aiohttp @@ -455,11 +454,15 @@ class PlayHTHttpTTSService(TTSService): # Warn about deprecated protocol parameter if explicitly provided if protocol: - warnings.warn( - "The 'protocol' parameter is deprecated and will be removed in a future version.", - DeprecationWarning, - stacklevel=2, - ) + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "The 'protocol' parameter is deprecated and will be removed in a future version.", + DeprecationWarning, + stacklevel=2, + ) params = params or PlayHTHttpTTSService.InputParams() diff --git a/src/pipecat/services/sarvam/tts.py b/src/pipecat/services/sarvam/tts.py index 642113158..b7579b26b 100644 --- a/src/pipecat/services/sarvam/tts.py +++ b/src/pipecat/services/sarvam/tts.py @@ -9,7 +9,6 @@ import asyncio import base64 import json -import warnings from typing import Any, AsyncGenerator, Mapping, Optional import aiohttp @@ -356,11 +355,15 @@ class SarvamTTSService(InterruptibleTTSService): ) params = params or SarvamTTSService.InputParams() if aiohttp_session is not None: - warnings.warn( - "The 'aiohttp_session' parameter is deprecated and will be removed in a future version. ", - DeprecationWarning, - stacklevel=2, - ) + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "The 'aiohttp_session' parameter is deprecated and will be removed in a future version. ", + DeprecationWarning, + stacklevel=2, + ) # WebSocket endpoint URL self._websocket_url = f"{url}?model={model}" self._api_key = api_key diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index 7ac4c6f2a..7136bbb0c 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -10,7 +10,6 @@ import asyncio import datetime import os import re -import warnings from dataclasses import dataclass, field from enum import Enum from typing import Any, AsyncGenerator @@ -1107,6 +1106,8 @@ def _check_deprecated_args(kwargs: dict, params: SpeechmaticsSTTService.InputPar # Show deprecation warnings def _deprecation_warning(old: str, new: str | None = None): + import warnings + with warnings.catch_warnings(): warnings.simplefilter("always") if new: diff --git a/src/pipecat/services/tts_service.py b/src/pipecat/services/tts_service.py index 9e34adf25..93800338b 100644 --- a/src/pipecat/services/tts_service.py +++ b/src/pipecat/services/tts_service.py @@ -276,11 +276,13 @@ class TTSService(AIService): """ import warnings - warnings.warn( - "`TTSService.say()` is deprecated. Push a `TTSSpeakFrame` instead.", - DeprecationWarning, - stacklevel=2, - ) + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "`TTSService.say()` is deprecated. Push a `TTSSpeakFrame` instead.", + DeprecationWarning, + stacklevel=2, + ) await self.queue_frame(TTSSpeakFrame(text)) From 5787743ab341e5c6b8b8e5f9a74ca93d41e4ae28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Thu, 28 Aug 2025 15:32:30 -0700 Subject: [PATCH 3/6] audio(dtmf): added DTMF audio files and load_dtmf_audio() --- pyproject.toml | 15 ++++++ src/pipecat/audio/dtmf/dtmf-0.wav | Bin 0 -> 4078 bytes src/pipecat/audio/dtmf/dtmf-1.wav | Bin 0 -> 4078 bytes src/pipecat/audio/dtmf/dtmf-2.wav | Bin 0 -> 4078 bytes src/pipecat/audio/dtmf/dtmf-3.wav | Bin 0 -> 4078 bytes src/pipecat/audio/dtmf/dtmf-4.wav | Bin 0 -> 4078 bytes src/pipecat/audio/dtmf/dtmf-5.wav | Bin 0 -> 4078 bytes src/pipecat/audio/dtmf/dtmf-6.wav | Bin 0 -> 4078 bytes src/pipecat/audio/dtmf/dtmf-7.wav | Bin 0 -> 4078 bytes src/pipecat/audio/dtmf/dtmf-8.wav | Bin 0 -> 4078 bytes src/pipecat/audio/dtmf/dtmf-9.wav | Bin 0 -> 4078 bytes src/pipecat/audio/dtmf/dtmf-pound.wav | Bin 0 -> 4078 bytes src/pipecat/audio/dtmf/dtmf-star.wav | Bin 0 -> 4078 bytes src/pipecat/audio/dtmf/utils.py | 70 ++++++++++++++++++++++++++ uv.lock | 2 + 15 files changed, 87 insertions(+) create mode 100644 src/pipecat/audio/dtmf/dtmf-0.wav create mode 100644 src/pipecat/audio/dtmf/dtmf-1.wav create mode 100644 src/pipecat/audio/dtmf/dtmf-2.wav create mode 100644 src/pipecat/audio/dtmf/dtmf-3.wav create mode 100644 src/pipecat/audio/dtmf/dtmf-4.wav create mode 100644 src/pipecat/audio/dtmf/dtmf-5.wav create mode 100644 src/pipecat/audio/dtmf/dtmf-6.wav create mode 100644 src/pipecat/audio/dtmf/dtmf-7.wav create mode 100644 src/pipecat/audio/dtmf/dtmf-8.wav create mode 100644 src/pipecat/audio/dtmf/dtmf-9.wav create mode 100644 src/pipecat/audio/dtmf/dtmf-pound.wav create mode 100644 src/pipecat/audio/dtmf/dtmf-star.wav create mode 100644 src/pipecat/audio/dtmf/utils.py diff --git a/pyproject.toml b/pyproject.toml index 61e3c1a87..35b4c5664 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Artificial Intelligence" ] dependencies = [ + "aiofiles>=24.1.0,<25", "aiohttp>=3.11.12,<4", "audioop-lts~=0.2.1; python_version>='3.13'", "docstring_parser~=0.16", @@ -137,6 +138,20 @@ where = ["src"] [tool.setuptools.package-data] "pipecat" = ["py.typed"] +"pipecat.audio.dtmf" = [ + "src/pipecat/audio/dtmf/dtmf-0.wav", + "src/pipecat/audio/dtmf/dtmf-1.wav", + "src/pipecat/audio/dtmf/dtmf-2.wav", + "src/pipecat/audio/dtmf/dtmf-3.wav", + "src/pipecat/audio/dtmf/dtmf-4.wav", + "src/pipecat/audio/dtmf/dtmf-5.wav", + "src/pipecat/audio/dtmf/dtmf-6.wav", + "src/pipecat/audio/dtmf/dtmf-7.wav", + "src/pipecat/audio/dtmf/dtmf-8.wav", + "src/pipecat/audio/dtmf/dtmf-9.wav", + "src/pipecat/audio/dtmf/dtmf-pound.wav", + "src/pipecat/audio/dtmf/dtmf-star.wav", +] "pipecat.services.aws_nova_sonic" = ["src/pipecat/services/aws_nova_sonic/ready.wav"] [tool.pytest.ini_options] diff --git a/src/pipecat/audio/dtmf/dtmf-0.wav b/src/pipecat/audio/dtmf/dtmf-0.wav new file mode 100644 index 0000000000000000000000000000000000000000..553eb49748b9a8c23d0bcd5430b6f1124847248c GIT binary patch literal 4078 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 P{1`tn@FN5NKLh^@x5`V? literal 0 HcmV?d00001 diff --git a/src/pipecat/audio/dtmf/dtmf-1.wav b/src/pipecat/audio/dtmf/dtmf-1.wav new file mode 100644 index 0000000000000000000000000000000000000000..ac852c45155648f205768fa6520e820b559be6ec GIT binary patch literal 4078 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 JBJlqs@PD1pROJ8w literal 0 HcmV?d00001 diff --git a/src/pipecat/audio/dtmf/dtmf-3.wav b/src/pipecat/audio/dtmf/dtmf-3.wav new file mode 100644 index 0000000000000000000000000000000000000000..141a7bcf1da6fc2fc22023c706a8694c3647361f GIT binary patch literal 4078 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 Jz`)-#@V`44W3vDN literal 0 HcmV?d00001 diff --git a/src/pipecat/audio/dtmf/dtmf-4.wav b/src/pipecat/audio/dtmf/dtmf-4.wav new file mode 100644 index 0000000000000000000000000000000000000000..59b0cb0294262f0b59fc676d6b7d56e69713b992 GIT binary patch literal 4078 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 HuN3$vKDjuM literal 0 HcmV?d00001 diff --git a/src/pipecat/audio/dtmf/dtmf-6.wav b/src/pipecat/audio/dtmf/dtmf-6.wav new file mode 100644 index 0000000000000000000000000000000000000000..27f42571b369bf1e064950a7e2d0b6acef6caf57 GIT binary patch literal 4078 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 SZ)v=uzdm0R_?p1KBJlqOnm%d( literal 0 HcmV?d00001 diff --git a/src/pipecat/audio/dtmf/dtmf-7.wav b/src/pipecat/audio/dtmf/dtmf-7.wav new file mode 100644 index 0000000000000000000000000000000000000000..39769f832c8df6be9390bd948c677b5528e773f0 GIT binary patch literal 4078 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 VmG2RGr&V!v6RC;9xROY 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 K-w^oc1pXg-)kz5e literal 0 HcmV?d00001 diff --git a/src/pipecat/audio/dtmf/dtmf-9.wav b/src/pipecat/audio/dtmf/dtmf-9.wav new file mode 100644 index 0000000000000000000000000000000000000000..43ceb9cb75c5ac8236c261054f660cdde20f7988 GIT binary patch literal 4078 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 L_<@1{pMn1a`RzZq literal 0 HcmV?d00001 diff --git a/src/pipecat/audio/dtmf/dtmf-star.wav b/src/pipecat/audio/dtmf/dtmf-star.wav new file mode 100644 index 0000000000000000000000000000000000000000..fa9fdc63a7322d7f34068ffdb0af321b859cabec GIT binary patch literal 4078 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 M4uS6w`2P|3U(#hf9RL6T literal 0 HcmV?d00001 diff --git a/src/pipecat/audio/dtmf/utils.py b/src/pipecat/audio/dtmf/utils.py new file mode 100644 index 000000000..21855b73c --- /dev/null +++ b/src/pipecat/audio/dtmf/utils.py @@ -0,0 +1,70 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""DTMF audio utilities. + +This module provides functionality to load DTMF (Dual-Tone Multi-Frequency) +audio files corresponding to phone keypad entries. Audio data is cached +in-memory after first load to improve performance on subsequent accesses. +""" + +import asyncio +import io +import wave +from importlib.resources import files +from typing import Dict, Optional + +import aiofiles + +from pipecat.audio.dtmf.types import KeypadEntry +from pipecat.audio.resamplers.base_audio_resampler import BaseAudioResampler +from pipecat.audio.utils import create_file_resampler + +__DTMF_LOCK__ = asyncio.Lock() +__DTMF_AUDIO__: Dict[KeypadEntry, bytes] = {} +__DTMF_RESAMPLER__: Optional[BaseAudioResampler] = None + +__DTMF_FILE_NAME = { + KeypadEntry.POUND: "dtmf-pound.wav", + KeypadEntry.STAR: "dtmf-star.wav", +} + + +async def load_dtmf_audio(button: KeypadEntry, *, sample_rate: int = 8000) -> bytes: + """Load audio for DTMF tones associated with the given button. + + Args: + button (KeypadEntry): The button for which the DTMF audio is to be loaded. + sample_rate (int, optional): The sample rate for the audio. Defaults to 8000. + + Returns: + bytes: The audio data for the DTMF tone as bytes. + """ + global __DTMF_AUDIO__, __DTMF_RESAMPLER__ + + async with __DTMF_LOCK__: + if button in __DTMF_AUDIO__: + return __DTMF_AUDIO__[button] + + if not __DTMF_RESAMPLER__: + __DTMF_RESAMPLER__ = create_file_resampler() + + dtmf_file_name = __DTMF_FILE_NAME.get(button, f"dtmf-{button.value}.wav") + dtmf_file_path = files("pipecat.audio.dtmf").joinpath(dtmf_file_name) + + async with aiofiles.open(dtmf_file_path, "rb") as f: + data = await f.read() + + with io.BytesIO(data) as buffer: + with wave.open(buffer, "rb") as wf: + audio = wf.readframes(wf.getnframes()) + in_sample_rate = wf.getframerate() + resampled_audio = await __DTMF_RESAMPLER__.resample( + audio, in_sample_rate, sample_rate + ) + __DTMF_AUDIO__[button] = resampled_audio + + return __DTMF_AUDIO__[button] diff --git a/uv.lock b/uv.lock index 1cc52c997..c5a225107 100644 --- a/uv.lock +++ b/uv.lock @@ -4189,6 +4189,7 @@ wheels = [ name = "pipecat-ai" source = { editable = "." } dependencies = [ + { name = "aiofiles" }, { name = "aiohttp" }, { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, { name = "docstring-parser" }, @@ -4409,6 +4410,7 @@ docs = [ requires-dist = [ { name = "accelerate", marker = "extra == 'moondream'", specifier = "~=1.10.0" }, { name = "aioboto3", marker = "extra == 'aws'", specifier = "~=15.0.0" }, + { name = "aiofiles", specifier = ">=24.1.0,<25" }, { name = "aiohttp", specifier = ">=3.11.12,<4" }, { name = "aiortc", marker = "extra == 'webrtc'", specifier = "~=1.11.0" }, { name = "anthropic", marker = "extra == 'anthropic'", specifier = "~=0.49.0" }, From 0e01ac8ef682387440ac7c2b0f6c5d7639dce3bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Thu, 28 Aug 2025 15:34:14 -0700 Subject: [PATCH 4/6] BaseOutputTransport: implement generic write_dtmf() --- CHANGELOG.md | 6 ++++++ src/pipecat/transports/base_output.py | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cdaea355..661921f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## Added + +- `BaseOutputTransport` now implements `write_dtmf()` by loading DTMF audio and + sending it through the transport. This makes sending DTMF generic across all + output transports. + ## Changed - `pipecat.frames.frames.KeypadEntry` is deprecated and has been moved to diff --git a/src/pipecat/transports/base_output.py b/src/pipecat/transports/base_output.py index 432908151..53d61e486 100644 --- a/src/pipecat/transports/base_output.py +++ b/src/pipecat/transports/base_output.py @@ -19,6 +19,7 @@ from typing import Any, AsyncGenerator, Dict, List, Mapping, Optional from loguru import logger from PIL import Image +from pipecat.audio.dtmf.utils import load_dtmf_audio from pipecat.audio.mixers.base_audio_mixer import BaseAudioMixer from pipecat.audio.utils import create_stream_resampler, is_silence from pipecat.frames.frames import ( @@ -223,7 +224,12 @@ class BaseOutputTransport(FrameProcessor): Args: frame: The DTMF frame to write. """ - pass + dtmf_audio = await load_dtmf_audio(frame.button, sample_rate=self._sample_rate) + dtmf_audio_frame = OutputAudioRawFrame( + audio=dtmf_audio, sample_rate=self._sample_rate, num_channels=1 + ) + dtmf_audio_frame.transport_destination = frame.transport_destination + await self.write_audio_frame(dtmf_audio_frame) async def send_audio(self, frame: OutputAudioRawFrame): """Send an audio frame downstream. From f03deb6eccd6d2d1a55cb6a55328c39b0e2f3ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Thu, 28 Aug 2025 15:37:41 -0700 Subject: [PATCH 5/6] DailyTransport: remove send_dtmf() and write_dtmf() --- CHANGELOG.md | 7 +++++ src/pipecat/transports/services/daily.py | 36 +----------------------- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 661921f30..5396a1eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `pipecat.frames.frames.KeypadEntry` is deprecated and has been moved to `pipecat.audio.dtmf.types.KeypadEntry`. +## Removed + +- `DailyTransport.write_dtmf()` has been removed in favor of the generic + `BaseOutputTransport.write_dtmf()`. + +- Remove deprecated `DailyTransport.send_dtmf()`. + ## Deprecated - `pipecat.frames.frames.KeypadEntry` is deprecated use diff --git a/src/pipecat/transports/services/daily.py b/src/pipecat/transports/services/daily.py index b8ffe2383..cfc7998ef 100644 --- a/src/pipecat/transports/services/daily.py +++ b/src/pipecat/transports/services/daily.py @@ -31,8 +31,6 @@ from pipecat.frames.frames import ( InputAudioRawFrame, InterimTranscriptionFrame, OutputAudioRawFrame, - OutputDTMFFrame, - OutputDTMFUrgentFrame, OutputImageRawFrame, SpriteFrame, StartFrame, @@ -1676,7 +1674,7 @@ class DailyInputTransport(BaseInputTransport): class DailyOutputTransport(BaseOutputTransport): """Handles outgoing media streams and events to Daily calls. - Manages sending audio, video, DTMF tones, and other data to Daily calls, + Manages sending audio, video and other data to Daily calls, including audio destination registration and message transmission. """ @@ -1783,19 +1781,6 @@ class DailyOutputTransport(BaseOutputTransport): """ await self._client.register_audio_destination(destination) - async def write_dtmf(self, frame: OutputDTMFFrame | OutputDTMFUrgentFrame): - """Write DTMF tones to the call. - - Args: - frame: The DTMF frame containing tone information. - """ - await self._client.send_dtmf( - { - "sessionId": frame.transport_destination, - "tones": frame.button.value, - } - ) - async def write_audio_frame(self, frame: OutputAudioRawFrame): """Write an audio frame to the Daily call. @@ -2022,25 +2007,6 @@ class DailyTransport(BaseTransport): """ await self._client.stop_dialout(participant_id) - async def send_dtmf(self, settings): - """Send DTMF tones during a call (deprecated). - - .. deprecated:: 0.0.69 - Push an `OutputDTMFFrame` or an `OutputDTMFUrgentFrame` instead. - - Args: - settings: DTMF settings including tones and target session. - """ - import warnings - - with warnings.catch_warnings(): - warnings.simplefilter("always") - warnings.warn( - "`DailyTransport.send_dtmf()` is deprecated, push an `OutputDTMFFrame` or an `OutputDTMFUrgentFrame` instead.", - DeprecationWarning, - ) - await self._client.send_dtmf(settings) - async def sip_call_transfer(self, settings): """Transfer a SIP call to another destination. From ea368e4c5fb18ca7baa1eaee940356476210e9b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Thu, 28 Aug 2025 16:10:01 -0700 Subject: [PATCH 6/6] scripts(dtmf): added generate_dtmf.sh to generate DTMF wav files --- scripts/dtmf/generate_dtmf.sh | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100755 scripts/dtmf/generate_dtmf.sh diff --git a/scripts/dtmf/generate_dtmf.sh b/scripts/dtmf/generate_dtmf.sh new file mode 100755 index 000000000..caca1e128 --- /dev/null +++ b/scripts/dtmf/generate_dtmf.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# DTMF frequency map (low, high) +declare -A DTMF=( + [1]="697 1209" + [2]="697 1336" + [3]="697 1477" + [4]="770 1209" + [5]="770 1336" + [6]="770 1477" + [7]="852 1209" + [8]="852 1336" + [9]="852 1477" + ["star"]="941 1209" + [0]="941 1336" + ["pound"]="941 1477" +) + +# Tone duration (seconds) + gap after +DURATION=0.2 +GAP=0.05 +SAMPLERATE=8000 + +for key in "${!DTMF[@]}"; do + freqs=(${DTMF[$key]}) + low=${freqs[0]} + high=${freqs[1]} + echo "Generating DTMF tone for $key ($low Hz + $high Hz)" + ffmpeg -hide_banner -loglevel error -y \ + -f lavfi -i "sine=frequency=$low:duration=$DURATION:sample_rate=$SAMPLERATE" \ + -f lavfi -i "sine=frequency=$high:duration=$DURATION:sample_rate=$SAMPLERATE" \ + -f lavfi -i "anullsrc=r=$SAMPLERATE:cl=mono:d=$GAP" \ + -filter_complex "[0][1]amix=2[a];[a][2]concat=n=2:v=0:a=1[out]" \ + -map "[out]" -c:a pcm_s16le -ar $SAMPLERATE "dtmf-${key}.wav" +done