Merge pull request #2532 from pipecat-ai/aleix/universal-dtmf-support
Universal DTMF support
This commit is contained in:
25
CHANGELOG.md
25
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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
35
scripts/dtmf/generate_dtmf.sh
Executable 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
|
||||
0
src/pipecat/audio/dtmf/__init__.py
Normal file
0
src/pipecat/audio/dtmf/__init__.py
Normal file
BIN
src/pipecat/audio/dtmf/dtmf-0.wav
Normal file
BIN
src/pipecat/audio/dtmf/dtmf-0.wav
Normal file
Binary file not shown.
BIN
src/pipecat/audio/dtmf/dtmf-1.wav
Normal file
BIN
src/pipecat/audio/dtmf/dtmf-1.wav
Normal file
Binary file not shown.
BIN
src/pipecat/audio/dtmf/dtmf-2.wav
Normal file
BIN
src/pipecat/audio/dtmf/dtmf-2.wav
Normal file
Binary file not shown.
BIN
src/pipecat/audio/dtmf/dtmf-3.wav
Normal file
BIN
src/pipecat/audio/dtmf/dtmf-3.wav
Normal file
Binary file not shown.
BIN
src/pipecat/audio/dtmf/dtmf-4.wav
Normal file
BIN
src/pipecat/audio/dtmf/dtmf-4.wav
Normal file
Binary file not shown.
BIN
src/pipecat/audio/dtmf/dtmf-5.wav
Normal file
BIN
src/pipecat/audio/dtmf/dtmf-5.wav
Normal file
Binary file not shown.
BIN
src/pipecat/audio/dtmf/dtmf-6.wav
Normal file
BIN
src/pipecat/audio/dtmf/dtmf-6.wav
Normal file
Binary file not shown.
BIN
src/pipecat/audio/dtmf/dtmf-7.wav
Normal file
BIN
src/pipecat/audio/dtmf/dtmf-7.wav
Normal file
Binary file not shown.
BIN
src/pipecat/audio/dtmf/dtmf-8.wav
Normal file
BIN
src/pipecat/audio/dtmf/dtmf-8.wav
Normal file
Binary file not shown.
BIN
src/pipecat/audio/dtmf/dtmf-9.wav
Normal file
BIN
src/pipecat/audio/dtmf/dtmf-9.wav
Normal file
Binary file not shown.
BIN
src/pipecat/audio/dtmf/dtmf-pound.wav
Normal file
BIN
src/pipecat/audio/dtmf/dtmf-pound.wav
Normal file
Binary file not shown.
BIN
src/pipecat/audio/dtmf/dtmf-star.wav
Normal file
BIN
src/pipecat/audio/dtmf/dtmf-star.wav
Normal file
Binary file not shown.
47
src/pipecat/audio/dtmf/types.py
Normal file
47
src/pipecat/audio/dtmf/types.py
Normal file
@@ -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 = "*"
|
||||
70
src/pipecat/audio/dtmf/utils.py
Normal file
70
src/pipecat/audio/dtmf/utils.py
Normal file
@@ -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]
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
2
uv.lock
generated
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user