diff --git a/pyproject.toml b/pyproject.toml index 862981c7b..b6ec2577c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ Website = "https://pipecat.ai" [project.optional-dependencies] anthropic = [ "anthropic~=0.40.0" ] assemblyai = [ "assemblyai~=0.34.0" ] +audio = [ "soundfile~=0.12.1" ] aws = [ "boto3~=1.35.27" ] azure = [ "azure-cognitiveservices-speech~=1.41.1", "openai~=1.57.2" ] canonical = [ "aiofiles~=24.1.0" ] diff --git a/src/pipecat/audio/utils.py b/src/pipecat/audio/utils.py index 057942e04..78a5e4060 100644 --- a/src/pipecat/audio/utils.py +++ b/src/pipecat/audio/utils.py @@ -4,7 +4,8 @@ # SPDX-License-Identifier: BSD 2-Clause License # -import audioop +import sys + import numpy as np import pyloudnorm as pyln import resampy @@ -74,21 +75,50 @@ def exp_smoothing(value: float, prev_value: float, factor: float) -> float: return prev_value + factor * (value - prev_value) -def ulaw_to_pcm(ulaw_bytes: bytes, in_sample_rate: int, out_sample_rate: int): - # Convert μ-law to PCM - in_pcm_bytes = audioop.ulaw2lin(ulaw_bytes, 2) +if sys.version_info >= (3, 13): + try: + import io - # Resample - out_pcm_bytes = audioop.ratecv(in_pcm_bytes, 2, 1, in_sample_rate, out_sample_rate, None)[0] + import soundfile as sf - return out_pcm_bytes + def ulaw_to_pcm(ulaw_bytes: bytes, in_sample_rate: int, out_sample_rate: int) -> bytes: + with io.BytesIO(ulaw_bytes) as buf: + data, _ = sf.read( + buf, channels=1, samplerate=in_sample_rate, format="RAW", subtype="ULAW" + ) + if in_sample_rate != out_sample_rate: + data = resampy.resample(data, in_sample_rate, out_sample_rate) + return (data * 32767).astype(np.int16).tobytes() + def pcm_to_ulaw(pcm_bytes: bytes, in_sample_rate: int, out_sample_rate: int) -> bytes: + data = np.frombuffer(pcm_bytes, dtype=np.int16).astype(np.float32) / 32767.0 + if in_sample_rate != out_sample_rate: + data = resampy.resample(data, in_sample_rate, out_sample_rate) + with io.BytesIO() as buf: + sf.write(buf, data, out_sample_rate, format="RAW", subtype="ULAW") + return buf.getvalue() + except ImportError: + raise ImportError( + "For Python 3.13+, please install soundfile: pip install pipecat-ai[audio]" + ) -def pcm_to_ulaw(pcm_bytes: bytes, in_sample_rate: int, out_sample_rate: int): - # Resample - in_pcm_bytes = audioop.ratecv(pcm_bytes, 2, 1, in_sample_rate, out_sample_rate, None)[0] +else: + import audioop - # Convert PCM to μ-law - ulaw_bytes = audioop.lin2ulaw(in_pcm_bytes, 2) + def ulaw_to_pcm(ulaw_bytes: bytes, in_sample_rate: int, out_sample_rate: int): + # Convert μ-law to PCM + in_pcm_bytes = audioop.ulaw2lin(ulaw_bytes, 2) - return ulaw_bytes + # Resample + out_pcm_bytes = audioop.ratecv(in_pcm_bytes, 2, 1, in_sample_rate, out_sample_rate, None)[0] + + return out_pcm_bytes + + def pcm_to_ulaw(pcm_bytes: bytes, in_sample_rate: int, out_sample_rate: int): + # Resample + in_pcm_bytes = audioop.ratecv(pcm_bytes, 2, 1, in_sample_rate, out_sample_rate, None)[0] + + # Convert PCM to μ-law + ulaw_bytes = audioop.lin2ulaw(in_pcm_bytes, 2) + + return ulaw_bytes diff --git a/src/pipecat/serializers/twilio.py b/src/pipecat/serializers/twilio.py index a0d02fa2f..89942e9ff 100644 --- a/src/pipecat/serializers/twilio.py +++ b/src/pipecat/serializers/twilio.py @@ -6,13 +6,24 @@ import base64 import json +import sys from pydantic import BaseModel -from pipecat.audio.utils import ulaw_to_pcm, pcm_to_ulaw from pipecat.frames.frames import AudioRawFrame, Frame, InputAudioRawFrame, StartInterruptionFrame from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializerType +if sys.version_info >= (3, 13): + try: + from pipecat.audio.utils import pcm_to_ulaw, ulaw_to_pcm + except ImportError: + raise ImportError( + "Audio processing support required for TwilioFrameSerializer. " + "Please install with: pip install pipecat-ai[audio]" + ) +else: + from pipecat.audio.utils import pcm_to_ulaw, ulaw_to_pcm + class TwilioFrameSerializer(FrameSerializer): class InputParams(BaseModel):