From 5cb6aecc9fc226bca3f2a22b69a3dc4b22c326a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Mon, 16 Mar 2026 17:57:39 -0700 Subject: [PATCH] Add DTMF input event support to Daily transport Handle Daily's on_dtmf_event callback, convert it to an InputDTMFFrame pushed into the input transport. Also add __str__ methods to InputDTMFFrame and OutputDTMFFrame for better logging. --- src/pipecat/frames/frames.py | 6 ++++-- src/pipecat/transports/daily/transport.py | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/pipecat/frames/frames.py b/src/pipecat/frames/frames.py index 1ef7bdb4a..fbc01c488 100644 --- a/src/pipecat/frames/frames.py +++ b/src/pipecat/frames/frames.py @@ -1001,7 +1001,8 @@ class OutputDTMFFrame(DTMFFrame, DataFrame): specify where the DTMF keypress should be sent. """ - pass + def __str__(self): + return f"{self.name}(tone: {self.button})" # @@ -1658,7 +1659,8 @@ class AssistantImageRawFrame(OutputImageRawFrame): class InputDTMFFrame(DTMFFrame, SystemFrame): """DTMF keypress input frame from transport.""" - pass + def __str__(self): + return f"{self.name}(tone: {self.button.value})" @dataclass diff --git a/src/pipecat/transports/daily/transport.py b/src/pipecat/transports/daily/transport.py index 4626f9f53..2798f6d5e 100644 --- a/src/pipecat/transports/daily/transport.py +++ b/src/pipecat/transports/daily/transport.py @@ -22,6 +22,7 @@ import aiohttp from loguru import logger from pydantic import BaseModel +from pipecat.audio.dtmf.types import KeypadEntry from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADParams from pipecat.frames.frames import ( BotConnectedFrame, @@ -31,6 +32,7 @@ from pipecat.frames.frames import ( EndFrame, Frame, InputAudioRawFrame, + InputDTMFFrame, InputTransportMessageFrame, InterimTranscriptionFrame, OutputAudioRawFrame, @@ -394,6 +396,7 @@ class DailyCallbacks(BaseModel): on_dialout_stopped: Called when dial-out is stopped. on_dialout_error: Called when dial-out encounters an error. on_dialout_warning: Called when dial-out has a warning. + on_dtmf_event: Called when a DTMF tone happens. on_participant_joined: Called when a participant joins. on_participant_left: Called when a participant leaves. on_participant_updated: Called when participant info is updated. @@ -424,6 +427,7 @@ class DailyCallbacks(BaseModel): on_dialout_stopped: Callable[[Any], Awaitable[None]] on_dialout_error: Callable[[Any], Awaitable[None]] on_dialout_warning: Callable[[Any], Awaitable[None]] + on_dtmf_event: Callable[[Any], Awaitable[None]] on_participant_joined: Callable[[Mapping[str, Any]], Awaitable[None]] on_participant_left: Callable[[Mapping[str, Any], str], Awaitable[None]] on_participant_updated: Callable[[Mapping[str, Any]], Awaitable[None]] @@ -1560,6 +1564,14 @@ class DailyTransportClient(EventHandler): """ self._call_event_callback(self._callbacks.on_dialout_warning, data) + def on_dtmf_event(self, data: Any): + """Handle incoming DTMF events. + + Args: + data: DTMF data. + """ + self._call_event_callback(self._callbacks.on_dtmf_event, data) + def on_participant_joined(self, participant): """Handle participant joined events. @@ -2313,6 +2325,7 @@ class DailyTransport(BaseTransport): on_dialout_stopped=self._on_dialout_stopped, on_dialout_error=self._on_dialout_error, on_dialout_warning=self._on_dialout_warning, + on_dtmf_event=self._on_dtmf_event, on_participant_joined=self._on_participant_joined, on_participant_left=self._on_participant_left, on_participant_updated=self._on_participant_updated, @@ -2354,6 +2367,7 @@ class DailyTransport(BaseTransport): self._register_event_handler("on_dialout_stopped") self._register_event_handler("on_dialout_error") self._register_event_handler("on_dialout_warning") + self._register_event_handler("on_dtmf_event") self._register_event_handler("on_first_participant_joined") self._register_event_handler("on_participant_joined") self._register_event_handler("on_participant_left") @@ -2864,6 +2878,15 @@ class DailyTransport(BaseTransport): logger.warning(f"{self} dial-out warning: {data}") await self._call_event_handler("on_dialout_warning", data) + async def _on_dtmf_event(self, data): + """Handle incoming DTMF events.""" + logger.debug(f"{self} DTMF event: {data}") + await self._call_event_handler("on_dtmf_event", data) + + if self._input: + frame = InputDTMFFrame(button=KeypadEntry(data["tone"])) + await self._input.push_frame(frame) + async def _on_participant_joined(self, participant): """Handle participant joined events.""" id = participant["id"]