From 173cf39aee845a0dd2e412c3e6ef4c440e2c3fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Tue, 14 Apr 2026 20:36:38 -0700 Subject: [PATCH 1/9] Add send_dtmf() to DailyTransport Exposes the Daily call client's DTMF sending capability so applications can send tones during a call (e.g. IVR navigation). --- src/pipecat/transports/daily/transport.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/pipecat/transports/daily/transport.py b/src/pipecat/transports/daily/transport.py index ae91db7b5..fe53f16f3 100644 --- a/src/pipecat/transports/daily/transport.py +++ b/src/pipecat/transports/daily/transport.py @@ -2400,6 +2400,22 @@ class DailyTransport(BaseTransport): """ return self._client.participant_counts() + async def send_dtmf(self, settings) -> Optional[CallClientError]: + """Send DTMF tones during a call. + + Args: + settings: DTMF settings including tones and target session. + + Returns: + error: An error description or None. + """ + logger.debug(f"Sending DTMF: settings={settings}") + + error = await self._client.send_dtmf(settings) + if error: + logger.error(f"Unable to send DTMF: {error}") + return error + async def start_dialout(self, settings=None) -> Tuple[str, Optional[CallClientError]]: """Start a dial-out call to a phone number. From fe2ef9c71290dba6ab1a30f7b99d0fbe48291acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 15 Apr 2026 10:14:59 -0700 Subject: [PATCH 2/9] Add changelog for #4313 --- changelog/4313.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/4313.added.md diff --git a/changelog/4313.added.md b/changelog/4313.added.md new file mode 100644 index 000000000..6a3cd2b1c --- /dev/null +++ b/changelog/4313.added.md @@ -0,0 +1 @@ +- Added `DailyTransport.send_dtmf()` for sending DTMF tones during a call (e.g. IVR navigation). From 30f39d7395baf231817529bcd6eed3bc00e620c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 15 Apr 2026 14:20:08 -0700 Subject: [PATCH 3/9] Add DailyOutputDTMFFrame and DailyOutputDTMFUrgentFrame Introduces Daily-specific DTMF output frames that carry explicit `tones`, `session_id` and `digit_duration_ms` fields, forwarded to Daily's `send_dtmf` as `tones`, `sessionId` and `digitDurationMs`. The inherited `button` and `transport_destination` fields are ignored for these frames in the Daily transport. --- changelog/4313.added.md | 3 +- src/pipecat/transports/daily/transport.py | 103 ++++++++++++++++++++-- 2 files changed, 97 insertions(+), 9 deletions(-) diff --git a/changelog/4313.added.md b/changelog/4313.added.md index 6a3cd2b1c..6bd4f22b2 100644 --- a/changelog/4313.added.md +++ b/changelog/4313.added.md @@ -1 +1,2 @@ -- Added `DailyTransport.send_dtmf()` for sending DTMF 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 `DailyOutputDTMFFrame` and `DailyOutputDTMFUrgentFrame` frames for sending DTMF through the Daily transport pipeline. Both carry explicit `tones`, `session_id` and `digit_duration_ms` fields that are forwarded to Daily's `send_dtmf` as `tones`, `sessionId` and `digitDurationMs`. When the Daily transport processes these frames, the inherited `button` and `transport_destination` fields are ignored. diff --git a/src/pipecat/transports/daily/transport.py b/src/pipecat/transports/daily/transport.py index fe53f16f3..31b96ae97 100644 --- a/src/pipecat/transports/daily/transport.py +++ b/src/pipecat/transports/daily/transport.py @@ -36,6 +36,8 @@ from pipecat.frames.frames import ( InputTransportMessageFrame, InterimTranscriptionFrame, OutputAudioRawFrame, + OutputDTMFFrame, + OutputDTMFUrgentFrame, OutputImageRawFrame, OutputTransportMessageFrame, OutputTransportMessageUrgentFrame, @@ -151,6 +153,78 @@ class DailyUpdateRemoteParticipantsFrame(DataFrame): remote_participants: Mapping[str, Any] = field(default_factory=dict) +@dataclass +class DailyOutputDTMFFrame(OutputDTMFFrame): + """DTMF output frame with Daily-specific options for transport queuing. + + A DTMF keypress output that will be queued after any preceding audio has + finished playing. When this frame is processed by the Daily transport, + the inherited ``button`` and ``transport_destination`` fields are ignored + in favor of the explicit ``tones``, ``session_id`` and + ``digit_duration_ms`` fields below. + + Parameters: + tones: String of one or more DTMF tones to send (e.g. ``"1"`` or + ``"123#"``). Forwarded to Daily's ``send_dtmf`` as ``tones``. + session_id: Target participant session id. Forwarded to Daily's + ``send_dtmf`` as ``sessionId``. When ``None``, Daily sends the + tones to the default destination for the call. + digit_duration_ms: Duration of each DTMF digit in milliseconds. + Forwarded to Daily's ``send_dtmf`` as ``digitDurationMs``. When + ``None``, Daily's default duration is used. + """ + + # Override the inherited `button` to be optional: Daily's send_dtmf takes + # a multi-character `tones` string, so a single KeypadEntry is not + # required here. + button: Optional[KeypadEntry] = None # pyright: ignore[reportIncompatibleVariableOverride] + tones: Optional[str] = None + session_id: Optional[str] = None + digit_duration_ms: Optional[int] = None + + def __post_init__(self): + super().__post_init__() + if not self.tones: + raise ValueError(f"{self.__class__.__name__} requires `tones` to be set") + + def __str__(self): + return f"{self.name}(tones: {self.tones})" + + +@dataclass +class DailyOutputDTMFUrgentFrame(OutputDTMFUrgentFrame): + """DTMF output frame with Daily-specific options for immediate sending. + + A DTMF keypress output that will be sent right away. When this frame is + processed by the Daily transport, the inherited ``button`` and + ``transport_destination`` fields are ignored in favor of the explicit + ``tones``, ``session_id`` and ``digit_duration_ms`` fields below. + + Parameters: + tones: String of one or more DTMF tones to send (e.g. ``"1"`` or + ``"123#"``). Forwarded to Daily's ``send_dtmf`` as ``tones``. + session_id: Target participant session id. Forwarded to Daily's + ``send_dtmf`` as ``sessionId``. When ``None``, Daily sends the + tones to the default destination for the call. + digit_duration_ms: Duration of each DTMF digit in milliseconds. + Forwarded to Daily's ``send_dtmf`` as ``digitDurationMs``. When + ``None``, Daily's default duration is used. + """ + + button: Optional[KeypadEntry] = None # pyright: ignore[reportIncompatibleVariableOverride] + tones: Optional[str] = None + session_id: Optional[str] = None + digit_duration_ms: Optional[int] = None + + def __post_init__(self): + super().__post_init__() + if not self.tones: + raise ValueError(f"{self.__class__.__name__} requires `tones` to be set") + + def __str__(self): + return f"{self.name}(tones: {self.tones})" + + class WebRTCVADAnalyzer(VADAnalyzer): """Voice Activity Detection analyzer using WebRTC. @@ -2131,18 +2205,31 @@ class DailyOutputTransport(BaseOutputTransport): """ return True - async def _write_dtmf_native(self, frame): + async def _write_dtmf_native(self, frame: OutputDTMFFrame | OutputDTMFUrgentFrame): """Use Daily's native send_dtmf method for telephone events. Args: - frame: The DTMF frame to write. + frame: The DTMF frame to write. When it is a + :class:`DailyOutputDTMFFrame` or + :class:`DailyOutputDTMFUrgentFrame`, the explicit ``tones``, + ``session_id`` and ``digit_duration_ms`` fields are forwarded + to the Daily call client (and the inherited ``button`` / + ``transport_destination`` fields are ignored). """ - await self._client.send_dtmf( - { - "sessionId": frame.transport_destination, - "tones": frame.button.value, - } - ) + if isinstance(frame, (DailyOutputDTMFFrame, DailyOutputDTMFUrgentFrame)): + settings: Dict[str, Any] = {"tones": frame.tones} + if frame.session_id is not None: + settings["sessionId"] = frame.session_id + if frame.digit_duration_ms is not None: + settings["digitDurationMs"] = frame.digit_duration_ms + await self._client.send_dtmf(settings) + else: + await self._client.send_dtmf( + { + "sessionId": frame.transport_destination, + "tones": frame.button.value, + } + ) class DailyTransport(BaseTransport): From 675b7df4083d60a96fc35c72c929fc64c752417c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 15 Apr 2026 14:48:02 -0700 Subject: [PATCH 4/9] Add `tones` to OutputDTMFFrame and simplify DTMF frame hierarchy Introduces a new `tones` field on `OutputDTMFFrame` and `OutputDTMFUrgentFrame` for sending multi-digit DTMF sequences and deprecates the existing single-key `button` field. When only `button` is set, it is used as a single-character `tones` string for backward compatibility. `DTMFFrame` is kept as an empty marker class so both input and output DTMF frames can still be identified via isinstance. `InputDTMFFrame` keeps its required `button` field (single keypress semantics). The Daily-specific `DailyOutputDTMFFrame` and `DailyOutputDTMFUrgentFrame` frames no longer need to override `button` and simply add `session_id` and `digit_duration_ms`, which are forwarded to Daily's `send_dtmf` as `sessionId` and `digitDurationMs`. The base output audio fallback now iterates `tones` and generates a tone per character; LiveKit's native DTMF path sends `tones[0]` since its API is single-tone. --- changelog/4313.added.md | 3 +- changelog/4313.deprecated.md | 1 + src/pipecat/frames/frames.py | 68 ++++++++++++++--- src/pipecat/transports/base_output.py | 19 +++-- src/pipecat/transports/daily/transport.py | 81 ++++++--------------- src/pipecat/transports/livekit/transport.py | 7 +- 6 files changed, 104 insertions(+), 75 deletions(-) create mode 100644 changelog/4313.deprecated.md diff --git a/changelog/4313.added.md b/changelog/4313.added.md index 6bd4f22b2..c64bbc12e 100644 --- a/changelog/4313.added.md +++ b/changelog/4313.added.md @@ -1,2 +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 `DailyOutputDTMFFrame` and `DailyOutputDTMFUrgentFrame` frames for sending DTMF through the Daily transport pipeline. Both carry explicit `tones`, `session_id` and `digit_duration_ms` fields that are forwarded to Daily's `send_dtmf` as `tones`, `sessionId` and `digitDurationMs`. When the Daily transport processes these frames, the inherited `button` and `transport_destination` fields are ignored. +- Added `tones` field to `OutputDTMFFrame` and `OutputDTMFUrgentFrame` for sending multi-digit DTMF sequences (e.g. `"123#"`). Valid characters are the values of `KeypadEntry`. +- 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`. diff --git a/changelog/4313.deprecated.md b/changelog/4313.deprecated.md new file mode 100644 index 000000000..fdf9249d4 --- /dev/null +++ b/changelog/4313.deprecated.md @@ -0,0 +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. diff --git a/src/pipecat/frames/frames.py b/src/pipecat/frames/frames.py index 00f38cab8..d02bfcd90 100644 --- a/src/pipecat/frames/frames.py +++ b/src/pipecat/frames/frames.py @@ -730,13 +730,14 @@ class OutputTransportMessageFrame(DataFrame): @dataclass class DTMFFrame: - """Base class for DTMF (Dual-Tone Multi-Frequency) keypad frames. + """Marker base class for DTMF (Dual-Tone Multi-Frequency) keypad frames. - Parameters: - button: The DTMF keypad entry that was pressed. + Used only as a shared tag so that both input and output DTMF frames can + be identified via ``isinstance(frame, DTMFFrame)``. The concrete frames + define their own fields. """ - button: KeypadEntry + pass @dataclass @@ -744,12 +745,32 @@ class OutputDTMFFrame(DTMFFrame, DataFrame): """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. + multiple dial-out destinations, use the ``transport_destination`` field + to specify where the DTMF keypress should be sent. + + Parameters: + tones: String of one or more DTMF tones to send (e.g. ``"1"`` or + ``"123#"``). Valid characters are the values of + :class:`~pipecat.audio.dtmf.types.KeypadEntry`. + button: A single DTMF keypad entry to send. + + .. deprecated:: 1.1.0 + Use ``tones`` instead. When only ``button`` is set, + ``button.value`` is used as a single-tone ``tones`` string. """ + button: Optional[KeypadEntry] = None + tones: Optional[str] = None + + def __post_init__(self): + super().__post_init__() + if self.tones is None and self.button is not None: + self.tones = self.button.value + if not self.tones: + raise ValueError(f"{self.__class__.__name__} requires `tones` or `button` to be set") + def __str__(self): - return f"{self.name}(tone: {self.button})" + return f"{self.name}(tones: {self.tones})" # @@ -1232,7 +1253,13 @@ class AssistantImageRawFrame(OutputImageRawFrame): @dataclass class InputDTMFFrame(DTMFFrame, SystemFrame): - """DTMF keypress input frame from transport.""" + """DTMF keypress input frame from transport. + + Parameters: + button: The DTMF keypad entry that was pressed. + """ + + button: KeypadEntry def __str__(self): return f"{self.name}(tone: {self.button.value})" @@ -1243,11 +1270,32 @@ class OutputDTMFUrgentFrame(DTMFFrame, SystemFrame): """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` + supports multiple dial-out destinations, use the ``transport_destination`` field to specify where the DTMF keypress should be sent. + + Parameters: + tones: String of one or more DTMF tones to send (e.g. ``"1"`` or + ``"123#"``). Valid characters are the values of + :class:`~pipecat.audio.dtmf.types.KeypadEntry`. + button: A single DTMF keypad entry to send. + + .. deprecated:: 1.1.0 + Use ``tones`` instead. When only ``button`` is set, + ``button.value`` is used as a single-tone ``tones`` string. """ - pass + button: Optional[KeypadEntry] = None + tones: Optional[str] = None + + def __post_init__(self): + super().__post_init__() + if self.tones is None and self.button is not None: + self.tones = self.button.value + if not self.tones: + raise ValueError(f"{self.__class__.__name__} requires `tones` or `button` to be set") + + def __str__(self): + return f"{self.name}(tones: {self.tones})" @dataclass diff --git a/src/pipecat/transports/base_output.py b/src/pipecat/transports/base_output.py index d14daecbd..8a6e41331 100644 --- a/src/pipecat/transports/base_output.py +++ b/src/pipecat/transports/base_output.py @@ -19,6 +19,7 @@ from typing import Any, AsyncGenerator, Dict, List, Mapping, Optional from loguru import logger from PIL import Image +from pipecat.audio.dtmf.types import KeypadEntry 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 @@ -275,11 +276,19 @@ class BaseOutputTransport(FrameProcessor): Args: frame: The DTMF frame to write. """ - 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 - ) - await self.write_audio_frame(dtmf_audio_frame) + if not frame.tones: + return + for char in frame.tones: + try: + 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( + audio=dtmf_audio, sample_rate=self._sample_rate, num_channels=1 + ) + await self.write_audio_frame(dtmf_audio_frame) async def send_audio(self, frame: OutputAudioRawFrame): """Send an audio frame downstream. diff --git a/src/pipecat/transports/daily/transport.py b/src/pipecat/transports/daily/transport.py index 31b96ae97..739d47b59 100644 --- a/src/pipecat/transports/daily/transport.py +++ b/src/pipecat/transports/daily/transport.py @@ -158,72 +158,39 @@ class DailyOutputDTMFFrame(OutputDTMFFrame): """DTMF output frame with Daily-specific options for transport queuing. A DTMF keypress output that will be queued after any preceding audio has - finished playing. When this frame is processed by the Daily transport, - the inherited ``button`` and ``transport_destination`` fields are ignored - in favor of the explicit ``tones``, ``session_id`` and - ``digit_duration_ms`` fields below. + finished playing. Inherits ``tones`` from :class:`OutputDTMFFrame`; the + two extra fields are forwarded to Daily's ``send_dtmf`` as ``sessionId`` + and ``digitDurationMs``. Parameters: - tones: String of one or more DTMF tones to send (e.g. ``"1"`` or - ``"123#"``). Forwarded to Daily's ``send_dtmf`` as ``tones``. - session_id: Target participant session id. Forwarded to Daily's - ``send_dtmf`` as ``sessionId``. When ``None``, Daily sends the - tones to the default destination for the call. + session_id: Target participant session id. When ``None``, Daily + sends the tones to the default destination for the call. digit_duration_ms: Duration of each DTMF digit in milliseconds. - Forwarded to Daily's ``send_dtmf`` as ``digitDurationMs``. When - ``None``, Daily's default duration is used. + When ``None``, Daily's default duration is used. """ - # Override the inherited `button` to be optional: Daily's send_dtmf takes - # a multi-character `tones` string, so a single KeypadEntry is not - # required here. - button: Optional[KeypadEntry] = None # pyright: ignore[reportIncompatibleVariableOverride] - tones: Optional[str] = None session_id: Optional[str] = None digit_duration_ms: Optional[int] = None - def __post_init__(self): - super().__post_init__() - if not self.tones: - raise ValueError(f"{self.__class__.__name__} requires `tones` to be set") - - def __str__(self): - return f"{self.name}(tones: {self.tones})" - @dataclass class DailyOutputDTMFUrgentFrame(OutputDTMFUrgentFrame): """DTMF output frame with Daily-specific options for immediate sending. - A DTMF keypress output that will be sent right away. When this frame is - processed by the Daily transport, the inherited ``button`` and - ``transport_destination`` fields are ignored in favor of the explicit - ``tones``, ``session_id`` and ``digit_duration_ms`` fields below. + A DTMF keypress output that will be sent right away. Inherits ``tones`` + from :class:`OutputDTMFUrgentFrame`; the two extra fields are forwarded + to Daily's ``send_dtmf`` as ``sessionId`` and ``digitDurationMs``. Parameters: - tones: String of one or more DTMF tones to send (e.g. ``"1"`` or - ``"123#"``). Forwarded to Daily's ``send_dtmf`` as ``tones``. - session_id: Target participant session id. Forwarded to Daily's - ``send_dtmf`` as ``sessionId``. When ``None``, Daily sends the - tones to the default destination for the call. + session_id: Target participant session id. When ``None``, Daily + sends the tones to the default destination for the call. digit_duration_ms: Duration of each DTMF digit in milliseconds. - Forwarded to Daily's ``send_dtmf`` as ``digitDurationMs``. When - ``None``, Daily's default duration is used. + When ``None``, Daily's default duration is used. """ - button: Optional[KeypadEntry] = None # pyright: ignore[reportIncompatibleVariableOverride] - tones: Optional[str] = None session_id: Optional[str] = None digit_duration_ms: Optional[int] = None - def __post_init__(self): - super().__post_init__() - if not self.tones: - raise ValueError(f"{self.__class__.__name__} requires `tones` to be set") - - def __str__(self): - return f"{self.name}(tones: {self.tones})" - class WebRTCVADAnalyzer(VADAnalyzer): """Voice Activity Detection analyzer using WebRTC. @@ -2211,25 +2178,23 @@ class DailyOutputTransport(BaseOutputTransport): Args: frame: The DTMF frame to write. When it is a :class:`DailyOutputDTMFFrame` or - :class:`DailyOutputDTMFUrgentFrame`, the explicit ``tones``, - ``session_id`` and ``digit_duration_ms`` fields are forwarded - to the Daily call client (and the inherited ``button`` / - ``transport_destination`` fields are ignored). + :class:`DailyOutputDTMFUrgentFrame`, the ``session_id`` and + ``digit_duration_ms`` fields are also forwarded to the Daily + call client. """ + if not frame.tones: + return + + settings: Dict[str, Any] = {"tones": frame.tones} if isinstance(frame, (DailyOutputDTMFFrame, DailyOutputDTMFUrgentFrame)): - settings: Dict[str, Any] = {"tones": frame.tones} if frame.session_id is not None: settings["sessionId"] = frame.session_id if frame.digit_duration_ms is not None: settings["digitDurationMs"] = frame.digit_duration_ms - await self._client.send_dtmf(settings) - else: - await self._client.send_dtmf( - { - "sessionId": frame.transport_destination, - "tones": frame.button.value, - } - ) + elif frame.transport_destination is not None: + settings["sessionId"] = frame.transport_destination + + await self._client.send_dtmf(settings) class DailyTransport(BaseTransport): diff --git a/src/pipecat/transports/livekit/transport.py b/src/pipecat/transports/livekit/transport.py index f3b0574b8..0e904066a 100644 --- a/src/pipecat/transports/livekit/transport.py +++ b/src/pipecat/transports/livekit/transport.py @@ -898,10 +898,15 @@ class LiveKitOutputTransport(BaseOutputTransport): async def _write_dtmf_native(self, frame: OutputDTMFFrame | OutputDTMFUrgentFrame): """Use LiveKit's native publish_dtmf method for telephone events. + LiveKit's DTMF API sends a single tone per call, so when ``frame.tones`` + contains multiple characters only the first one is sent. + Args: frame: The DTMF frame to write. """ - await self._client.send_dtmf(frame.button.value) + if not frame.tones: + return + await self._client.send_dtmf(frame.tones[0]) def _convert_pipecat_audio_to_livekit(self, pipecat_audio: bytes) -> rtc.AudioFrame: """Convert Pipecat audio data to LiveKit audio frame.""" From d8b0e78bc8cb5e7465cd2993bf50eb668de95507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 15 Apr 2026 15:05:45 -0700 Subject: [PATCH 5/9] 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. --- changelog/4313.added.md | 4 +- changelog/4313.deprecated.md | 2 +- src/pipecat/frames/frames.py | 84 +++++++++++++-------- src/pipecat/transports/base_output.py | 12 +-- src/pipecat/transports/daily/transport.py | 4 +- src/pipecat/transports/livekit/transport.py | 9 ++- 6 files changed, 67 insertions(+), 48 deletions(-) diff --git a/changelog/4313.added.md b/changelog/4313.added.md index c64bbc12e..1c704a3a7 100644 --- a/changelog/4313.added.md +++ b/changelog/4313.added.md @@ -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 `tones` field to `OutputDTMFFrame` and `OutputDTMFUrgentFrame` for sending multi-digit DTMF sequences (e.g. `"123#"`). Valid characters are the values of `KeypadEntry`. -- 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 `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 `buttons`, they accept `session_id` and `digit_duration_ms`, which are forwarded to Daily's `send_dtmf` as `sessionId` and `digitDurationMs`. diff --git a/changelog/4313.deprecated.md b/changelog/4313.deprecated.md index fdf9249d4..58b826c6f 100644 --- a/changelog/4313.deprecated.md +++ b/changelog/4313.deprecated.md @@ -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. diff --git a/src/pipecat/frames/frames.py b/src/pipecat/frames/frames.py index d02bfcd90..68574b6a5 100644 --- a/src/pipecat/frames/frames.py +++ b/src/pipecat/frames/frames.py @@ -744,33 +744,45 @@ class DTMFFrame: class OutputDTMFFrame(DTMFFrame, DataFrame): """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: - tones: String of one or more DTMF tones to send (e.g. ``"1"`` or - ``"123#"``). Valid characters are the values of - :class:`~pipecat.audio.dtmf.types.KeypadEntry`. + buttons: Sequence of one or more DTMF keypad buttons to send. Use + :meth:`from_string` to build this from a string like ``"123#"``. button: A single DTMF keypad entry to send. .. deprecated:: 1.1.0 - Use ``tones`` instead. When only ``button`` is set, - ``button.value`` is used as a single-tone ``tones`` string. + Use ``buttons`` instead. When only ``button`` is set, it is + used as a single-entry ``buttons`` list. """ button: Optional[KeypadEntry] = None - tones: Optional[str] = None + buttons: Optional[List[KeypadEntry]] = None def __post_init__(self): super().__post_init__() - if self.tones is None and self.button is not None: - self.tones = self.button.value - if not self.tones: - raise ValueError(f"{self.__class__.__name__} requires `tones` or `button` to be set") + if self.buttons is None and self.button is not None: + self.buttons = [self.button] + if not self.buttons: + raise ValueError(f"{self.__class__.__name__} requires `buttons` or `button` to be set") 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): """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: - tones: String of one or more DTMF tones to send (e.g. ``"1"`` or - ``"123#"``). Valid characters are the values of - :class:`~pipecat.audio.dtmf.types.KeypadEntry`. + buttons: Sequence of one or more DTMF keypad buttons to send. Use + :meth:`from_string` to build this from a string like ``"123#"``. button: A single DTMF keypad entry to send. .. deprecated:: 1.1.0 - Use ``tones`` instead. When only ``button`` is set, - ``button.value`` is used as a single-tone ``tones`` string. + Use ``buttons`` instead. When only ``button`` is set, it is + used as a single-entry ``buttons`` list. """ button: Optional[KeypadEntry] = None - tones: Optional[str] = None + buttons: Optional[List[KeypadEntry]] = None def __post_init__(self): super().__post_init__() - if self.tones is None and self.button is not None: - self.tones = self.button.value - if not self.tones: - raise ValueError(f"{self.__class__.__name__} requires `tones` or `button` to be set") + if self.buttons is None and self.button is not None: + self.buttons = [self.button] + if not self.buttons: + raise ValueError(f"{self.__class__.__name__} requires `buttons` or `button` to be set") 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 diff --git a/src/pipecat/transports/base_output.py b/src/pipecat/transports/base_output.py index 8a6e41331..fe1044f51 100644 --- a/src/pipecat/transports/base_output.py +++ b/src/pipecat/transports/base_output.py @@ -19,7 +19,6 @@ from typing import Any, AsyncGenerator, Dict, List, Mapping, Optional from loguru import logger from PIL import Image -from pipecat.audio.dtmf.types import KeypadEntry 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 @@ -276,15 +275,10 @@ class BaseOutputTransport(FrameProcessor): Args: frame: The DTMF frame to write. """ - if not frame.tones: + if not frame.buttons: return - for char in frame.tones: - try: - 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) + for button in frame.buttons: + dtmf_audio = await load_dtmf_audio(button, sample_rate=self._sample_rate) dtmf_audio_frame = OutputAudioRawFrame( audio=dtmf_audio, sample_rate=self._sample_rate, num_channels=1 ) diff --git a/src/pipecat/transports/daily/transport.py b/src/pipecat/transports/daily/transport.py index 739d47b59..779a7e622 100644 --- a/src/pipecat/transports/daily/transport.py +++ b/src/pipecat/transports/daily/transport.py @@ -2182,10 +2182,10 @@ class DailyOutputTransport(BaseOutputTransport): ``digit_duration_ms`` fields are also forwarded to the Daily call client. """ - if not frame.tones: + if not frame.buttons: 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 frame.session_id is not None: settings["sessionId"] = frame.session_id diff --git a/src/pipecat/transports/livekit/transport.py b/src/pipecat/transports/livekit/transport.py index 0e904066a..04e99f4c0 100644 --- a/src/pipecat/transports/livekit/transport.py +++ b/src/pipecat/transports/livekit/transport.py @@ -898,15 +898,16 @@ class LiveKitOutputTransport(BaseOutputTransport): async def _write_dtmf_native(self, frame: OutputDTMFFrame | OutputDTMFUrgentFrame): """Use LiveKit's native publish_dtmf method for telephone events. - LiveKit's DTMF API sends a single tone per call, so when ``frame.tones`` - contains multiple characters only the first one is sent. + LiveKit's DTMF API sends a single tone per call, so when + ``frame.buttons`` contains multiple entries only the first one is + sent. Args: frame: The DTMF frame to write. """ - if not frame.tones: + if not frame.buttons: 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: """Convert Pipecat audio data to LiveKit audio frame.""" From 9fbe1bf2a3aadf74294963b9c9e2308bcce50b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 15 Apr 2026 15:09:01 -0700 Subject: [PATCH 6/9] Document `button` as a convenience shortcut, not a deprecation The single-key `button` field on `OutputDTMFFrame` and `OutputDTMFUrgentFrame` is kept as a first-class ergonomic shortcut for the common single-keypress case, equivalent to `buttons=[button]`. `buttons` takes precedence when both are set. --- changelog/4313.deprecated.md | 1 - src/pipecat/frames/frames.py | 16 ++++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) delete mode 100644 changelog/4313.deprecated.md diff --git a/changelog/4313.deprecated.md b/changelog/4313.deprecated.md deleted file mode 100644 index 58b826c6f..000000000 --- a/changelog/4313.deprecated.md +++ /dev/null @@ -1 +0,0 @@ -- 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. diff --git a/src/pipecat/frames/frames.py b/src/pipecat/frames/frames.py index 68574b6a5..230fe9c8f 100644 --- a/src/pipecat/frames/frames.py +++ b/src/pipecat/frames/frames.py @@ -747,11 +747,9 @@ class OutputDTMFFrame(DTMFFrame, DataFrame): Parameters: buttons: Sequence of one or more DTMF keypad buttons to send. Use :meth:`from_string` to build this from a string like ``"123#"``. - button: A single DTMF keypad entry to send. - - .. deprecated:: 1.1.0 - Use ``buttons`` instead. When only ``button`` is set, it is - used as a single-entry ``buttons`` list. + button: Convenience shortcut for sending a single DTMF keypad + entry. Equivalent to ``buttons=[button]``. If both ``buttons`` + and ``button`` are provided, ``buttons`` takes precedence. """ button: Optional[KeypadEntry] = None @@ -1284,11 +1282,9 @@ class OutputDTMFUrgentFrame(DTMFFrame, SystemFrame): Parameters: buttons: Sequence of one or more DTMF keypad buttons to send. Use :meth:`from_string` to build this from a string like ``"123#"``. - button: A single DTMF keypad entry to send. - - .. deprecated:: 1.1.0 - Use ``buttons`` instead. When only ``button`` is set, it is - used as a single-entry ``buttons`` list. + button: Convenience shortcut for sending a single DTMF keypad + entry. Equivalent to ``buttons=[button]``. If both ``buttons`` + and ``button`` are provided, ``buttons`` takes precedence. """ button: Optional[KeypadEntry] = None From f094ce80fb3191c63671bda105675abe2a925fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 15 Apr 2026 15:14:47 -0700 Subject: [PATCH 7/9] Add `to_string` helper on output DTMF frames Mirrors the existing `from_string` classmethod and lets callers turn a frame's `buttons` list back into a dial string like `"123#"`. `__str__` and the Daily transport's native DTMF path reuse it. --- src/pipecat/frames/frames.py | 34 +++++++++++++++++------ src/pipecat/transports/daily/transport.py | 4 +-- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/pipecat/frames/frames.py b/src/pipecat/frames/frames.py index 230fe9c8f..21780d6cb 100644 --- a/src/pipecat/frames/frames.py +++ b/src/pipecat/frames/frames.py @@ -745,11 +745,11 @@ class OutputDTMFFrame(DTMFFrame, DataFrame): """DTMF keypress output frame for transport queuing. Parameters: - buttons: Sequence of one or more DTMF keypad buttons to send. Use - :meth:`from_string` to build this from a string like ``"123#"``. button: Convenience shortcut for sending a single DTMF keypad entry. Equivalent to ``buttons=[button]``. If both ``buttons`` and ``button`` are provided, ``buttons`` takes precedence. + buttons: Sequence of one or more DTMF keypad buttons to send. Use + :meth:`from_string` to build this from a string like ``"123#"``. """ button: Optional[KeypadEntry] = None @@ -763,8 +763,7 @@ class OutputDTMFFrame(DTMFFrame, DataFrame): raise ValueError(f"{self.__class__.__name__} requires `buttons` or `button` to be set") def __str__(self): - buttons_str = "".join(b.value for b in self.buttons) if self.buttons else "" - return f"{self.name}(buttons: {buttons_str})" + return f"{self.name}(buttons: {self.to_string()})" @classmethod def from_string(cls, buttons: str, **kwargs) -> "OutputDTMFFrame": @@ -782,6 +781,16 @@ class OutputDTMFFrame(DTMFFrame, DataFrame): """ return cls(buttons=[KeypadEntry(c) for c in buttons], **kwargs) + def to_string(self) -> str: + """Return the frame's ``buttons`` as a dial string. + + Returns: + A string such as ``"123#"`` formed by concatenating the values + of each :class:`~pipecat.audio.dtmf.types.KeypadEntry` in + ``buttons``, or an empty string if ``buttons`` is not set. + """ + return "".join(b.value for b in self.buttons) if self.buttons else "" + # # System frames @@ -1280,11 +1289,11 @@ class OutputDTMFUrgentFrame(DTMFFrame, SystemFrame): """DTMF keypress output frame for immediate sending. Parameters: - buttons: Sequence of one or more DTMF keypad buttons to send. Use - :meth:`from_string` to build this from a string like ``"123#"``. button: Convenience shortcut for sending a single DTMF keypad entry. Equivalent to ``buttons=[button]``. If both ``buttons`` and ``button`` are provided, ``buttons`` takes precedence. + buttons: Sequence of one or more DTMF keypad buttons to send. Use + :meth:`from_string` to build this from a string like ``"123#"``. """ button: Optional[KeypadEntry] = None @@ -1298,8 +1307,7 @@ class OutputDTMFUrgentFrame(DTMFFrame, SystemFrame): raise ValueError(f"{self.__class__.__name__} requires `buttons` or `button` to be set") def __str__(self): - buttons_str = "".join(b.value for b in self.buttons) if self.buttons else "" - return f"{self.name}(buttons: {buttons_str})" + return f"{self.name}(buttons: {self.to_string()})" @classmethod def from_string(cls, buttons: str, **kwargs) -> "OutputDTMFUrgentFrame": @@ -1317,6 +1325,16 @@ class OutputDTMFUrgentFrame(DTMFFrame, SystemFrame): """ return cls(buttons=[KeypadEntry(c) for c in buttons], **kwargs) + def to_string(self) -> str: + """Return the frame's ``buttons`` as a dial string. + + Returns: + A string such as ``"123#"`` formed by concatenating the values + of each :class:`~pipecat.audio.dtmf.types.KeypadEntry` in + ``buttons``, or an empty string if ``buttons`` is not set. + """ + return "".join(b.value for b in self.buttons) if self.buttons else "" + @dataclass class SpeechControlParamsFrame(SystemFrame): diff --git a/src/pipecat/transports/daily/transport.py b/src/pipecat/transports/daily/transport.py index 779a7e622..e96799b37 100644 --- a/src/pipecat/transports/daily/transport.py +++ b/src/pipecat/transports/daily/transport.py @@ -2185,14 +2185,12 @@ class DailyOutputTransport(BaseOutputTransport): if not frame.buttons: return - settings: Dict[str, Any] = {"tones": "".join(b.value for b in frame.buttons)} + settings: Dict[str, Any] = {"tones": frame.to_string()} if isinstance(frame, (DailyOutputDTMFFrame, DailyOutputDTMFUrgentFrame)): if frame.session_id is not None: settings["sessionId"] = frame.session_id if frame.digit_duration_ms is not None: settings["digitDurationMs"] = frame.digit_duration_ms - elif frame.transport_destination is not None: - settings["sessionId"] = frame.transport_destination await self._client.send_dtmf(settings) From b11a3bc43f54e0cb4c34f99c017a85ef8531d5cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 15 Apr 2026 15:16:32 -0700 Subject: [PATCH 8/9] Add `method` field to Daily DTMF output frames Lets callers specify Daily's DTMF delivery method (e.g. "rfc2833" or "info") alongside `session_id` and `digit_duration_ms`. Forwarded to Daily's `send_dtmf` as `method`. --- changelog/4313.added.md | 2 +- src/pipecat/transports/daily/transport.py | 27 +++++++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/changelog/4313.added.md b/changelog/4313.added.md index 1c704a3a7..738744a80 100644 --- a/changelog/4313.added.md +++ b/changelog/4313.added.md @@ -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 `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 `buttons`, 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`, `digit_duration_ms` and `method`, which are forwarded to Daily's `send_dtmf` as `sessionId`, `digitDurationMs` and `method`. diff --git a/src/pipecat/transports/daily/transport.py b/src/pipecat/transports/daily/transport.py index e96799b37..b58701a0b 100644 --- a/src/pipecat/transports/daily/transport.py +++ b/src/pipecat/transports/daily/transport.py @@ -158,38 +158,45 @@ class DailyOutputDTMFFrame(OutputDTMFFrame): """DTMF output frame with Daily-specific options for transport queuing. A DTMF keypress output that will be queued after any preceding audio has - finished playing. Inherits ``tones`` from :class:`OutputDTMFFrame`; the - two extra fields are forwarded to Daily's ``send_dtmf`` as ``sessionId`` - and ``digitDurationMs``. + finished playing. Inherits ``buttons`` from :class:`OutputDTMFFrame`; the + extra fields are forwarded to Daily's ``send_dtmf`` as ``sessionId``, + ``digitDurationMs`` and ``method``. Parameters: session_id: Target participant session id. When ``None``, Daily sends the tones to the default destination for the call. digit_duration_ms: Duration of each DTMF digit in milliseconds. When ``None``, Daily's default duration is used. + method: DTMF delivery method (e.g. ``"telephone-event"``, ``"sip-info"`` + or ``auto``). When ``None``, Daily's default method is used. """ session_id: Optional[str] = None digit_duration_ms: Optional[int] = None + method: Optional[str] = None @dataclass class DailyOutputDTMFUrgentFrame(OutputDTMFUrgentFrame): """DTMF output frame with Daily-specific options for immediate sending. - A DTMF keypress output that will be sent right away. Inherits ``tones`` - from :class:`OutputDTMFUrgentFrame`; the two extra fields are forwarded - to Daily's ``send_dtmf`` as ``sessionId`` and ``digitDurationMs``. + A DTMF keypress output that will be sent right away. Inherits + ``buttons`` from :class:`OutputDTMFUrgentFrame`; the extra fields are + forwarded to Daily's ``send_dtmf`` as ``sessionId``, ``digitDurationMs`` + and ``method``. Parameters: session_id: Target participant session id. When ``None``, Daily sends the tones to the default destination for the call. digit_duration_ms: Duration of each DTMF digit in milliseconds. When ``None``, Daily's default duration is used. + method: DTMF delivery method (e.g. ``"telephone-event"``, ``"sip-info"`` + or ``auto``). When ``None``, Daily's default method is used. """ session_id: Optional[str] = None digit_duration_ms: Optional[int] = None + method: Optional[str] = None class WebRTCVADAnalyzer(VADAnalyzer): @@ -2178,9 +2185,9 @@ class DailyOutputTransport(BaseOutputTransport): Args: frame: The DTMF frame to write. When it is a :class:`DailyOutputDTMFFrame` or - :class:`DailyOutputDTMFUrgentFrame`, the ``session_id`` and - ``digit_duration_ms`` fields are also forwarded to the Daily - call client. + :class:`DailyOutputDTMFUrgentFrame`, the ``session_id``, + ``digit_duration_ms`` and ``method`` fields are also + forwarded to the Daily call client. """ if not frame.buttons: return @@ -2191,6 +2198,8 @@ class DailyOutputTransport(BaseOutputTransport): settings["sessionId"] = frame.session_id if frame.digit_duration_ms is not None: settings["digitDurationMs"] = frame.digit_duration_ms + if frame.method is not None: + settings["method"] = frame.method await self._client.send_dtmf(settings) From 8d4feede235f3b098df12bac9dc3197fca0c7ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 15 Apr 2026 15:21:26 -0700 Subject: [PATCH 9/9] Split #4313 changelog into one entry per file --- changelog/4313.added.2.md | 1 + changelog/4313.added.3.md | 1 + changelog/4313.added.md | 2 -- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 changelog/4313.added.2.md create mode 100644 changelog/4313.added.3.md diff --git a/changelog/4313.added.2.md b/changelog/4313.added.2.md new file mode 100644 index 000000000..2093e0a8a --- /dev/null +++ b/changelog/4313.added.2.md @@ -0,0 +1 @@ +- 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, and `to_string()` to convert back. diff --git a/changelog/4313.added.3.md b/changelog/4313.added.3.md new file mode 100644 index 000000000..8dc2c89f1 --- /dev/null +++ b/changelog/4313.added.3.md @@ -0,0 +1 @@ +- Added `DailyOutputDTMFFrame` and `DailyOutputDTMFUrgentFrame` frames. In addition to the inherited `buttons`, they accept `session_id`, `digit_duration_ms` and `method`, which are forwarded to Daily's `send_dtmf` as `sessionId`, `digitDurationMs` and `method`. diff --git a/changelog/4313.added.md b/changelog/4313.added.md index 738744a80..36e4b4f2e 100644 --- a/changelog/4313.added.md +++ b/changelog/4313.added.md @@ -1,3 +1 @@ - 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 `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 `buttons`, they accept `session_id`, `digit_duration_ms` and `method`, which are forwarded to Daily's `send_dtmf` as `sessionId`, `digitDurationMs` and `method`.