diff --git a/src/pipecat/frames/frames.py b/src/pipecat/frames/frames.py index e2ac25e7a..6f0861a37 100644 --- a/src/pipecat/frames/frames.py +++ b/src/pipecat/frames/frames.py @@ -5,6 +5,7 @@ # from dataclasses import dataclass, field +from enum import Enum from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Literal, Mapping, Optional, Tuple from pipecat.audio.vad.vad_analyzer import VADParams @@ -18,6 +19,23 @@ if TYPE_CHECKING: from pipecat.observers.base_observer import BaseObserver +class KeypadEntry(str, Enum): + """DTMF entries.""" + + ONE = "1" + TWO = "2" + THREE = "3" + FOUR = "4" + FIVE = "5" + SIX = "6" + SEVEN = "7" + EIGHT = "8" + NINE = "9" + ZERO = "0" + POUND = "#" + STAR = "*" + + def format_pts(pts: int | None): return nanoseconds_to_str(pts) if pts else None @@ -615,6 +633,13 @@ class VisionImageRawFrame(InputImageRawFrame): return f"{self.name}(pts: {pts}, text: [{self.text}], size: {self.size}, format: {self.format})" +@dataclass +class InputDTMFFrame(Frame): + """A DTMF button input""" + + button: KeypadEntry + + # # Control frames # diff --git a/src/pipecat/serializers/twilio.py b/src/pipecat/serializers/twilio.py index 558a68046..298e8a6cb 100644 --- a/src/pipecat/serializers/twilio.py +++ b/src/pipecat/serializers/twilio.py @@ -10,7 +10,14 @@ import json from pydantic import BaseModel from pipecat.audio.utils import pcm_to_ulaw, ulaw_to_pcm -from pipecat.frames.frames import AudioRawFrame, Frame, InputAudioRawFrame, StartInterruptionFrame +from pipecat.frames.frames import ( + AudioRawFrame, + Frame, + InputAudioRawFrame, + InputDTMFFrame, + KeypadEntry, + StartInterruptionFrame, +) from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializerType @@ -48,9 +55,7 @@ class TwilioFrameSerializer(FrameSerializer): def deserialize(self, data: str | bytes) -> Frame | None: message = json.loads(data) - if message["event"] != "media": - return None - else: + if message["event"] == "media": payload_base64 = message["media"]["payload"] payload = base64.b64decode(payload_base64) @@ -61,3 +66,13 @@ class TwilioFrameSerializer(FrameSerializer): audio=deserialized_data, num_channels=1, sample_rate=self._params.sample_rate ) return audio_frame + elif message["event"] == "dtmf": + digit = message.get("dtmf", {}).get("digit") + + try: + return InputDTMFFrame(KeypadEntry(digit)) + except ValueError as e: + # Handle case where string doesn't match any enum value + return None + else: + return None