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.
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ from pipecat.frames.frames import (
|
|||||||
InputTransportMessageFrame,
|
InputTransportMessageFrame,
|
||||||
InterimTranscriptionFrame,
|
InterimTranscriptionFrame,
|
||||||
OutputAudioRawFrame,
|
OutputAudioRawFrame,
|
||||||
|
OutputDTMFFrame,
|
||||||
|
OutputDTMFUrgentFrame,
|
||||||
OutputImageRawFrame,
|
OutputImageRawFrame,
|
||||||
OutputTransportMessageFrame,
|
OutputTransportMessageFrame,
|
||||||
OutputTransportMessageUrgentFrame,
|
OutputTransportMessageUrgentFrame,
|
||||||
@@ -151,6 +153,78 @@ class DailyUpdateRemoteParticipantsFrame(DataFrame):
|
|||||||
remote_participants: Mapping[str, Any] = field(default_factory=dict)
|
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):
|
class WebRTCVADAnalyzer(VADAnalyzer):
|
||||||
"""Voice Activity Detection analyzer using WebRTC.
|
"""Voice Activity Detection analyzer using WebRTC.
|
||||||
|
|
||||||
@@ -2131,18 +2205,31 @@ class DailyOutputTransport(BaseOutputTransport):
|
|||||||
"""
|
"""
|
||||||
return True
|
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.
|
"""Use Daily's native send_dtmf method for telephone events.
|
||||||
|
|
||||||
Args:
|
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(
|
if isinstance(frame, (DailyOutputDTMFFrame, DailyOutputDTMFUrgentFrame)):
|
||||||
{
|
settings: Dict[str, Any] = {"tones": frame.tones}
|
||||||
"sessionId": frame.transport_destination,
|
if frame.session_id is not None:
|
||||||
"tones": frame.button.value,
|
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):
|
class DailyTransport(BaseTransport):
|
||||||
|
|||||||
Reference in New Issue
Block a user