98 lines
3.4 KiB
Python
98 lines
3.4 KiB
Python
#
|
||
# Copyright (c) 2024–2025, Daily
|
||
#
|
||
# SPDX-License-Identifier: BSD 2-Clause License
|
||
#
|
||
|
||
import base64
|
||
import json
|
||
from typing import Optional
|
||
|
||
from pydantic import BaseModel
|
||
|
||
from pipecat.audio.utils import create_default_resampler, pcm_to_ulaw, ulaw_to_pcm
|
||
from pipecat.frames.frames import (
|
||
AudioRawFrame,
|
||
Frame,
|
||
InputAudioRawFrame,
|
||
InputDTMFFrame,
|
||
KeypadEntry,
|
||
StartFrame,
|
||
StartInterruptionFrame,
|
||
TransportMessageFrame,
|
||
TransportMessageUrgentFrame,
|
||
)
|
||
from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializerType
|
||
|
||
|
||
class TwilioFrameSerializer(FrameSerializer):
|
||
class InputParams(BaseModel):
|
||
twilio_sample_rate: Optional[int] = None # Default Twilio rate (8kHz)
|
||
sample_rate: Optional[int] = None # Pipeline input rate
|
||
|
||
def __init__(self, stream_sid: str, params: InputParams = InputParams()):
|
||
self._stream_sid = stream_sid
|
||
self._params = params
|
||
|
||
self._twilio_sample_rate = 0 # Fixed rate for Twilio (8kHz)
|
||
self._sample_rate = 0 # Pipeline input rate
|
||
|
||
self._resampler = create_default_resampler()
|
||
|
||
@property
|
||
def type(self) -> FrameSerializerType:
|
||
return FrameSerializerType.TEXT
|
||
|
||
async def setup(self, frame: StartFrame):
|
||
# Configure rates for input path: Twilio (8kHz μ-law) -> Pipeline (PCM)
|
||
self._twilio_sample_rate = self._params.twilio_sample_rate or frame.audio_in_sample_rate
|
||
self._sample_rate = self._params.sample_rate or frame.audio_in_sample_rate
|
||
|
||
async def serialize(self, frame: Frame) -> str | bytes | None:
|
||
if isinstance(frame, StartInterruptionFrame):
|
||
answer = {"event": "clear", "streamSid": self._stream_sid}
|
||
return json.dumps(answer)
|
||
elif isinstance(frame, AudioRawFrame):
|
||
data = frame.audio
|
||
|
||
# Output: Convert PCM at frame's rate to 8kHz μ-law for Twilio
|
||
serialized_data = await pcm_to_ulaw(
|
||
data, frame.sample_rate, self._twilio_sample_rate, self._resampler
|
||
)
|
||
payload = base64.b64encode(serialized_data).decode("utf-8")
|
||
answer = {
|
||
"event": "media",
|
||
"streamSid": self._stream_sid,
|
||
"media": {"payload": payload},
|
||
}
|
||
|
||
return json.dumps(answer)
|
||
elif isinstance(frame, (TransportMessageFrame, TransportMessageUrgentFrame)):
|
||
return json.dumps(frame.message)
|
||
|
||
async def deserialize(self, data: str | bytes) -> Frame | None:
|
||
message = json.loads(data)
|
||
|
||
if message["event"] == "media":
|
||
payload_base64 = message["media"]["payload"]
|
||
payload = base64.b64decode(payload_base64)
|
||
|
||
# Input: Convert Twilio's 8kHz μ-law to PCM at pipeline input rate
|
||
deserialized_data = await ulaw_to_pcm(
|
||
payload, self._twilio_sample_rate, self._sample_rate, self._resampler
|
||
)
|
||
audio_frame = InputAudioRawFrame(
|
||
audio=deserialized_data, num_channels=1, sample_rate=self._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
|