Merge pull request #2532 from pipecat-ai/aleix/universal-dtmf-support

Universal DTMF support
This commit is contained in:
Aleix Conchillo Flaqué
2025-08-28 21:04:13 -07:00
committed by GitHub
41 changed files with 387 additions and 157 deletions

View File

@@ -5,6 +5,31 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [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
`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
`pipecat.audio.dtmf.types.KeypadEntry` instead.
## [0.0.82] - 2025-08-28 ## [0.0.82] - 2025-08-28
### Added ### Added

View File

@@ -20,6 +20,7 @@ classifiers = [
"Topic :: Scientific/Engineering :: Artificial Intelligence" "Topic :: Scientific/Engineering :: Artificial Intelligence"
] ]
dependencies = [ dependencies = [
"aiofiles>=24.1.0,<25",
"aiohttp>=3.11.12,<4", "aiohttp>=3.11.12,<4",
"audioop-lts~=0.2.1; python_version>='3.13'", "audioop-lts~=0.2.1; python_version>='3.13'",
"docstring_parser~=0.16", "docstring_parser~=0.16",
@@ -137,6 +138,20 @@ where = ["src"]
[tool.setuptools.package-data] [tool.setuptools.package-data]
"pipecat" = ["py.typed"] "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"] "pipecat.services.aws_nova_sonic" = ["src/pipecat/services/aws_nova_sonic/ready.wav"]
[tool.pytest.ini_options] [tool.pytest.ini_options]

35
scripts/dtmf/generate_dtmf.sh Executable file
View File

@@ -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

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,47 @@
# Copyright (c) 20242025, 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 = "*"

View File

@@ -0,0 +1,70 @@
#
# Copyright (c) 20242025, 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]

View File

@@ -41,13 +41,15 @@ def create_default_resampler(**kwargs) -> BaseAudioResampler:
""" """
import warnings import warnings
warnings.warn( with warnings.catch_warnings():
"`create_default_resampler` is deprecated. " warnings.simplefilter("always")
"Use `create_stream_resampler` for real-time processing scenarios or " warnings.warn(
"`create_file_resampler` for batch processing of complete audio files.", "`create_default_resampler` is deprecated. "
DeprecationWarning, "Use `create_stream_resampler` for real-time processing scenarios or "
stacklevel=2, "`create_file_resampler` for batch processing of complete audio files.",
) DeprecationWarning,
stacklevel=2,
)
return SOXRAudioResampler(**kwargs) return SOXRAudioResampler(**kwargs)

View File

@@ -12,7 +12,6 @@ and LLM processing.
""" """
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
@@ -28,6 +27,7 @@ from typing import (
) )
from pipecat.adapters.schemas.tools_schema import ToolsSchema 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.interruptions.base_interruption_strategy import BaseInterruptionStrategy
from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams
from pipecat.audio.vad.vad_analyzer import VADParams from pipecat.audio.vad.vad_analyzer import VADParams
@@ -41,9 +41,13 @@ if TYPE_CHECKING:
from pipecat.processors.frame_processor import FrameProcessor from pipecat.processors.frame_processor import FrameProcessor
class KeypadEntry(str, Enum): class DeprecatedKeypadEntry:
"""DTMF keypad entries for phone system integration. """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: Parameters:
ONE: Number key 1. ONE: Number key 1.
TWO: Number key 2. TWO: Number key 2.
@@ -59,18 +63,38 @@ class KeypadEntry(str, Enum):
STAR: Star/asterisk key (*). STAR: Star/asterisk key (*).
""" """
ONE = "1" _enum = NewKeypadEntry
TWO = "2"
THREE = "3" @classmethod
FOUR = "4" def _warn(cls):
FIVE = "5" import warnings
SIX = "6"
SEVEN = "7" with warnings.catch_warnings():
EIGHT = "8" warnings.simplefilter("always")
NINE = "9" warnings.warn(
ZERO = "0" "`pipecat.frames.frames.KeypadEntry` is deprecated and will be removed in a future version. "
POUND = "#" "Use `pipecat.audio.dtmf.types.KeypadEntry` instead.",
STAR = "*" 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]): def format_pts(pts: Optional[int]):
@@ -526,15 +550,16 @@ class LLMMessagesFrame(DataFrame):
super().__post_init__() super().__post_init__()
import warnings import warnings
warnings.simplefilter("always") with warnings.catch_warnings():
warnings.warn( warnings.simplefilter("always")
"LLMMessagesFrame is deprecated and will be removed in a future version. " warnings.warn(
"Instead, use either " "LLMMessagesFrame is deprecated and will be removed in a future version. "
"`LLMMessagesUpdateFrame` with `run_llm=True`, or " "Instead, use either "
"`OpenAILLMContextFrame` with desired messages in a new context", "`LLMMessagesUpdateFrame` with `run_llm=True`, or "
DeprecationWarning, "`OpenAILLMContextFrame` with desired messages in a new context",
stacklevel=2, DeprecationWarning,
) stacklevel=2,
)
@dataclass @dataclass
@@ -668,7 +693,7 @@ class DTMFFrame:
button: The DTMF keypad entry that was pressed. button: The DTMF keypad entry that was pressed.
""" """
button: KeypadEntry button: NewKeypadEntry
@dataclass @dataclass

View File

@@ -14,13 +14,13 @@ for downstream processing by LLM context aggregators.
import asyncio import asyncio
from typing import Optional from typing import Optional
from pipecat.audio.dtmf.types import KeypadEntry
from pipecat.frames.frames import ( from pipecat.frames.frames import (
BotInterruptionFrame, BotInterruptionFrame,
CancelFrame, CancelFrame,
EndFrame, EndFrame,
Frame, Frame,
InputDTMFFrame, InputDTMFFrame,
KeypadEntry,
StartFrame, StartFrame,
TranscriptionFrame, TranscriptionFrame,
) )

View File

@@ -12,7 +12,6 @@ LLM processing, and text-to-speech components in conversational AI pipelines.
""" """
import asyncio import asyncio
import warnings
from abc import abstractmethod from abc import abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, List, Literal, Optional, Set from typing import Dict, List, Literal, Optional, Set
@@ -326,11 +325,15 @@ class LLMContextResponseAggregator(BaseLLMResponseAggregator):
Returns: Returns:
LLMContextFrame containing the current context. LLMContextFrame containing the current context.
""" """
warnings.warn( import warnings
"get_context_frame() is deprecated and will be removed in a future version. To trigger an LLM response, use LLMRunFrame instead.",
DeprecationWarning, with warnings.catch_warnings():
stacklevel=2, 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() return self._get_context_frame()
def _get_context_frame(self) -> OpenAILLMContextFrame: def _get_context_frame(self) -> OpenAILLMContextFrame:
@@ -1035,12 +1038,16 @@ class LLMUserResponseAggregator(LLMUserContextAggregator):
params: Configuration parameters for aggregation behavior. params: Configuration parameters for aggregation behavior.
**kwargs: Additional arguments passed to parent class. **kwargs: Additional arguments passed to parent class.
""" """
warnings.warn( import warnings
"LLMUserResponseAggregator is deprecated and will be removed in a future version. "
"Use LLMUserContextAggregator or another LLM-specific subclass instead.", with warnings.catch_warnings():
DeprecationWarning, warnings.simplefilter("always")
stacklevel=2, 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) super().__init__(context=OpenAILLMContext(messages), params=params, **kwargs)
async def _process_aggregation(self): async def _process_aggregation(self):
@@ -1078,12 +1085,16 @@ class LLMAssistantResponseAggregator(LLMAssistantContextAggregator):
params: Configuration parameters for aggregation behavior. params: Configuration parameters for aggregation behavior.
**kwargs: Additional arguments passed to parent class. **kwargs: Additional arguments passed to parent class.
""" """
warnings.warn( import warnings
"LLMAssistantResponseAggregator is deprecated and will be removed in a future version. "
"Use LLMAssistantContextAggregator or another LLM-specific subclass instead.", with warnings.catch_warnings():
DeprecationWarning, warnings.simplefilter("always")
stacklevel=2, 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) super().__init__(context=OpenAILLMContext(messages), params=params, **kwargs)
async def push_aggregation(self): async def push_aggregation(self):

View File

@@ -452,12 +452,14 @@ class FrameProcessor(BaseObject):
""" """
import warnings import warnings
warnings.warn( with warnings.catch_warnings():
"`FrameProcessor.wait_for_task()` is deprecated. " warnings.simplefilter("always")
"Use `await task` or `await asyncio.wait_for(task, timeout)` instead.", warnings.warn(
DeprecationWarning, "`FrameProcessor.wait_for_task()` is deprecated. "
stacklevel=2, "Use `await task` or `await asyncio.wait_for(task, timeout)` instead.",
) DeprecationWarning,
stacklevel=2,
)
if timeout: if timeout:
await asyncio.wait_for(task, timeout) await asyncio.wait_for(task, timeout)

View File

@@ -122,11 +122,13 @@ async def configure_with_args(aiohttp_session: aiohttp.ClientSession, parser=Non
""" """
import warnings import warnings
warnings.warn( with warnings.catch_warnings():
"configure_with_args is deprecated. Use configure() instead.", warnings.simplefilter("always")
DeprecationWarning, warnings.warn(
stacklevel=2, "configure_with_args is deprecated. Use configure() instead.",
) DeprecationWarning,
stacklevel=2,
)
room_url, token = await configure(aiohttp_session) room_url, token = await configure(aiohttp_session)
return (room_url, token, None) return (room_url, token, None)

View File

@@ -13,13 +13,13 @@ from typing import Optional
from loguru import logger from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
from pipecat.audio.dtmf.types import KeypadEntry
from pipecat.audio.utils import create_stream_resampler from pipecat.audio.utils import create_stream_resampler
from pipecat.frames.frames import ( from pipecat.frames.frames import (
AudioRawFrame, AudioRawFrame,
Frame, Frame,
InputAudioRawFrame, InputAudioRawFrame,
InputDTMFFrame, InputDTMFFrame,
KeypadEntry,
StartFrame, StartFrame,
StartInterruptionFrame, StartInterruptionFrame,
TransportMessageFrame, TransportMessageFrame,

View File

@@ -13,6 +13,7 @@ from typing import Optional
from loguru import logger from loguru import logger
from pydantic import BaseModel 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.audio.utils import create_stream_resampler, pcm_to_ulaw, ulaw_to_pcm
from pipecat.frames.frames import ( from pipecat.frames.frames import (
AudioRawFrame, AudioRawFrame,
@@ -21,7 +22,6 @@ from pipecat.frames.frames import (
Frame, Frame,
InputAudioRawFrame, InputAudioRawFrame,
InputDTMFFrame, InputDTMFFrame,
KeypadEntry,
StartFrame, StartFrame,
StartInterruptionFrame, StartInterruptionFrame,
TransportMessageFrame, TransportMessageFrame,

View File

@@ -14,6 +14,7 @@ import aiohttp
from loguru import logger from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
from pipecat.audio.dtmf.types import KeypadEntry
from pipecat.audio.utils import ( from pipecat.audio.utils import (
alaw_to_pcm, alaw_to_pcm,
create_stream_resampler, create_stream_resampler,
@@ -28,7 +29,6 @@ from pipecat.frames.frames import (
Frame, Frame,
InputAudioRawFrame, InputAudioRawFrame,
InputDTMFFrame, InputDTMFFrame,
KeypadEntry,
StartFrame, StartFrame,
StartInterruptionFrame, StartInterruptionFrame,
) )

View File

@@ -13,6 +13,7 @@ from typing import Optional
from loguru import logger from loguru import logger
from pydantic import BaseModel 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.audio.utils import create_stream_resampler, pcm_to_ulaw, ulaw_to_pcm
from pipecat.frames.frames import ( from pipecat.frames.frames import (
AudioRawFrame, AudioRawFrame,
@@ -21,7 +22,6 @@ from pipecat.frames.frames import (
Frame, Frame,
InputAudioRawFrame, InputAudioRawFrame,
InputDTMFFrame, InputDTMFFrame,
KeypadEntry,
StartFrame, StartFrame,
StartInterruptionFrame, StartInterruptionFrame,
TransportMessageFrame, TransportMessageFrame,

View File

@@ -11,11 +11,11 @@ _warned_modules = set()
def _warn_deprecated_access(globals: Dict[str, Any], attr, old: str, new: str): def _warn_deprecated_access(globals: Dict[str, Any], attr, old: str, new: str):
import warnings
# Only warn once per old->new module pair # Only warn once per old->new module pair
module_key = (old, new) module_key = (old, new)
if module_key not in _warned_modules: if module_key not in _warned_modules:
import warnings
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("always") warnings.simplefilter("always")
warnings.warn( warnings.warn(

View File

@@ -271,11 +271,13 @@ class CartesiaTTSService(AudioContextWordTTSService):
voice_config["id"] = self._voice_id voice_config["id"] = self._voice_id
if self._settings["emotion"]: if self._settings["emotion"]:
warnings.warn( with warnings.catch_warnings():
"The 'emotion' parameter in __experimental_controls is deprecated and will be removed in a future version.", warnings.simplefilter("always")
DeprecationWarning, warnings.warn(
stacklevel=2, "The 'emotion' parameter in __experimental_controls is deprecated and will be removed in a future version.",
) DeprecationWarning,
stacklevel=2,
)
voice_config["__experimental_controls"] = {} voice_config["__experimental_controls"] = {}
if self._settings["emotion"]: if self._settings["emotion"]:
voice_config["__experimental_controls"]["emotion"] = 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} voice_config = {"mode": "id", "id": self._voice_id}
if self._settings["emotion"]: if self._settings["emotion"]:
warnings.warn( with warnings.catch_warnings():
"The 'emotion' parameter in voice.__experimental_controls is deprecated and will be removed in a future version.", warnings.simplefilter("always")
DeprecationWarning, warnings.warn(
stacklevel=2, "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"]} voice_config["__experimental_controls"] = {"emotion": self._settings["emotion"]}
await self.start_ttfb_metrics() await self.start_ttfb_metrics()

View File

@@ -120,12 +120,14 @@ class FishAudioTTSService(InterruptibleTTSService):
if model: if model:
import warnings import warnings
warnings.warn( with warnings.catch_warnings():
"Parameter 'model' is deprecated and will be removed in a future version. " warnings.simplefilter("always")
"Use 'reference_id' instead.", warnings.warn(
DeprecationWarning, "Parameter 'model' is deprecated and will be removed in a future version. "
stacklevel=2, "Use 'reference_id' instead.",
) DeprecationWarning,
stacklevel=2,
)
reference_id = model reference_id = model
self._api_key = api_key self._api_key = api_key

View File

@@ -13,7 +13,6 @@ supporting multiple languages, custom vocabulary, and various audio processing o
import asyncio import asyncio
import base64 import base64
import json import json
import warnings
from typing import Any, AsyncGenerator, Dict, Literal, Optional from typing import Any, AsyncGenerator, Dict, Literal, Optional
import aiohttp import aiohttp
@@ -173,12 +172,16 @@ class _InputParamsDescriptor:
"""Descriptor for backward compatibility with deprecation warning.""" """Descriptor for backward compatibility with deprecation warning."""
def __get__(self, obj, objtype=None): def __get__(self, obj, objtype=None):
warnings.warn( import warnings
"GladiaSTTService.InputParams is deprecated and will be removed in a future version. "
"Import and use GladiaInputParams directly instead.", with warnings.catch_warnings():
DeprecationWarning, warnings.simplefilter("always")
stacklevel=2, 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 return GladiaInputParams
@@ -234,12 +237,14 @@ class GladiaSTTService(STTService):
# Warn about deprecated language parameter if it's used # Warn about deprecated language parameter if it's used
if params.language is not None: if params.language is not None:
warnings.warn( with warnings.catch_warnings():
"The 'language' parameter is deprecated and will be removed in a future version. " warnings.simplefilter("always")
"Use 'language_config' instead.", warnings.warn(
DeprecationWarning, "The 'language' parameter is deprecated and will be removed in a future version. "
stacklevel=2, "Use 'language_config' instead.",
) DeprecationWarning,
stacklevel=2,
)
self._api_key = api_key self._api_key = api_key
self._region = region self._region = region

View File

@@ -65,12 +65,14 @@ class GoogleLLMOpenAIBetaService(OpenAILLMService):
""" """
import warnings import warnings
warnings.warn( with warnings.catch_warnings():
"GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version. " warnings.simplefilter("always")
"Use GoogleLLMService instead for better integration with Google's native API.", warnings.warn(
DeprecationWarning, "GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version. "
stacklevel=2, "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) super().__init__(api_key=api_key, base_url=base_url, model=model, **kwargs)

View File

@@ -14,7 +14,6 @@ import io
import json import json
import struct import struct
import uuid import uuid
import warnings
from typing import AsyncGenerator, Optional from typing import AsyncGenerator, Optional
import aiohttp import aiohttp
@@ -455,11 +454,15 @@ class PlayHTHttpTTSService(TTSService):
# Warn about deprecated protocol parameter if explicitly provided # Warn about deprecated protocol parameter if explicitly provided
if protocol: if protocol:
warnings.warn( import warnings
"The 'protocol' parameter is deprecated and will be removed in a future version.",
DeprecationWarning, with warnings.catch_warnings():
stacklevel=2, 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() params = params or PlayHTHttpTTSService.InputParams()

View File

@@ -9,7 +9,6 @@
import asyncio import asyncio
import base64 import base64
import json import json
import warnings
from typing import Any, AsyncGenerator, Mapping, Optional from typing import Any, AsyncGenerator, Mapping, Optional
import aiohttp import aiohttp
@@ -356,11 +355,15 @@ class SarvamTTSService(InterruptibleTTSService):
) )
params = params or SarvamTTSService.InputParams() params = params or SarvamTTSService.InputParams()
if aiohttp_session is not None: if aiohttp_session is not None:
warnings.warn( import warnings
"The 'aiohttp_session' parameter is deprecated and will be removed in a future version. ",
DeprecationWarning, with warnings.catch_warnings():
stacklevel=2, 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 # WebSocket endpoint URL
self._websocket_url = f"{url}?model={model}" self._websocket_url = f"{url}?model={model}"
self._api_key = api_key self._api_key = api_key

View File

@@ -10,7 +10,6 @@ import asyncio
import datetime import datetime
import os import os
import re import re
import warnings
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from typing import Any, AsyncGenerator from typing import Any, AsyncGenerator
@@ -1107,6 +1106,8 @@ def _check_deprecated_args(kwargs: dict, params: SpeechmaticsSTTService.InputPar
# Show deprecation warnings # Show deprecation warnings
def _deprecation_warning(old: str, new: str | None = None): def _deprecation_warning(old: str, new: str | None = None):
import warnings
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("always") warnings.simplefilter("always")
if new: if new:

View File

@@ -276,11 +276,13 @@ class TTSService(AIService):
""" """
import warnings import warnings
warnings.warn( with warnings.catch_warnings():
"`TTSService.say()` is deprecated. Push a `TTSSpeakFrame` instead.", warnings.simplefilter("always")
DeprecationWarning, warnings.warn(
stacklevel=2, "`TTSService.say()` is deprecated. Push a `TTSSpeakFrame` instead.",
) DeprecationWarning,
stacklevel=2,
)
await self.queue_frame(TTSSpeakFrame(text)) await self.queue_frame(TTSSpeakFrame(text))

View File

@@ -19,6 +19,7 @@ from typing import Any, AsyncGenerator, Dict, List, Mapping, Optional
from loguru import logger from loguru import logger
from PIL import Image 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.mixers.base_audio_mixer import BaseAudioMixer
from pipecat.audio.utils import create_stream_resampler, is_silence from pipecat.audio.utils import create_stream_resampler, is_silence
from pipecat.frames.frames import ( from pipecat.frames.frames import (
@@ -223,7 +224,12 @@ class BaseOutputTransport(FrameProcessor):
Args: Args:
frame: The DTMF frame to write. 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): async def send_audio(self, frame: OutputAudioRawFrame):
"""Send an audio frame downstream. """Send an audio frame downstream.

View File

@@ -31,8 +31,6 @@ from pipecat.frames.frames import (
InputAudioRawFrame, InputAudioRawFrame,
InterimTranscriptionFrame, InterimTranscriptionFrame,
OutputAudioRawFrame, OutputAudioRawFrame,
OutputDTMFFrame,
OutputDTMFUrgentFrame,
OutputImageRawFrame, OutputImageRawFrame,
SpriteFrame, SpriteFrame,
StartFrame, StartFrame,
@@ -1676,7 +1674,7 @@ class DailyInputTransport(BaseInputTransport):
class DailyOutputTransport(BaseOutputTransport): class DailyOutputTransport(BaseOutputTransport):
"""Handles outgoing media streams and events to Daily calls. """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. including audio destination registration and message transmission.
""" """
@@ -1783,19 +1781,6 @@ class DailyOutputTransport(BaseOutputTransport):
""" """
await self._client.register_audio_destination(destination) 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): async def write_audio_frame(self, frame: OutputAudioRawFrame):
"""Write an audio frame to the Daily call. """Write an audio frame to the Daily call.
@@ -2022,25 +2007,6 @@ class DailyTransport(BaseTransport):
""" """
await self._client.stop_dialout(participant_id) 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): async def sip_call_transfer(self, settings):
"""Transfer a SIP call to another destination. """Transfer a SIP call to another destination.

View File

@@ -6,10 +6,10 @@
import unittest import unittest
from pipecat.audio.dtmf.types import KeypadEntry
from pipecat.frames.frames import ( from pipecat.frames.frames import (
EndFrame, EndFrame,
InputDTMFFrame, InputDTMFFrame,
KeypadEntry,
TranscriptionFrame, TranscriptionFrame,
) )
from pipecat.processors.aggregators.dtmf_aggregator import DTMFAggregator from pipecat.processors.aggregators.dtmf_aggregator import DTMFAggregator

2
uv.lock generated
View File

@@ -4189,6 +4189,7 @@ wheels = [
name = "pipecat-ai" name = "pipecat-ai"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiofiles" },
{ name = "aiohttp" }, { name = "aiohttp" },
{ name = "audioop-lts", marker = "python_full_version >= '3.13'" }, { name = "audioop-lts", marker = "python_full_version >= '3.13'" },
{ name = "docstring-parser" }, { name = "docstring-parser" },
@@ -4409,6 +4410,7 @@ docs = [
requires-dist = [ requires-dist = [
{ name = "accelerate", marker = "extra == 'moondream'", specifier = "~=1.10.0" }, { name = "accelerate", marker = "extra == 'moondream'", specifier = "~=1.10.0" },
{ name = "aioboto3", marker = "extra == 'aws'", specifier = "~=15.0.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 = "aiohttp", specifier = ">=3.11.12,<4" },
{ name = "aiortc", marker = "extra == 'webrtc'", specifier = "~=1.11.0" }, { name = "aiortc", marker = "extra == 'webrtc'", specifier = "~=1.11.0" },
{ name = "anthropic", marker = "extra == 'anthropic'", specifier = "~=0.49.0" }, { name = "anthropic", marker = "extra == 'anthropic'", specifier = "~=0.49.0" },