Represent DTMF sequences as list[KeypadEntry] via buttons field
Replaces the string-based `tones` field with a type-safe `buttons: list[KeypadEntry]` on `OutputDTMFFrame` and `OutputDTMFUrgentFrame`, matching the existing singular `button` field on `InputDTMFFrame`. A `from_string` classmethod builds the list from a dial string like `"123#"` (invalid characters raise ValueError from the `KeypadEntry` constructor). The base output audio fallback now iterates `frame.buttons` directly, LiveKit sends `frame.buttons[0].value`, and the Daily transport joins the button values into the single string Daily's `send_dtmf` expects.
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
- Added `DailyTransport.send_dtmf()` to expose the Daily call client's DTMF sending capability, enabling applications to send tones during a call (e.g. IVR navigation).
|
- Added `DailyTransport.send_dtmf()` to expose the Daily call client's DTMF sending capability, enabling applications to send tones during a call (e.g. IVR navigation).
|
||||||
- Added `tones` field to `OutputDTMFFrame` and `OutputDTMFUrgentFrame` for sending multi-digit DTMF sequences (e.g. `"123#"`). Valid characters are the values of `KeypadEntry`.
|
- Added `buttons` field to `OutputDTMFFrame` and `OutputDTMFUrgentFrame` for sending multi-key DTMF sequences as a `list[KeypadEntry]`. Use `OutputDTMFFrame.from_string("123#")` (or the equivalent on `OutputDTMFUrgentFrame`) to build one from a dial string.
|
||||||
- Added `DailyOutputDTMFFrame` and `DailyOutputDTMFUrgentFrame` frames. In addition to the inherited `tones`, they accept `session_id` and `digit_duration_ms`, which are forwarded to Daily's `send_dtmf` as `sessionId` and `digitDurationMs`.
|
- Added `DailyOutputDTMFFrame` and `DailyOutputDTMFUrgentFrame` frames. In addition to the inherited `buttons`, they accept `session_id` and `digit_duration_ms`, which are forwarded to Daily's `send_dtmf` as `sessionId` and `digitDurationMs`.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
- Deprecated the `button` field on `OutputDTMFFrame` and `OutputDTMFUrgentFrame`. Use the new `tones` field instead. When only `button` is set, `button.value` is used as a single-tone `tones` string for backward compatibility.
|
- Deprecated the `button` field on `OutputDTMFFrame` and `OutputDTMFUrgentFrame`. Use the new `buttons` field (a `list[KeypadEntry]`) instead. When only `button` is set, it is wrapped in a single-entry list for backward compatibility.
|
||||||
|
|||||||
@@ -744,33 +744,45 @@ class DTMFFrame:
|
|||||||
class OutputDTMFFrame(DTMFFrame, DataFrame):
|
class OutputDTMFFrame(DTMFFrame, DataFrame):
|
||||||
"""DTMF keypress output frame for transport queuing.
|
"""DTMF keypress output frame for transport queuing.
|
||||||
|
|
||||||
A DTMF keypress output that will be queued. If your transport supports
|
|
||||||
multiple dial-out destinations, use the ``transport_destination`` field
|
|
||||||
to specify where the DTMF keypress should be sent.
|
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
tones: String of one or more DTMF tones to send (e.g. ``"1"`` or
|
buttons: Sequence of one or more DTMF keypad buttons to send. Use
|
||||||
``"123#"``). Valid characters are the values of
|
:meth:`from_string` to build this from a string like ``"123#"``.
|
||||||
:class:`~pipecat.audio.dtmf.types.KeypadEntry`.
|
|
||||||
button: A single DTMF keypad entry to send.
|
button: A single DTMF keypad entry to send.
|
||||||
|
|
||||||
.. deprecated:: 1.1.0
|
.. deprecated:: 1.1.0
|
||||||
Use ``tones`` instead. When only ``button`` is set,
|
Use ``buttons`` instead. When only ``button`` is set, it is
|
||||||
``button.value`` is used as a single-tone ``tones`` string.
|
used as a single-entry ``buttons`` list.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
button: Optional[KeypadEntry] = None
|
button: Optional[KeypadEntry] = None
|
||||||
tones: Optional[str] = None
|
buttons: Optional[List[KeypadEntry]] = None
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
super().__post_init__()
|
super().__post_init__()
|
||||||
if self.tones is None and self.button is not None:
|
if self.buttons is None and self.button is not None:
|
||||||
self.tones = self.button.value
|
self.buttons = [self.button]
|
||||||
if not self.tones:
|
if not self.buttons:
|
||||||
raise ValueError(f"{self.__class__.__name__} requires `tones` or `button` to be set")
|
raise ValueError(f"{self.__class__.__name__} requires `buttons` or `button` to be set")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}(tones: {self.tones})"
|
buttons_str = "".join(b.value for b in self.buttons) if self.buttons else ""
|
||||||
|
return f"{self.name}(buttons: {buttons_str})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_string(cls, buttons: str, **kwargs) -> "OutputDTMFFrame":
|
||||||
|
"""Build an ``OutputDTMFFrame`` from a string of DTMF characters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
buttons: A string like ``"123#"``. Each character must be a
|
||||||
|
valid :class:`~pipecat.audio.dtmf.types.KeypadEntry` value.
|
||||||
|
**kwargs: Additional keyword arguments forwarded to the frame
|
||||||
|
constructor.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A frame of type ``cls`` with ``buttons`` populated as a list of
|
||||||
|
:class:`~pipecat.audio.dtmf.types.KeypadEntry`.
|
||||||
|
"""
|
||||||
|
return cls(buttons=[KeypadEntry(c) for c in buttons], **kwargs)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -1269,33 +1281,45 @@ class InputDTMFFrame(DTMFFrame, SystemFrame):
|
|||||||
class OutputDTMFUrgentFrame(DTMFFrame, SystemFrame):
|
class OutputDTMFUrgentFrame(DTMFFrame, SystemFrame):
|
||||||
"""DTMF keypress output frame for immediate sending.
|
"""DTMF keypress output frame for immediate sending.
|
||||||
|
|
||||||
A DTMF keypress output that will be sent right away. If your transport
|
|
||||||
supports multiple dial-out destinations, use the ``transport_destination``
|
|
||||||
field to specify where the DTMF keypress should be sent.
|
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
tones: String of one or more DTMF tones to send (e.g. ``"1"`` or
|
buttons: Sequence of one or more DTMF keypad buttons to send. Use
|
||||||
``"123#"``). Valid characters are the values of
|
:meth:`from_string` to build this from a string like ``"123#"``.
|
||||||
:class:`~pipecat.audio.dtmf.types.KeypadEntry`.
|
|
||||||
button: A single DTMF keypad entry to send.
|
button: A single DTMF keypad entry to send.
|
||||||
|
|
||||||
.. deprecated:: 1.1.0
|
.. deprecated:: 1.1.0
|
||||||
Use ``tones`` instead. When only ``button`` is set,
|
Use ``buttons`` instead. When only ``button`` is set, it is
|
||||||
``button.value`` is used as a single-tone ``tones`` string.
|
used as a single-entry ``buttons`` list.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
button: Optional[KeypadEntry] = None
|
button: Optional[KeypadEntry] = None
|
||||||
tones: Optional[str] = None
|
buttons: Optional[List[KeypadEntry]] = None
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
super().__post_init__()
|
super().__post_init__()
|
||||||
if self.tones is None and self.button is not None:
|
if self.buttons is None and self.button is not None:
|
||||||
self.tones = self.button.value
|
self.buttons = [self.button]
|
||||||
if not self.tones:
|
if not self.buttons:
|
||||||
raise ValueError(f"{self.__class__.__name__} requires `tones` or `button` to be set")
|
raise ValueError(f"{self.__class__.__name__} requires `buttons` or `button` to be set")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}(tones: {self.tones})"
|
buttons_str = "".join(b.value for b in self.buttons) if self.buttons else ""
|
||||||
|
return f"{self.name}(buttons: {buttons_str})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_string(cls, buttons: str, **kwargs) -> "OutputDTMFUrgentFrame":
|
||||||
|
"""Build an ``OutputDTMFUrgentFrame`` from a string of DTMF characters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
buttons: A string like ``"123#"``. Each character must be a
|
||||||
|
valid :class:`~pipecat.audio.dtmf.types.KeypadEntry` value.
|
||||||
|
**kwargs: Additional keyword arguments forwarded to the frame
|
||||||
|
constructor.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A frame of type ``cls`` with ``buttons`` populated as a list of
|
||||||
|
:class:`~pipecat.audio.dtmf.types.KeypadEntry`.
|
||||||
|
"""
|
||||||
|
return cls(buttons=[KeypadEntry(c) for c in buttons], **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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.types import KeypadEntry
|
|
||||||
from pipecat.audio.dtmf.utils import load_dtmf_audio
|
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
|
||||||
@@ -276,15 +275,10 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
Args:
|
Args:
|
||||||
frame: The DTMF frame to write.
|
frame: The DTMF frame to write.
|
||||||
"""
|
"""
|
||||||
if not frame.tones:
|
if not frame.buttons:
|
||||||
return
|
return
|
||||||
for char in frame.tones:
|
for button in frame.buttons:
|
||||||
try:
|
dtmf_audio = await load_dtmf_audio(button, sample_rate=self._sample_rate)
|
||||||
keypad_entry = KeypadEntry(char)
|
|
||||||
except ValueError:
|
|
||||||
logger.warning(f"Skipping invalid DTMF tone: {char!r}")
|
|
||||||
continue
|
|
||||||
dtmf_audio = await load_dtmf_audio(keypad_entry, sample_rate=self._sample_rate)
|
|
||||||
dtmf_audio_frame = OutputAudioRawFrame(
|
dtmf_audio_frame = OutputAudioRawFrame(
|
||||||
audio=dtmf_audio, sample_rate=self._sample_rate, num_channels=1
|
audio=dtmf_audio, sample_rate=self._sample_rate, num_channels=1
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2182,10 +2182,10 @@ class DailyOutputTransport(BaseOutputTransport):
|
|||||||
``digit_duration_ms`` fields are also forwarded to the Daily
|
``digit_duration_ms`` fields are also forwarded to the Daily
|
||||||
call client.
|
call client.
|
||||||
"""
|
"""
|
||||||
if not frame.tones:
|
if not frame.buttons:
|
||||||
return
|
return
|
||||||
|
|
||||||
settings: Dict[str, Any] = {"tones": frame.tones}
|
settings: Dict[str, Any] = {"tones": "".join(b.value for b in frame.buttons)}
|
||||||
if isinstance(frame, (DailyOutputDTMFFrame, DailyOutputDTMFUrgentFrame)):
|
if isinstance(frame, (DailyOutputDTMFFrame, DailyOutputDTMFUrgentFrame)):
|
||||||
if frame.session_id is not None:
|
if frame.session_id is not None:
|
||||||
settings["sessionId"] = frame.session_id
|
settings["sessionId"] = frame.session_id
|
||||||
|
|||||||
@@ -898,15 +898,16 @@ class LiveKitOutputTransport(BaseOutputTransport):
|
|||||||
async def _write_dtmf_native(self, frame: OutputDTMFFrame | OutputDTMFUrgentFrame):
|
async def _write_dtmf_native(self, frame: OutputDTMFFrame | OutputDTMFUrgentFrame):
|
||||||
"""Use LiveKit's native publish_dtmf method for telephone events.
|
"""Use LiveKit's native publish_dtmf method for telephone events.
|
||||||
|
|
||||||
LiveKit's DTMF API sends a single tone per call, so when ``frame.tones``
|
LiveKit's DTMF API sends a single tone per call, so when
|
||||||
contains multiple characters only the first one is sent.
|
``frame.buttons`` contains multiple entries only the first one is
|
||||||
|
sent.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
frame: The DTMF frame to write.
|
frame: The DTMF frame to write.
|
||||||
"""
|
"""
|
||||||
if not frame.tones:
|
if not frame.buttons:
|
||||||
return
|
return
|
||||||
await self._client.send_dtmf(frame.tones[0])
|
await self._client.send_dtmf(frame.buttons[0].value)
|
||||||
|
|
||||||
def _convert_pipecat_audio_to_livekit(self, pipecat_audio: bytes) -> rtc.AudioFrame:
|
def _convert_pipecat_audio_to_livekit(self, pipecat_audio: bytes) -> rtc.AudioFrame:
|
||||||
"""Convert Pipecat audio data to LiveKit audio frame."""
|
"""Convert Pipecat audio data to LiveKit audio frame."""
|
||||||
|
|||||||
Reference in New Issue
Block a user