diff --git a/CHANGELOG.md b/CHANGELOG.md index aae14f431..abf343513 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- It is now possible to push `OutputDTMFFrame` or `OutputDTMFUrgentFrame` with + `DailyTransport`. This will be sent properly if a Daily dial-out connection + has been established. + +- Added `OutputDTMFUrgentFrame` to send a DTMF keypress quickly. The previous + `OutputDTMFFrame` queues the keypress with the rest of data frames. + - Added new functions `DailyTransport.start_transcription()` and `DailyTransport.stop_transcription()` to be able to start and stop Daily transcription dynamically (maybe with different settings). +### Deprecated + +- `DailyTransport.send_dtmf()` is deprecated, push an `OutputDTMFFrame` or an + `OutputDTMFUrgentFrame` instead. + ### Fixed - In `AWSBedrockLLMService`, worked around a possible bug in AWS Bedrock where diff --git a/src/pipecat/frames/frames.py b/src/pipecat/frames/frames.py index 18bc2874b..758ba7556 100644 --- a/src/pipecat/frames/frames.py +++ b/src/pipecat/frames/frames.py @@ -413,22 +413,19 @@ class TransportMessageFrame(DataFrame): @dataclass -class DTMFFrame(DataFrame): +class DTMFFrame: """A DTMF button frame""" button: KeypadEntry @dataclass -class InputDTMFFrame(DTMFFrame): - """A DTMF button input""" +class OutputDTMFFrame(DTMFFrame, DataFrame): + """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. - pass - - -@dataclass -class OutputDTMFFrame(DTMFFrame): - """A DTMF button output""" + """ pass @@ -777,6 +774,24 @@ class VisionImageRawFrame(InputImageRawFrame): return f"{self.name}(pts: {pts}, text: [{self.text}], size: {self.size}, format: {self.format})" +@dataclass +class InputDTMFFrame(DTMFFrame, SystemFrame): + """A DTMF keypress input.""" + + pass + + +@dataclass +class OutputDTMFUrgentFrame(DTMFFrame, SystemFrame): + """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. + + """ + + pass + + # # Control frames # diff --git a/src/pipecat/transports/base_output.py b/src/pipecat/transports/base_output.py index f26602ed7..ba8eb4916 100644 --- a/src/pipecat/transports/base_output.py +++ b/src/pipecat/transports/base_output.py @@ -25,6 +25,8 @@ from pipecat.frames.frames import ( Frame, MixerControlFrame, OutputAudioRawFrame, + OutputDTMFFrame, + OutputDTMFUrgentFrame, OutputImageRawFrame, SpriteFrame, StartFrame, @@ -140,6 +142,9 @@ class BaseOutputTransport(FrameProcessor): async def write_raw_audio_frames(self, frames: bytes, destination: Optional[str] = None): pass + async def write_dtmf(self, frame: OutputDTMFFrame | OutputDTMFUrgentFrame): + pass + async def send_audio(self, frame: OutputAudioRawFrame): await self.queue_frame(frame, FrameDirection.DOWNSTREAM) @@ -171,6 +176,8 @@ class BaseOutputTransport(FrameProcessor): await self._handle_frame(frame) elif isinstance(frame, TransportMessageUrgentFrame): await self.send_message(frame) + elif isinstance(frame, OutputDTMFUrgentFrame): + await self.write_dtmf(frame) elif isinstance(frame, SystemFrame): await self.push_frame(frame, direction) # Control frames. @@ -425,6 +432,8 @@ class BaseOutputTransport(FrameProcessor): await self._set_video_images(frame.images) elif isinstance(frame, TransportMessageFrame): await self._transport.send_message(frame) + elif isinstance(frame, OutputDTMFFrame): + await self._transport.write_dtmf(frame) def _next_frame(self) -> AsyncGenerator[Frame, None]: async def without_mixer(vad_stop_secs: float) -> AsyncGenerator[Frame, None]: diff --git a/src/pipecat/transports/services/daily.py b/src/pipecat/transports/services/daily.py index 0011dd2fd..075d57dfd 100644 --- a/src/pipecat/transports/services/daily.py +++ b/src/pipecat/transports/services/daily.py @@ -22,6 +22,8 @@ from pipecat.frames.frames import ( Frame, InterimTranscriptionFrame, OutputAudioRawFrame, + OutputDTMFFrame, + OutputDTMFUrgentFrame, OutputImageRawFrame, SpriteFrame, StartFrame, @@ -1220,6 +1222,14 @@ class DailyOutputTransport(BaseOutputTransport): async def register_audio_destination(self, destination: str): await self._client.register_audio_destination(destination) + async def write_dtmf(self, frame: OutputDTMFFrame | OutputDTMFUrgentFrame): + await self._client.send_dtmf( + { + "sessionId": frame.transport_destination, + "tones": frame.button.value, + } + ) + async def write_raw_audio_frames(self, frames: bytes, destination: Optional[str] = None): await self._client.write_raw_audio_frames(frames, destination) @@ -1372,6 +1382,14 @@ class DailyTransport(BaseTransport): await self._client.stop_dialout(participant_id) async def send_dtmf(self, settings): + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "`DailyTransport.send_dtmf()` is deprecated, push an `OutputDTMFFrame` or an `OutputDTMFUrgentFrame` instead.", + DeprecationWarning, + ) await self._client.send_dtmf(settings) async def sip_call_transfer(self, settings):