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/),
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
### Added

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
uv.lock generated
View File

@@ -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" },