Merge pull request #1914 from pipecat-ai/aleix/base-output-daily-handle-dtmf-frames
output transport can now handle DTMF keypress
This commit is contained in:
12
CHANGELOG.md
12
CHANGELOG.md
@@ -9,10 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### 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
|
- Added new functions `DailyTransport.start_transcription()` and
|
||||||
`DailyTransport.stop_transcription()` to be able to start and stop Daily
|
`DailyTransport.stop_transcription()` to be able to start and stop Daily
|
||||||
transcription dynamically (maybe with different settings).
|
transcription dynamically (maybe with different settings).
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- `DailyTransport.send_dtmf()` is deprecated, push an `OutputDTMFFrame` or an
|
||||||
|
`OutputDTMFUrgentFrame` instead.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- In `AWSBedrockLLMService`, worked around a possible bug in AWS Bedrock where
|
- In `AWSBedrockLLMService`, worked around a possible bug in AWS Bedrock where
|
||||||
|
|||||||
@@ -413,22 +413,19 @@ class TransportMessageFrame(DataFrame):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DTMFFrame(DataFrame):
|
class DTMFFrame:
|
||||||
"""A DTMF button frame"""
|
"""A DTMF button frame"""
|
||||||
|
|
||||||
button: KeypadEntry
|
button: KeypadEntry
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class InputDTMFFrame(DTMFFrame):
|
class OutputDTMFFrame(DTMFFrame, DataFrame):
|
||||||
"""A DTMF button input"""
|
"""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
|
pass
|
||||||
|
|
||||||
@@ -777,6 +774,24 @@ class VisionImageRawFrame(InputImageRawFrame):
|
|||||||
return f"{self.name}(pts: {pts}, text: [{self.text}], size: {self.size}, format: {self.format})"
|
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
|
# Control frames
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ from pipecat.frames.frames import (
|
|||||||
Frame,
|
Frame,
|
||||||
MixerControlFrame,
|
MixerControlFrame,
|
||||||
OutputAudioRawFrame,
|
OutputAudioRawFrame,
|
||||||
|
OutputDTMFFrame,
|
||||||
|
OutputDTMFUrgentFrame,
|
||||||
OutputImageRawFrame,
|
OutputImageRawFrame,
|
||||||
SpriteFrame,
|
SpriteFrame,
|
||||||
StartFrame,
|
StartFrame,
|
||||||
@@ -140,6 +142,9 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
async def write_raw_audio_frames(self, frames: bytes, destination: Optional[str] = None):
|
async def write_raw_audio_frames(self, frames: bytes, destination: Optional[str] = None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def write_dtmf(self, frame: OutputDTMFFrame | OutputDTMFUrgentFrame):
|
||||||
|
pass
|
||||||
|
|
||||||
async def send_audio(self, frame: OutputAudioRawFrame):
|
async def send_audio(self, frame: OutputAudioRawFrame):
|
||||||
await self.queue_frame(frame, FrameDirection.DOWNSTREAM)
|
await self.queue_frame(frame, FrameDirection.DOWNSTREAM)
|
||||||
|
|
||||||
@@ -171,6 +176,8 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
await self._handle_frame(frame)
|
await self._handle_frame(frame)
|
||||||
elif isinstance(frame, TransportMessageUrgentFrame):
|
elif isinstance(frame, TransportMessageUrgentFrame):
|
||||||
await self.send_message(frame)
|
await self.send_message(frame)
|
||||||
|
elif isinstance(frame, OutputDTMFUrgentFrame):
|
||||||
|
await self.write_dtmf(frame)
|
||||||
elif isinstance(frame, SystemFrame):
|
elif isinstance(frame, SystemFrame):
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
# Control frames.
|
# Control frames.
|
||||||
@@ -425,6 +432,8 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
await self._set_video_images(frame.images)
|
await self._set_video_images(frame.images)
|
||||||
elif isinstance(frame, TransportMessageFrame):
|
elif isinstance(frame, TransportMessageFrame):
|
||||||
await self._transport.send_message(frame)
|
await self._transport.send_message(frame)
|
||||||
|
elif isinstance(frame, OutputDTMFFrame):
|
||||||
|
await self._transport.write_dtmf(frame)
|
||||||
|
|
||||||
def _next_frame(self) -> AsyncGenerator[Frame, None]:
|
def _next_frame(self) -> AsyncGenerator[Frame, None]:
|
||||||
async def without_mixer(vad_stop_secs: float) -> AsyncGenerator[Frame, None]:
|
async def without_mixer(vad_stop_secs: float) -> AsyncGenerator[Frame, None]:
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ from pipecat.frames.frames import (
|
|||||||
Frame,
|
Frame,
|
||||||
InterimTranscriptionFrame,
|
InterimTranscriptionFrame,
|
||||||
OutputAudioRawFrame,
|
OutputAudioRawFrame,
|
||||||
|
OutputDTMFFrame,
|
||||||
|
OutputDTMFUrgentFrame,
|
||||||
OutputImageRawFrame,
|
OutputImageRawFrame,
|
||||||
SpriteFrame,
|
SpriteFrame,
|
||||||
StartFrame,
|
StartFrame,
|
||||||
@@ -1220,6 +1222,14 @@ class DailyOutputTransport(BaseOutputTransport):
|
|||||||
async def register_audio_destination(self, destination: str):
|
async def register_audio_destination(self, destination: str):
|
||||||
await self._client.register_audio_destination(destination)
|
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):
|
async def write_raw_audio_frames(self, frames: bytes, destination: Optional[str] = None):
|
||||||
await self._client.write_raw_audio_frames(frames, destination)
|
await self._client.write_raw_audio_frames(frames, destination)
|
||||||
|
|
||||||
@@ -1372,6 +1382,14 @@ class DailyTransport(BaseTransport):
|
|||||||
await self._client.stop_dialout(participant_id)
|
await self._client.stop_dialout(participant_id)
|
||||||
|
|
||||||
async def send_dtmf(self, settings):
|
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)
|
await self._client.send_dtmf(settings)
|
||||||
|
|
||||||
async def sip_call_transfer(self, settings):
|
async def sip_call_transfer(self, settings):
|
||||||
|
|||||||
Reference in New Issue
Block a user