Merge pull request #791 from pipecat-ai/aleix/fastapi-generic-websocket
FastAPIWebsocketTransport: fix to work with text and binary
This commit is contained in:
@@ -68,6 +68,9 @@ async def on_audio_data(processor, audio, sample_rate, num_channels):
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed `FastAPIWebsocketTransport` so it can work with binary data (e.g. using
|
||||||
|
the protobuf serializer).
|
||||||
|
|
||||||
- Fixed an issue in `CartesiaTTSService` that could cause previous audio to be
|
- Fixed an issue in `CartesiaTTSService` that could cause previous audio to be
|
||||||
received after an interruption.
|
received after an interruption.
|
||||||
|
|
||||||
|
|||||||
@@ -49,13 +49,13 @@
|
|||||||
let startBtn = document.getElementById('startAudioBtn');
|
let startBtn = document.getElementById('startAudioBtn');
|
||||||
let stopBtn = document.getElementById('stopAudioBtn');
|
let stopBtn = document.getElementById('stopAudioBtn');
|
||||||
|
|
||||||
const proto = protobuf.load("frames.proto", (err, root) => {
|
const proto = protobuf.load('frames.proto', (err, root) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
Frame = root.lookupType("pipecat.Frame");
|
Frame = root.lookupType('pipecat.Frame');
|
||||||
const progressText = document.getElementById("progressText");
|
const progressText = document.getElementById('progressText');
|
||||||
progressText.textContent = "We are ready! Make sure to run the server and then click `Start Audio`.";
|
progressText.textContent = 'We are ready! Make sure to run the server and then click `Start Audio`.';
|
||||||
|
|
||||||
startBtn.disabled = false;
|
startBtn.disabled = false;
|
||||||
stopBtn.disabled = true;
|
stopBtn.disabled = true;
|
||||||
@@ -63,18 +63,60 @@
|
|||||||
|
|
||||||
function initWebSocket() {
|
function initWebSocket() {
|
||||||
ws = new WebSocket('ws://localhost:8765');
|
ws = new WebSocket('ws://localhost:8765');
|
||||||
|
// This is so `event.data` is already an ArrayBuffer.
|
||||||
|
ws.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
ws.addEventListener('open', () => console.log('WebSocket connection established.'));
|
ws.addEventListener('open', handleWebSocketOpen);
|
||||||
ws.addEventListener('message', handleWebSocketMessage);
|
ws.addEventListener('message', handleWebSocketMessage);
|
||||||
ws.addEventListener('close', (event) => {
|
ws.addEventListener('close', (event) => {
|
||||||
console.log("WebSocket connection closed.", event.code, event.reason);
|
console.log('WebSocket connection closed.', event.code, event.reason);
|
||||||
stopAudio(false);
|
stopAudio(false);
|
||||||
});
|
});
|
||||||
ws.addEventListener('error', (event) => console.error('WebSocket error:', event));
|
ws.addEventListener('error', (event) => console.error('WebSocket error:', event));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleWebSocketMessage(event) {
|
function handleWebSocketOpen(event) {
|
||||||
const arrayBuffer = await event.data.arrayBuffer();
|
console.log('WebSocket connection established.', event)
|
||||||
|
|
||||||
|
navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: {
|
||||||
|
sampleRate: SAMPLE_RATE,
|
||||||
|
channelCount: NUM_CHANNELS,
|
||||||
|
autoGainControl: true,
|
||||||
|
echoCancellation: true,
|
||||||
|
noiseSuppression: true,
|
||||||
|
}
|
||||||
|
}).then((stream) => {
|
||||||
|
microphoneStream = stream;
|
||||||
|
// 512 is closest thing to 200ms.
|
||||||
|
scriptProcessor = audioContext.createScriptProcessor(512, 1, 1);
|
||||||
|
source = audioContext.createMediaStreamSource(stream);
|
||||||
|
source.connect(scriptProcessor);
|
||||||
|
scriptProcessor.connect(audioContext.destination);
|
||||||
|
|
||||||
|
scriptProcessor.onaudioprocess = (event) => {
|
||||||
|
if (!ws) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioData = event.inputBuffer.getChannelData(0);
|
||||||
|
const pcmS16Array = convertFloat32ToS16PCM(audioData);
|
||||||
|
const pcmByteArray = new Uint8Array(pcmS16Array.buffer);
|
||||||
|
const frame = Frame.create({
|
||||||
|
audio: {
|
||||||
|
audio: Array.from(pcmByteArray),
|
||||||
|
sampleRate: SAMPLE_RATE,
|
||||||
|
numChannels: NUM_CHANNELS
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const encodedFrame = new Uint8Array(Frame.encode(frame).finish());
|
||||||
|
ws.send(encodedFrame);
|
||||||
|
};
|
||||||
|
}).catch((error) => console.error('Error accessing microphone:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWebSocketMessage(event) {
|
||||||
|
const arrayBuffer = event.data;
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
enqueueAudioFromProto(arrayBuffer);
|
enqueueAudioFromProto(arrayBuffer);
|
||||||
}
|
}
|
||||||
@@ -127,49 +169,13 @@
|
|||||||
stopBtn.disabled = false;
|
stopBtn.disabled = false;
|
||||||
|
|
||||||
audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
||||||
latencyHint: "interactive",
|
latencyHint: 'interactive',
|
||||||
sampleRate: SAMPLE_RATE
|
sampleRate: SAMPLE_RATE
|
||||||
});
|
});
|
||||||
|
|
||||||
isPlaying = true;
|
isPlaying = true;
|
||||||
|
|
||||||
initWebSocket();
|
initWebSocket();
|
||||||
|
|
||||||
navigator.mediaDevices.getUserMedia({
|
|
||||||
audio: {
|
|
||||||
sampleRate: SAMPLE_RATE,
|
|
||||||
channelCount: NUM_CHANNELS,
|
|
||||||
autoGainControl: true,
|
|
||||||
echoCancellation: true,
|
|
||||||
noiseSuppression: true,
|
|
||||||
}
|
|
||||||
}).then((stream) => {
|
|
||||||
microphoneStream = stream;
|
|
||||||
// 512 is closest thing to 200ms.
|
|
||||||
scriptProcessor = audioContext.createScriptProcessor(512, 1, 1);
|
|
||||||
source = audioContext.createMediaStreamSource(stream);
|
|
||||||
source.connect(scriptProcessor);
|
|
||||||
scriptProcessor.connect(audioContext.destination);
|
|
||||||
|
|
||||||
scriptProcessor.onaudioprocess = (event) => {
|
|
||||||
if (!ws) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioData = event.inputBuffer.getChannelData(0);
|
|
||||||
const pcmS16Array = convertFloat32ToS16PCM(audioData);
|
|
||||||
const pcmByteArray = new Uint8Array(pcmS16Array.buffer);
|
|
||||||
const frame = Frame.create({
|
|
||||||
audio: {
|
|
||||||
audio: Array.from(pcmByteArray),
|
|
||||||
sampleRate: SAMPLE_RATE,
|
|
||||||
numChannels: NUM_CHANNELS
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const encodedFrame = new Uint8Array(Frame.encode(frame).finish());
|
|
||||||
ws.send(encodedFrame);
|
|
||||||
};
|
|
||||||
}).catch((error) => console.error('Error accessing microphone:', error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopAudio(closeWebsocket) {
|
function stopAudio(closeWebsocket) {
|
||||||
|
|||||||
@@ -5,11 +5,22 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
from pipecat.frames.frames import Frame
|
from pipecat.frames.frames import Frame
|
||||||
|
|
||||||
|
|
||||||
|
class FrameSerializerType(Enum):
|
||||||
|
BINARY = "binary"
|
||||||
|
TEXT = "text"
|
||||||
|
|
||||||
|
|
||||||
class FrameSerializer(ABC):
|
class FrameSerializer(ABC):
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def type(self) -> FrameSerializerType:
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def serialize(self, frame: Frame) -> str | bytes | None:
|
def serialize(self, frame: Frame) -> str | bytes | None:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import ctypes
|
|||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
from pipecat.frames.frames import Frame, InputAudioRawFrame, OutputAudioRawFrame
|
from pipecat.frames.frames import Frame, InputAudioRawFrame, OutputAudioRawFrame
|
||||||
from pipecat.serializers.base_serializer import FrameSerializer
|
from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializerType
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -21,6 +21,10 @@ except ModuleNotFoundError as e:
|
|||||||
|
|
||||||
|
|
||||||
class LivekitFrameSerializer(FrameSerializer):
|
class LivekitFrameSerializer(FrameSerializer):
|
||||||
|
@property
|
||||||
|
def type(self) -> FrameSerializerType:
|
||||||
|
return FrameSerializerType.BINARY
|
||||||
|
|
||||||
def serialize(self, frame: Frame) -> str | bytes | None:
|
def serialize(self, frame: Frame) -> str | bytes | None:
|
||||||
if not isinstance(frame, OutputAudioRawFrame):
|
if not isinstance(frame, OutputAudioRawFrame):
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -8,8 +8,14 @@ import dataclasses
|
|||||||
|
|
||||||
import pipecat.frames.protobufs.frames_pb2 as frame_protos
|
import pipecat.frames.protobufs.frames_pb2 as frame_protos
|
||||||
|
|
||||||
from pipecat.frames.frames import AudioRawFrame, Frame, TextFrame, TranscriptionFrame
|
from pipecat.frames.frames import (
|
||||||
from pipecat.serializers.base_serializer import FrameSerializer
|
Frame,
|
||||||
|
InputAudioRawFrame,
|
||||||
|
OutputAudioRawFrame,
|
||||||
|
TextFrame,
|
||||||
|
TranscriptionFrame,
|
||||||
|
)
|
||||||
|
from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializerType
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -17,15 +23,25 @@ from loguru import logger
|
|||||||
class ProtobufFrameSerializer(FrameSerializer):
|
class ProtobufFrameSerializer(FrameSerializer):
|
||||||
SERIALIZABLE_TYPES = {
|
SERIALIZABLE_TYPES = {
|
||||||
TextFrame: "text",
|
TextFrame: "text",
|
||||||
AudioRawFrame: "audio",
|
OutputAudioRawFrame: "audio",
|
||||||
TranscriptionFrame: "transcription",
|
TranscriptionFrame: "transcription",
|
||||||
}
|
}
|
||||||
|
|
||||||
SERIALIZABLE_FIELDS = {v: k for k, v in SERIALIZABLE_TYPES.items()}
|
SERIALIZABLE_FIELDS = {v: k for k, v in SERIALIZABLE_TYPES.items()}
|
||||||
|
|
||||||
|
DESERIALIZABLE_TYPES = {
|
||||||
|
TextFrame: "text",
|
||||||
|
InputAudioRawFrame: "audio",
|
||||||
|
TranscriptionFrame: "transcription",
|
||||||
|
}
|
||||||
|
DESERIALIZABLE_FIELDS = {v: k for k, v in DESERIALIZABLE_TYPES.items()}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self) -> FrameSerializerType:
|
||||||
|
return FrameSerializerType.BINARY
|
||||||
|
|
||||||
def serialize(self, frame: Frame) -> str | bytes | None:
|
def serialize(self, frame: Frame) -> str | bytes | None:
|
||||||
proto_frame = frame_protos.Frame()
|
proto_frame = frame_protos.Frame()
|
||||||
if type(frame) not in self.SERIALIZABLE_TYPES:
|
if type(frame) not in self.SERIALIZABLE_TYPES:
|
||||||
@@ -40,8 +56,7 @@ class ProtobufFrameSerializer(FrameSerializer):
|
|||||||
if value and hasattr(proto_attr, field.name):
|
if value and hasattr(proto_attr, field.name):
|
||||||
setattr(proto_attr, field.name, value)
|
setattr(proto_attr, field.name, value)
|
||||||
|
|
||||||
result = proto_frame.SerializeToString()
|
return proto_frame.SerializeToString()
|
||||||
return result
|
|
||||||
|
|
||||||
def deserialize(self, data: str | bytes) -> Frame | None:
|
def deserialize(self, data: str | bytes) -> Frame | None:
|
||||||
"""Returns a Frame object from a Frame protobuf. Used to convert frames
|
"""Returns a Frame object from a Frame protobuf. Used to convert frames
|
||||||
@@ -64,11 +79,11 @@ class ProtobufFrameSerializer(FrameSerializer):
|
|||||||
|
|
||||||
proto = frame_protos.Frame.FromString(data)
|
proto = frame_protos.Frame.FromString(data)
|
||||||
which = proto.WhichOneof("frame")
|
which = proto.WhichOneof("frame")
|
||||||
if which not in self.SERIALIZABLE_FIELDS:
|
if which not in self.DESERIALIZABLE_FIELDS:
|
||||||
logger.error("Unable to deserialize a valid frame")
|
logger.error("Unable to deserialize a valid frame")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
class_name = self.SERIALIZABLE_FIELDS[which]
|
class_name = self.DESERIALIZABLE_FIELDS[which]
|
||||||
args = getattr(proto, which)
|
args = getattr(proto, which)
|
||||||
args_dict = {}
|
args_dict = {}
|
||||||
for field in proto.DESCRIPTOR.fields_by_name[which].message_type.fields:
|
for field in proto.DESCRIPTOR.fields_by_name[which].message_type.fields:
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import json
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from pipecat.audio.utils import ulaw_to_pcm, pcm_to_ulaw
|
from pipecat.audio.utils import ulaw_to_pcm, pcm_to_ulaw
|
||||||
from pipecat.frames.frames import AudioRawFrame, Frame, StartInterruptionFrame
|
from pipecat.frames.frames import AudioRawFrame, Frame, InputAudioRawFrame, StartInterruptionFrame
|
||||||
from pipecat.serializers.base_serializer import FrameSerializer
|
from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializerType
|
||||||
|
|
||||||
|
|
||||||
class TwilioFrameSerializer(FrameSerializer):
|
class TwilioFrameSerializer(FrameSerializer):
|
||||||
@@ -23,6 +23,10 @@ class TwilioFrameSerializer(FrameSerializer):
|
|||||||
self._stream_sid = stream_sid
|
self._stream_sid = stream_sid
|
||||||
self._params = params
|
self._params = params
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self) -> FrameSerializerType:
|
||||||
|
return FrameSerializerType.TEXT
|
||||||
|
|
||||||
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
|
||||||
@@ -53,7 +57,7 @@ class TwilioFrameSerializer(FrameSerializer):
|
|||||||
deserialized_data = ulaw_to_pcm(
|
deserialized_data = ulaw_to_pcm(
|
||||||
payload, self._params.twilio_sample_rate, self._params.sample_rate
|
payload, self._params.twilio_sample_rate, self._params.sample_rate
|
||||||
)
|
)
|
||||||
audio_frame = AudioRawFrame(
|
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
|
||||||
)
|
)
|
||||||
return audio_frame
|
return audio_frame
|
||||||
|
|||||||
@@ -8,20 +8,21 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import io
|
import io
|
||||||
import time
|
import time
|
||||||
|
import typing
|
||||||
import wave
|
import wave
|
||||||
|
|
||||||
from typing import Awaitable, Callable
|
from typing import Awaitable, Callable
|
||||||
from pydantic.main import BaseModel
|
from pydantic.main import BaseModel
|
||||||
|
|
||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
AudioRawFrame,
|
|
||||||
Frame,
|
Frame,
|
||||||
InputAudioRawFrame,
|
InputAudioRawFrame,
|
||||||
|
OutputAudioRawFrame,
|
||||||
StartFrame,
|
StartFrame,
|
||||||
StartInterruptionFrame,
|
StartInterruptionFrame,
|
||||||
)
|
)
|
||||||
from pipecat.processors.frame_processor import FrameDirection
|
from pipecat.processors.frame_processor import FrameDirection
|
||||||
from pipecat.serializers.base_serializer import FrameSerializer
|
from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializerType
|
||||||
from pipecat.transports.base_input import BaseInputTransport
|
from pipecat.transports.base_input import BaseInputTransport
|
||||||
from pipecat.transports.base_output import BaseOutputTransport
|
from pipecat.transports.base_output import BaseOutputTransport
|
||||||
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
||||||
@@ -68,21 +69,23 @@ class FastAPIWebsocketInputTransport(BaseInputTransport):
|
|||||||
await self._callbacks.on_client_connected(self._websocket)
|
await self._callbacks.on_client_connected(self._websocket)
|
||||||
self._receive_task = self.get_event_loop().create_task(self._receive_messages())
|
self._receive_task = self.get_event_loop().create_task(self._receive_messages())
|
||||||
|
|
||||||
|
def _iter_data(self) -> typing.AsyncIterator[bytes | str]:
|
||||||
|
if self._params.serializer.type == FrameSerializerType.BINARY:
|
||||||
|
return self._websocket.iter_bytes()
|
||||||
|
else:
|
||||||
|
return self._websocket.iter_text()
|
||||||
|
|
||||||
async def _receive_messages(self):
|
async def _receive_messages(self):
|
||||||
async for message in self._websocket.iter_text():
|
async for message in self._iter_data():
|
||||||
frame = self._params.serializer.deserialize(message)
|
frame = self._params.serializer.deserialize(message)
|
||||||
|
|
||||||
if not frame:
|
if not frame:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(frame, AudioRawFrame):
|
if isinstance(frame, InputAudioRawFrame):
|
||||||
await self.push_audio_frame(
|
await self.push_audio_frame(frame)
|
||||||
InputAudioRawFrame(
|
else:
|
||||||
audio=frame.audio,
|
await self.push_frame(frame)
|
||||||
sample_rate=frame.sample_rate,
|
|
||||||
num_channels=frame.num_channels,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self._callbacks.on_client_disconnected(self._websocket)
|
await self._callbacks.on_client_disconnected(self._websocket)
|
||||||
|
|
||||||
@@ -110,29 +113,27 @@ class FastAPIWebsocketOutputTransport(BaseOutputTransport):
|
|||||||
await self._write_audio_sleep()
|
await self._write_audio_sleep()
|
||||||
return
|
return
|
||||||
|
|
||||||
frame = AudioRawFrame(
|
frame = OutputAudioRawFrame(
|
||||||
audio=frames,
|
audio=frames,
|
||||||
sample_rate=self._params.audio_out_sample_rate,
|
sample_rate=self._params.audio_out_sample_rate,
|
||||||
num_channels=self._params.audio_out_channels,
|
num_channels=self._params.audio_out_channels,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._params.add_wav_header:
|
if self._params.add_wav_header:
|
||||||
content = io.BytesIO()
|
with io.BytesIO() as buffer:
|
||||||
ww = wave.open(content, "wb")
|
with wave.open(buffer, "wb") as wf:
|
||||||
ww.setsampwidth(2)
|
wf.setsampwidth(2)
|
||||||
ww.setnchannels(frame.num_channels)
|
wf.setnchannels(frame.num_channels)
|
||||||
ww.setframerate(frame.sample_rate)
|
wf.setframerate(frame.sample_rate)
|
||||||
ww.writeframes(frame.audio)
|
wf.writeframes(frame.audio)
|
||||||
ww.close()
|
wav_frame = OutputAudioRawFrame(
|
||||||
content.seek(0)
|
buffer.getvalue(),
|
||||||
wav_frame = AudioRawFrame(
|
sample_rate=frame.sample_rate,
|
||||||
content.read(), sample_rate=frame.sample_rate, num_channels=frame.num_channels
|
num_channels=frame.num_channels,
|
||||||
)
|
)
|
||||||
frame = wav_frame
|
frame = wav_frame
|
||||||
|
|
||||||
payload = self._params.serializer.serialize(frame)
|
await self._write_frame(frame)
|
||||||
if payload:
|
|
||||||
await self._websocket.send_text(payload)
|
|
||||||
|
|
||||||
self._websocket_audio_buffer = bytes()
|
self._websocket_audio_buffer = bytes()
|
||||||
|
|
||||||
@@ -142,7 +143,13 @@ class FastAPIWebsocketOutputTransport(BaseOutputTransport):
|
|||||||
async def _write_frame(self, frame: Frame):
|
async def _write_frame(self, frame: Frame):
|
||||||
payload = self._params.serializer.serialize(frame)
|
payload = self._params.serializer.serialize(frame)
|
||||||
if payload and self._websocket.client_state == WebSocketState.CONNECTED:
|
if payload and self._websocket.client_state == WebSocketState.CONNECTED:
|
||||||
await self._websocket.send_text(payload)
|
await self._send_data(payload)
|
||||||
|
|
||||||
|
def _send_data(self, data: str | bytes):
|
||||||
|
if self._params.serializer.type == FrameSerializerType.BINARY:
|
||||||
|
return self._websocket.send_bytes(data)
|
||||||
|
else:
|
||||||
|
return self._websocket.send_text(data)
|
||||||
|
|
||||||
async def _write_audio_sleep(self):
|
async def _write_audio_sleep(self):
|
||||||
# Simulate a clock.
|
# Simulate a clock.
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ from typing import Awaitable, Callable
|
|||||||
from pydantic.main import BaseModel
|
from pydantic.main import BaseModel
|
||||||
|
|
||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
AudioRawFrame,
|
|
||||||
CancelFrame,
|
CancelFrame,
|
||||||
EndFrame,
|
EndFrame,
|
||||||
Frame,
|
Frame,
|
||||||
InputAudioRawFrame,
|
InputAudioRawFrame,
|
||||||
|
OutputAudioRawFrame,
|
||||||
StartFrame,
|
StartFrame,
|
||||||
StartInterruptionFrame,
|
StartInterruptionFrame,
|
||||||
)
|
)
|
||||||
@@ -105,14 +105,8 @@ class WebsocketServerInputTransport(BaseInputTransport):
|
|||||||
if not frame:
|
if not frame:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(frame, AudioRawFrame):
|
if isinstance(frame, InputAudioRawFrame):
|
||||||
await self.push_audio_frame(
|
await self.push_audio_frame(frame)
|
||||||
InputAudioRawFrame(
|
|
||||||
audio=frame.audio,
|
|
||||||
sample_rate=frame.sample_rate,
|
|
||||||
num_channels=frame.num_channels,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|
||||||
@@ -157,29 +151,27 @@ class WebsocketServerOutputTransport(BaseOutputTransport):
|
|||||||
await self._write_audio_sleep()
|
await self._write_audio_sleep()
|
||||||
return
|
return
|
||||||
|
|
||||||
frame = AudioRawFrame(
|
frame = OutputAudioRawFrame(
|
||||||
audio=frames,
|
audio=frames,
|
||||||
sample_rate=self._params.audio_out_sample_rate,
|
sample_rate=self._params.audio_out_sample_rate,
|
||||||
num_channels=self._params.audio_out_channels,
|
num_channels=self._params.audio_out_channels,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._params.add_wav_header:
|
if self._params.add_wav_header:
|
||||||
content = io.BytesIO()
|
with io.BytesIO() as buffer:
|
||||||
ww = wave.open(content, "wb")
|
with wave.open(buffer, "wb") as wf:
|
||||||
ww.setsampwidth(2)
|
wf.setsampwidth(2)
|
||||||
ww.setnchannels(frame.num_channels)
|
wf.setnchannels(frame.num_channels)
|
||||||
ww.setframerate(frame.sample_rate)
|
wf.setframerate(frame.sample_rate)
|
||||||
ww.writeframes(frame.audio)
|
wf.writeframes(frame.audio)
|
||||||
ww.close()
|
wav_frame = OutputAudioRawFrame(
|
||||||
content.seek(0)
|
buffer.getvalue(),
|
||||||
wav_frame = AudioRawFrame(
|
sample_rate=frame.sample_rate,
|
||||||
content.read(), sample_rate=frame.sample_rate, num_channels=frame.num_channels
|
num_channels=frame.num_channels,
|
||||||
)
|
)
|
||||||
frame = wav_frame
|
frame = wav_frame
|
||||||
|
|
||||||
proto = self._params.serializer.serialize(frame)
|
await self._write_frame(frame)
|
||||||
if proto:
|
|
||||||
await self._websocket.send(proto)
|
|
||||||
|
|
||||||
self._websocket_audio_buffer = bytes()
|
self._websocket_audio_buffer = bytes()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user