update CHANGELOG and fix formatting
This commit is contained in:
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added `TelnyxFrameSerializer` to support Telnyx calls. A full running example
|
||||||
|
has also been added to `examples/telnyx-chatbot`.
|
||||||
|
|
||||||
- Allow pushing silence audio frames before `TTSStoppedFrame`. This might be
|
- Allow pushing silence audio frames before `TTSStoppedFrame`. This might be
|
||||||
useful for testing purposes, for example, passing bot audio to an STT service
|
useful for testing purposes, for example, passing bot audio to an STT service
|
||||||
which usually needs additional audio data to detect the utterance stopped.
|
which usually needs additional audio data to detect the utterance stopped.
|
||||||
|
|||||||
@@ -16,18 +16,15 @@ from pipecat.pipeline.pipeline import Pipeline
|
|||||||
from pipecat.pipeline.runner import PipelineRunner
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
|
|
||||||
from pipecat.serializers.telnyx import TelnyxFrameSerializer
|
from pipecat.serializers.telnyx import TelnyxFrameSerializer
|
||||||
|
|
||||||
from pipecat.services.elevenlabs import ElevenLabsTTSService, Language
|
|
||||||
from pipecat.services.deepgram import DeepgramSTTService
|
from pipecat.services.deepgram import DeepgramSTTService
|
||||||
|
from pipecat.services.elevenlabs import ElevenLabsTTSService, Language
|
||||||
from pipecat.services.openai import OpenAILLMService
|
from pipecat.services.openai import OpenAILLMService
|
||||||
from pipecat.transports.network.fastapi_websocket import (
|
from pipecat.transports.network.fastapi_websocket import (
|
||||||
FastAPIWebsocketParams,
|
FastAPIWebsocketParams,
|
||||||
FastAPIWebsocketTransport,
|
FastAPIWebsocketTransport,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
load_dotenv(override=True)
|
load_dotenv(override=True)
|
||||||
|
|
||||||
logger.remove(0)
|
logger.remove(0)
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@@ -16,11 +22,13 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/")
|
@app.post("/")
|
||||||
async def start_call():
|
async def start_call():
|
||||||
print("POST TeXML")
|
print("POST TeXML")
|
||||||
return HTMLResponse(content=open("templates/streams.xml").read(), media_type="application/xml")
|
return HTMLResponse(content=open("templates/streams.xml").read(), media_type="application/xml")
|
||||||
|
|
||||||
|
|
||||||
@app.websocket("/ws")
|
@app.websocket("/ws")
|
||||||
async def websocket_endpoint(websocket: WebSocket):
|
async def websocket_endpoint(websocket: WebSocket):
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
@@ -33,5 +41,6 @@ async def websocket_endpoint(websocket: WebSocket):
|
|||||||
print("WebSocket connection accepted")
|
print("WebSocket connection accepted")
|
||||||
await run_bot(websocket, stream_id, outbound_encoding, "PCMU")
|
await run_bot(websocket, stream_id, outbound_encoding, "PCMU")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8765)
|
uvicorn.run(app, host="0.0.0.0", port=8765)
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ def ulaw_to_pcm(ulaw_bytes: bytes, in_sample_rate: int, out_sample_rate: int):
|
|||||||
|
|
||||||
# Resample
|
# Resample
|
||||||
out_pcm_bytes = resample_audio(in_pcm_bytes, in_sample_rate, out_sample_rate)
|
out_pcm_bytes = resample_audio(in_pcm_bytes, in_sample_rate, out_sample_rate)
|
||||||
|
|
||||||
return out_pcm_bytes
|
return out_pcm_bytes
|
||||||
|
|
||||||
|
|
||||||
@@ -95,13 +95,13 @@ def pcm_to_ulaw(pcm_bytes: bytes, in_sample_rate: int, out_sample_rate: int):
|
|||||||
return ulaw_bytes
|
return ulaw_bytes
|
||||||
|
|
||||||
|
|
||||||
def alaw_to_pcm(alaw_bytes: bytes, in_sample_rate: int, out_sample_rate: int) -> bytes:
|
def alaw_to_pcm(alaw_bytes: bytes, in_sample_rate: int, out_sample_rate: int) -> bytes:
|
||||||
# Convert a-law to PCM
|
# Convert a-law to PCM
|
||||||
in_pcm_bytes = audioop.alaw2lin(alaw_bytes, 2)
|
in_pcm_bytes = audioop.alaw2lin(alaw_bytes, 2)
|
||||||
|
|
||||||
# Resample
|
# Resample
|
||||||
out_pcm_bytes = resample_audio(in_pcm_bytes, in_sample_rate, out_sample_rate)
|
out_pcm_bytes = resample_audio(in_pcm_bytes, in_sample_rate, out_sample_rate)
|
||||||
|
|
||||||
return out_pcm_bytes
|
return out_pcm_bytes
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import json
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from pipecat.audio.utils import pcm_to_ulaw, ulaw_to_pcm, pcm_to_alaw, alaw_to_pcm
|
from pipecat.audio.utils import alaw_to_pcm, pcm_to_alaw, pcm_to_ulaw, ulaw_to_pcm
|
||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
AudioRawFrame,
|
AudioRawFrame,
|
||||||
Frame,
|
Frame,
|
||||||
@@ -28,10 +28,16 @@ class TelnyxFrameSerializer(FrameSerializer):
|
|||||||
inbound_encoding: str = "PCMU"
|
inbound_encoding: str = "PCMU"
|
||||||
outbound_encoding: str = "PCMU"
|
outbound_encoding: str = "PCMU"
|
||||||
|
|
||||||
def __init__(self, stream_id: str, outbound_encoding: str, inbound_encoding: str, params: InputParams = InputParams()):
|
def __init__(
|
||||||
|
self,
|
||||||
|
stream_id: str,
|
||||||
|
outbound_encoding: str,
|
||||||
|
inbound_encoding: str,
|
||||||
|
params: InputParams = InputParams(),
|
||||||
|
):
|
||||||
self._stream_id = stream_id
|
self._stream_id = stream_id
|
||||||
params.outbound_encoding = outbound_encoding
|
params.outbound_encoding = outbound_encoding
|
||||||
params.inbound_encoding = inbound_encoding
|
params.inbound_encoding = inbound_encoding
|
||||||
self._params = params
|
self._params = params
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -41,18 +47,18 @@ class TelnyxFrameSerializer(FrameSerializer):
|
|||||||
def serialize(self, frame: Frame) -> str | bytes | None:
|
def serialize(self, frame: Frame) -> str | bytes | None:
|
||||||
if isinstance(frame, AudioRawFrame):
|
if isinstance(frame, AudioRawFrame):
|
||||||
data = frame.audio
|
data = frame.audio
|
||||||
|
|
||||||
if self._params.inbound_encoding == "PCMU":
|
if self._params.inbound_encoding == "PCMU":
|
||||||
serialized_data = pcm_to_ulaw(
|
serialized_data = pcm_to_ulaw(
|
||||||
data, frame.sample_rate, self._params.telnyx_sample_rate
|
data, frame.sample_rate, self._params.telnyx_sample_rate
|
||||||
)
|
)
|
||||||
elif self._params.inbound_encoding == "PCMA":
|
elif self._params.inbound_encoding == "PCMA":
|
||||||
serialized_data = pcm_to_alaw(
|
serialized_data = pcm_to_alaw(
|
||||||
data, frame.sample_rate, self._params.telnyx_sample_rate
|
data, frame.sample_rate, self._params.telnyx_sample_rate
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported encoding: {self._params.encoding}")
|
raise ValueError(f"Unsupported encoding: {self._params.inbound_encoding}")
|
||||||
|
|
||||||
payload = base64.b64encode(serialized_data).decode("utf-8")
|
payload = base64.b64encode(serialized_data).decode("utf-8")
|
||||||
answer = {
|
answer = {
|
||||||
"event": "media",
|
"event": "media",
|
||||||
@@ -67,7 +73,7 @@ class TelnyxFrameSerializer(FrameSerializer):
|
|||||||
|
|
||||||
def deserialize(self, data: str | bytes) -> Frame | None:
|
def deserialize(self, data: str | bytes) -> Frame | None:
|
||||||
message = json.loads(data)
|
message = json.loads(data)
|
||||||
|
|
||||||
if message["event"] == "media":
|
if message["event"] == "media":
|
||||||
payload_base64 = message["media"]["payload"]
|
payload_base64 = message["media"]["payload"]
|
||||||
payload = base64.b64decode(payload_base64)
|
payload = base64.b64decode(payload_base64)
|
||||||
@@ -81,7 +87,7 @@ class TelnyxFrameSerializer(FrameSerializer):
|
|||||||
payload, self._params.telnyx_sample_rate, self._params.sample_rate
|
payload, self._params.telnyx_sample_rate, self._params.sample_rate
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported encoding: {self._params.encoding}")
|
raise ValueError(f"Unsupported encoding: {self._params.outbound_encoding}")
|
||||||
|
|
||||||
audio_frame = InputAudioRawFrame(
|
audio_frame = InputAudioRawFrame(
|
||||||
audio=deserialized_data, num_channels=1, sample_rate=self._params.sample_rate
|
audio=deserialized_data, num_channels=1, sample_rate=self._params.sample_rate
|
||||||
|
|||||||
Reference in New Issue
Block a user