introduce input/output audio and image frames
We now distinguish between input and output audio and image frames. We introduce `InputAudioRawFrame`, `OutputAudioRawFrame`, `InputImageRawFrame` and `OutputImageRawFrame` (and other subclasses of those). The input frames usually come from an input transport and are meant to be processed inside the pipeline to generate new frames. However, the input frames will not be sent through an output transport. The output frames can also be processed by any frame processor in the pipeline and they are allowed to be sent by the output transport.
This commit is contained in:
@@ -24,6 +24,7 @@ message AudioRawFrame {
|
||||
bytes audio = 3;
|
||||
uint32 sample_rate = 4;
|
||||
uint32 num_channels = 5;
|
||||
optional uint64 pts = 6;
|
||||
}
|
||||
|
||||
message TranscriptionFrame {
|
||||
|
||||
@@ -41,10 +41,7 @@ class DataFrame(Frame):
|
||||
|
||||
@dataclass
|
||||
class AudioRawFrame(DataFrame):
|
||||
"""A chunk of audio. Will be played by the transport if the transport's
|
||||
microphone has been enabled.
|
||||
|
||||
"""
|
||||
"""A chunk of audio."""
|
||||
audio: bytes
|
||||
sample_rate: int
|
||||
num_channels: int
|
||||
@@ -58,6 +55,31 @@ class AudioRawFrame(DataFrame):
|
||||
return f"{self.name}(pts: {pts}, size: {len(self.audio)}, frames: {self.num_frames}, sample_rate: {self.sample_rate}, channels: {self.num_channels})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class InputAudioRawFrame(AudioRawFrame):
|
||||
"""A chunk of audio usually coming from an input transport.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutputAudioRawFrame(AudioRawFrame):
|
||||
"""A chunk of audio. Will be played by the output transport if the
|
||||
transport's microphone has been enabled.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class TTSAudioRawFrame(OutputAudioRawFrame):
|
||||
"""A chunk of output audio generated by a TTS service.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImageRawFrame(DataFrame):
|
||||
"""An image. Will be shown by the transport if the transport's camera is
|
||||
@@ -74,20 +96,30 @@ class ImageRawFrame(DataFrame):
|
||||
|
||||
|
||||
@dataclass
|
||||
class URLImageRawFrame(ImageRawFrame):
|
||||
"""An image with an associated URL. Will be shown by the transport if the
|
||||
transport's camera is enabled.
|
||||
|
||||
"""
|
||||
url: str | None
|
||||
|
||||
def __str__(self):
|
||||
pts = format_pts(self.pts)
|
||||
return f"{self.name}(pts: {pts}, url: {self.url}, size: {self.size}, format: {self.format})"
|
||||
class InputImageRawFrame(ImageRawFrame):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class VisionImageRawFrame(ImageRawFrame):
|
||||
class OutputImageRawFrame(ImageRawFrame):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserImageRawFrame(InputImageRawFrame):
|
||||
"""An image associated to a user. Will be shown by the transport if the
|
||||
transport's camera is enabled.
|
||||
|
||||
"""
|
||||
user_id: str
|
||||
|
||||
def __str__(self):
|
||||
pts = format_pts(self.pts)
|
||||
return f"{self.name}(pts: {pts}, user: {self.user_id}, size: {self.size}, format: {self.format})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class VisionImageRawFrame(InputImageRawFrame):
|
||||
"""An image with an associated text to ask for a description of it. Will be
|
||||
shown by the transport if the transport's camera is enabled.
|
||||
|
||||
@@ -100,16 +132,16 @@ class VisionImageRawFrame(ImageRawFrame):
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserImageRawFrame(ImageRawFrame):
|
||||
"""An image associated to a user. Will be shown by the transport if the
|
||||
class URLImageRawFrame(OutputImageRawFrame):
|
||||
"""An image with an associated URL. Will be shown by the transport if the
|
||||
transport's camera is enabled.
|
||||
|
||||
"""
|
||||
user_id: str
|
||||
url: str | None
|
||||
|
||||
def __str__(self):
|
||||
pts = format_pts(self.pts)
|
||||
return f"{self.name}(pts: {pts}, user: {self.user_id}, size: {self.size}, format: {self.format})"
|
||||
return f"{self.name}(pts: {pts}, url: {self.url}, size: {self.size}, format: {self.format})"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -420,10 +452,10 @@ class BotSpeakingFrame(ControlFrame):
|
||||
@dataclass
|
||||
class TTSStartedFrame(ControlFrame):
|
||||
"""Used to indicate the beginning of a TTS response. Following
|
||||
AudioRawFrames are part of the TTS response until an TTSStoppedFrame. These
|
||||
frames can be used for aggregating audio frames in a transport to optimize
|
||||
the size of frames sent to the session, without needing to control this in
|
||||
the TTS service.
|
||||
TTSAudioRawFrames are part of the TTS response until an
|
||||
TTSStoppedFrame. These frames can be used for aggregating audio frames in a
|
||||
transport to optimize the size of frames sent to the session, without
|
||||
needing to control this in the TTS service.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -14,7 +14,7 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x66rames.proto\x12\x07pipecat\"3\n\tTextFrame\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04text\x18\x03 \x01(\t\"c\n\rAudioRawFrame\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05\x61udio\x18\x03 \x01(\x0c\x12\x13\n\x0bsample_rate\x18\x04 \x01(\r\x12\x14\n\x0cnum_channels\x18\x05 \x01(\r\"`\n\x12TranscriptionFrame\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04text\x18\x03 \x01(\t\x12\x0f\n\x07user_id\x18\x04 \x01(\t\x12\x11\n\ttimestamp\x18\x05 \x01(\t\"\x93\x01\n\x05\x46rame\x12\"\n\x04text\x18\x01 \x01(\x0b\x32\x12.pipecat.TextFrameH\x00\x12\'\n\x05\x61udio\x18\x02 \x01(\x0b\x32\x16.pipecat.AudioRawFrameH\x00\x12\x34\n\rtranscription\x18\x03 \x01(\x0b\x32\x1b.pipecat.TranscriptionFrameH\x00\x42\x07\n\x05\x66rameb\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x66rames.proto\x12\x07pipecat\"3\n\tTextFrame\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04text\x18\x03 \x01(\t\"}\n\rAudioRawFrame\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05\x61udio\x18\x03 \x01(\x0c\x12\x13\n\x0bsample_rate\x18\x04 \x01(\r\x12\x14\n\x0cnum_channels\x18\x05 \x01(\r\x12\x10\n\x03pts\x18\x06 \x01(\x04H\x00\x88\x01\x01\x42\x06\n\x04_pts\"`\n\x12TranscriptionFrame\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04text\x18\x03 \x01(\t\x12\x0f\n\x07user_id\x18\x04 \x01(\t\x12\x11\n\ttimestamp\x18\x05 \x01(\t\"\x93\x01\n\x05\x46rame\x12\"\n\x04text\x18\x01 \x01(\x0b\x32\x12.pipecat.TextFrameH\x00\x12\'\n\x05\x61udio\x18\x02 \x01(\x0b\x32\x16.pipecat.AudioRawFrameH\x00\x12\x34\n\rtranscription\x18\x03 \x01(\x0b\x32\x1b.pipecat.TranscriptionFrameH\x00\x42\x07\n\x05\x66rameb\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
@@ -24,9 +24,9 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
_globals['_TEXTFRAME']._serialized_start=25
|
||||
_globals['_TEXTFRAME']._serialized_end=76
|
||||
_globals['_AUDIORAWFRAME']._serialized_start=78
|
||||
_globals['_AUDIORAWFRAME']._serialized_end=177
|
||||
_globals['_TRANSCRIPTIONFRAME']._serialized_start=179
|
||||
_globals['_TRANSCRIPTIONFRAME']._serialized_end=275
|
||||
_globals['_FRAME']._serialized_start=278
|
||||
_globals['_FRAME']._serialized_end=425
|
||||
_globals['_AUDIORAWFRAME']._serialized_end=203
|
||||
_globals['_TRANSCRIPTIONFRAME']._serialized_start=205
|
||||
_globals['_TRANSCRIPTIONFRAME']._serialized_end=301
|
||||
_globals['_FRAME']._serialized_start=304
|
||||
_globals['_FRAME']._serialized_end=451
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
|
||||
from typing import List
|
||||
from typing import List, Type
|
||||
|
||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContextFrame, OpenAILLMContext
|
||||
|
||||
@@ -34,8 +34,8 @@ class LLMResponseAggregator(FrameProcessor):
|
||||
role: str,
|
||||
start_frame,
|
||||
end_frame,
|
||||
accumulator_frame: TextFrame,
|
||||
interim_accumulator_frame: TextFrame | None = None,
|
||||
accumulator_frame: Type[TextFrame],
|
||||
interim_accumulator_frame: Type[TextFrame] | None = None,
|
||||
handle_interruptions: bool = False
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
@@ -13,7 +13,11 @@ from typing import Any, Awaitable, Callable, List
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from pipecat.frames.frames import Frame, VisionImageRawFrame, FunctionCallInProgressFrame, FunctionCallResultFrame
|
||||
from pipecat.frames.frames import (
|
||||
Frame,
|
||||
VisionImageRawFrame,
|
||||
FunctionCallInProgressFrame,
|
||||
FunctionCallResultFrame)
|
||||
from pipecat.processors.frame_processor import FrameProcessor
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -4,13 +4,19 @@
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
|
||||
from pipecat.frames.frames import Frame, ImageRawFrame, TextFrame, VisionImageRawFrame
|
||||
from pipecat.frames.frames import (
|
||||
Frame,
|
||||
InputImageRawFrame,
|
||||
TextFrame,
|
||||
VisionImageRawFrame
|
||||
)
|
||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||
|
||||
|
||||
class VisionImageFrameAggregator(FrameProcessor):
|
||||
"""This aggregator waits for a consecutive TextFrame and an
|
||||
ImageFrame. After the ImageFrame arrives it will output a VisionImageFrame.
|
||||
InputImageRawFrame. After the InputImageRawFrame arrives it will output a
|
||||
VisionImageRawFrame.
|
||||
|
||||
>>> from pipecat.frames.frames import ImageFrame
|
||||
|
||||
@@ -34,7 +40,7 @@ class VisionImageFrameAggregator(FrameProcessor):
|
||||
|
||||
if isinstance(frame, TextFrame):
|
||||
self._describe_text = frame.text
|
||||
elif isinstance(frame, ImageRawFrame):
|
||||
elif isinstance(frame, InputImageRawFrame):
|
||||
if self._describe_text:
|
||||
frame = VisionImageRawFrame(
|
||||
text=self._describe_text,
|
||||
|
||||
@@ -9,11 +9,11 @@ import asyncio
|
||||
from pydantic import BaseModel
|
||||
|
||||
from pipecat.frames.frames import (
|
||||
AudioRawFrame,
|
||||
CancelFrame,
|
||||
EndFrame,
|
||||
Frame,
|
||||
ImageRawFrame,
|
||||
OutputAudioRawFrame,
|
||||
OutputImageRawFrame,
|
||||
StartFrame,
|
||||
SystemFrame)
|
||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||
@@ -182,9 +182,9 @@ class GStreamerPipelineSource(FrameProcessor):
|
||||
def _appsink_audio_new_sample(self, appsink: GstApp.AppSink):
|
||||
buffer = appsink.pull_sample().get_buffer()
|
||||
(_, info) = buffer.map(Gst.MapFlags.READ)
|
||||
frame = AudioRawFrame(audio=info.data,
|
||||
sample_rate=self._out_params.audio_sample_rate,
|
||||
num_channels=self._out_params.audio_channels)
|
||||
frame = OutputAudioRawFrame(audio=info.data,
|
||||
sample_rate=self._out_params.audio_sample_rate,
|
||||
num_channels=self._out_params.audio_channels)
|
||||
asyncio.run_coroutine_threadsafe(self.push_frame(frame), self.get_event_loop())
|
||||
buffer.unmap(info)
|
||||
return Gst.FlowReturn.OK
|
||||
@@ -192,7 +192,7 @@ class GStreamerPipelineSource(FrameProcessor):
|
||||
def _appsink_video_new_sample(self, appsink: GstApp.AppSink):
|
||||
buffer = appsink.pull_sample().get_buffer()
|
||||
(_, info) = buffer.map(Gst.MapFlags.READ)
|
||||
frame = ImageRawFrame(
|
||||
frame = OutputImageRawFrame(
|
||||
image=info.data,
|
||||
size=(self._out_params.video_width, self._out_params.video_height),
|
||||
format="RGB")
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
import ctypes
|
||||
import pickle
|
||||
|
||||
from pipecat.frames.frames import AudioRawFrame, Frame
|
||||
from pipecat.frames.frames import (
|
||||
Frame,
|
||||
InputAudioRawFrame,
|
||||
OutputAudioRawFrame)
|
||||
from pipecat.serializers.base_serializer import FrameSerializer
|
||||
|
||||
from loguru import logger
|
||||
@@ -22,12 +25,8 @@ except ModuleNotFoundError as e:
|
||||
|
||||
|
||||
class LivekitFrameSerializer(FrameSerializer):
|
||||
SERIALIZABLE_TYPES = {
|
||||
AudioRawFrame: "audio",
|
||||
}
|
||||
|
||||
def serialize(self, frame: Frame) -> str | bytes | None:
|
||||
if not isinstance(frame, AudioRawFrame):
|
||||
if not isinstance(frame, OutputAudioRawFrame):
|
||||
return None
|
||||
audio_frame = AudioFrame(
|
||||
data=frame.audio,
|
||||
@@ -39,7 +38,7 @@ class LivekitFrameSerializer(FrameSerializer):
|
||||
|
||||
def deserialize(self, data: str | bytes) -> Frame | None:
|
||||
audio_frame: AudioFrame = pickle.loads(data)['frame']
|
||||
return AudioRawFrame(
|
||||
return InputAudioRawFrame(
|
||||
audio=bytes(audio_frame.data),
|
||||
sample_rate=audio_frame.sample_rate,
|
||||
num_channels=audio_frame.num_channels,
|
||||
|
||||
@@ -8,7 +8,11 @@ import dataclasses
|
||||
|
||||
import pipecat.frames.protobufs.frames_pb2 as frame_protos
|
||||
|
||||
from pipecat.frames.frames import AudioRawFrame, Frame, TextFrame, TranscriptionFrame
|
||||
from pipecat.frames.frames import (
|
||||
AudioRawFrame,
|
||||
Frame,
|
||||
TextFrame,
|
||||
TranscriptionFrame)
|
||||
from pipecat.serializers.base_serializer import FrameSerializer
|
||||
|
||||
from loguru import logger
|
||||
@@ -29,14 +33,15 @@ class ProtobufFrameSerializer(FrameSerializer):
|
||||
def serialize(self, frame: Frame) -> str | bytes | None:
|
||||
proto_frame = frame_protos.Frame()
|
||||
if type(frame) not in self.SERIALIZABLE_TYPES:
|
||||
raise ValueError(
|
||||
f"Frame type {type(frame)} is not serializable. You may need to add it to ProtobufFrameSerializer.SERIALIZABLE_FIELDS.")
|
||||
logger.warning(f"Frame type {type(frame)} is not serializable")
|
||||
return None
|
||||
|
||||
# ignoring linter errors; we check that type(frame) is in this dict above
|
||||
proto_optional_name = self.SERIALIZABLE_TYPES[type(frame)] # type: ignore
|
||||
for field in dataclasses.fields(frame): # type: ignore
|
||||
setattr(getattr(proto_frame, proto_optional_name), field.name,
|
||||
getattr(frame, field.name))
|
||||
value = getattr(frame, field.name)
|
||||
if value:
|
||||
setattr(getattr(proto_frame, proto_optional_name), field.name, value)
|
||||
|
||||
result = proto_frame.SerializeToString()
|
||||
return result
|
||||
@@ -48,8 +53,8 @@ class ProtobufFrameSerializer(FrameSerializer):
|
||||
|
||||
>>> serializer = ProtobufFrameSerializer()
|
||||
>>> serializer.deserialize(
|
||||
... serializer.serialize(AudioFrame(data=b'1234567890')))
|
||||
AudioFrame(data=b'1234567890')
|
||||
... serializer.serialize(OutputAudioFrame(data=b'1234567890')))
|
||||
InputAudioFrame(data=b'1234567890')
|
||||
|
||||
>>> serializer.deserialize(
|
||||
... serializer.serialize(TextFrame(text='hello world')))
|
||||
@@ -75,10 +80,13 @@ class ProtobufFrameSerializer(FrameSerializer):
|
||||
# Remove special fields if needed
|
||||
id = getattr(args, "id")
|
||||
name = getattr(args, "name")
|
||||
pts = getattr(args, "pts")
|
||||
if not id:
|
||||
del args_dict["id"]
|
||||
if not name:
|
||||
del args_dict["name"]
|
||||
if not pts:
|
||||
del args_dict["pts"]
|
||||
|
||||
# Create the instance
|
||||
instance = class_name(**args_dict)
|
||||
@@ -88,5 +96,7 @@ class ProtobufFrameSerializer(FrameSerializer):
|
||||
setattr(instance, "id", getattr(args, "id"))
|
||||
if name:
|
||||
setattr(instance, "name", getattr(args, "name"))
|
||||
if pts:
|
||||
setattr(instance, "pts", getattr(args, "pts"))
|
||||
|
||||
return instance
|
||||
|
||||
@@ -9,7 +9,10 @@ import json
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from pipecat.frames.frames import AudioRawFrame, Frame, StartInterruptionFrame
|
||||
from pipecat.frames.frames import (
|
||||
AudioRawFrame,
|
||||
Frame,
|
||||
StartInterruptionFrame)
|
||||
from pipecat.serializers.base_serializer import FrameSerializer
|
||||
from pipecat.utils.audio import ulaw_to_pcm, pcm_to_ulaw
|
||||
|
||||
@@ -19,10 +22,6 @@ class TwilioFrameSerializer(FrameSerializer):
|
||||
twilio_sample_rate: int = 8000
|
||||
sample_rate: int = 16000
|
||||
|
||||
SERIALIZABLE_TYPES = {
|
||||
AudioRawFrame: "audio",
|
||||
}
|
||||
|
||||
def __init__(self, stream_sid: str, params: InputParams = InputParams()):
|
||||
self._stream_sid = stream_sid
|
||||
self._params = params
|
||||
|
||||
@@ -22,6 +22,7 @@ from pipecat.frames.frames import (
|
||||
STTModelUpdateFrame,
|
||||
StartFrame,
|
||||
StartInterruptionFrame,
|
||||
TTSAudioRawFrame,
|
||||
TTSLanguageUpdateFrame,
|
||||
TTSModelUpdateFrame,
|
||||
TTSSpeakFrame,
|
||||
@@ -277,7 +278,7 @@ class AsyncTTSService(TTSService):
|
||||
if self._push_stop_frames and (
|
||||
isinstance(frame, StartInterruptionFrame) or
|
||||
isinstance(frame, TTSStartedFrame) or
|
||||
isinstance(frame, AudioRawFrame) or
|
||||
isinstance(frame, TTSAudioRawFrame) or
|
||||
isinstance(frame, TTSStoppedFrame)):
|
||||
await self._stop_frame_queue.put(frame)
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@ from PIL import Image
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from pipecat.frames.frames import (
|
||||
AudioRawFrame,
|
||||
CancelFrame,
|
||||
EndFrame,
|
||||
ErrorFrame,
|
||||
Frame,
|
||||
StartFrame,
|
||||
TTSAudioRawFrame,
|
||||
TTSStartedFrame,
|
||||
TTSStoppedFrame,
|
||||
TranscriptionFrame,
|
||||
@@ -115,7 +115,7 @@ class AzureTTSService(TTSService):
|
||||
await self.stop_ttfb_metrics()
|
||||
await self.push_frame(TTSStartedFrame())
|
||||
# Azure always sends a 44-byte header. Strip it off.
|
||||
yield AudioRawFrame(audio=result.audio_data[44:], sample_rate=self._sample_rate, num_channels=1)
|
||||
yield TTSAudioRawFrame(audio=result.audio_data[44:], sample_rate=self._sample_rate, num_channels=1)
|
||||
await self.push_frame(TTSStoppedFrame())
|
||||
elif result.reason == ResultReason.Canceled:
|
||||
cancellation_details = result.cancellation_details
|
||||
|
||||
@@ -15,10 +15,10 @@ from pipecat.frames.frames import (
|
||||
CancelFrame,
|
||||
ErrorFrame,
|
||||
Frame,
|
||||
AudioRawFrame,
|
||||
StartInterruptionFrame,
|
||||
StartFrame,
|
||||
EndFrame,
|
||||
TTSAudioRawFrame,
|
||||
TTSStartedFrame,
|
||||
TTSStoppedFrame,
|
||||
LLMFullResponseEndFrame
|
||||
@@ -201,7 +201,7 @@ class CartesiaTTSService(AsyncWordTTSService):
|
||||
elif msg["type"] == "chunk":
|
||||
await self.stop_ttfb_metrics()
|
||||
self.start_word_timestamps()
|
||||
frame = AudioRawFrame(
|
||||
frame = TTSAudioRawFrame(
|
||||
audio=base64.b64decode(msg["data"]),
|
||||
sample_rate=self._output_format["sample_rate"],
|
||||
num_channels=1
|
||||
@@ -326,7 +326,7 @@ class CartesiaHttpTTSService(TTSService):
|
||||
|
||||
await self.stop_ttfb_metrics()
|
||||
|
||||
frame = AudioRawFrame(
|
||||
frame = TTSAudioRawFrame(
|
||||
audio=output["audio"],
|
||||
sample_rate=self._output_format["sample_rate"],
|
||||
num_channels=1
|
||||
|
||||
@@ -9,13 +9,13 @@ import aiohttp
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from pipecat.frames.frames import (
|
||||
AudioRawFrame,
|
||||
CancelFrame,
|
||||
EndFrame,
|
||||
ErrorFrame,
|
||||
Frame,
|
||||
InterimTranscriptionFrame,
|
||||
StartFrame,
|
||||
TTSAudioRawFrame,
|
||||
TTSStartedFrame,
|
||||
TTSStoppedFrame,
|
||||
TranscriptionFrame)
|
||||
@@ -101,7 +101,8 @@ class DeepgramTTSService(TTSService):
|
||||
await self.push_frame(TTSStartedFrame())
|
||||
async for data in r.content:
|
||||
await self.stop_ttfb_metrics()
|
||||
frame = AudioRawFrame(audio=data, sample_rate=self._sample_rate, num_channels=1)
|
||||
frame = TTSAudioRawFrame(
|
||||
audio=data, sample_rate=self._sample_rate, num_channels=1)
|
||||
yield frame
|
||||
await self.push_frame(TTSStoppedFrame())
|
||||
except Exception as e:
|
||||
|
||||
@@ -12,12 +12,12 @@ from typing import Any, AsyncGenerator, List, Literal, Mapping, Tuple
|
||||
from pydantic import BaseModel
|
||||
|
||||
from pipecat.frames.frames import (
|
||||
AudioRawFrame,
|
||||
CancelFrame,
|
||||
EndFrame,
|
||||
Frame,
|
||||
StartFrame,
|
||||
StartInterruptionFrame,
|
||||
TTSAudioRawFrame,
|
||||
TTSStartedFrame,
|
||||
TTSStoppedFrame)
|
||||
from pipecat.processors.frame_processor import FrameDirection
|
||||
@@ -209,7 +209,7 @@ class ElevenLabsTTSService(AsyncWordTTSService):
|
||||
self.start_word_timestamps()
|
||||
|
||||
audio = base64.b64decode(msg["audio"])
|
||||
frame = AudioRawFrame(audio, self.sample_rate, 1)
|
||||
frame = TTSAudioRawFrame(audio, self.sample_rate, 1)
|
||||
await self.push_frame(frame)
|
||||
|
||||
if msg.get("alignment"):
|
||||
|
||||
@@ -10,13 +10,13 @@ from typing import AsyncGenerator
|
||||
|
||||
from pipecat.processors.frame_processor import FrameDirection
|
||||
from pipecat.frames.frames import (
|
||||
AudioRawFrame,
|
||||
CancelFrame,
|
||||
EndFrame,
|
||||
ErrorFrame,
|
||||
Frame,
|
||||
StartFrame,
|
||||
StartInterruptionFrame,
|
||||
TTSAudioRawFrame,
|
||||
TTSStartedFrame,
|
||||
TTSStoppedFrame,
|
||||
)
|
||||
@@ -126,7 +126,7 @@ class LmntTTSService(AsyncTTSService):
|
||||
await self.push_error(ErrorFrame(f'{self} error: {msg["error"]}'))
|
||||
elif "audio" in msg:
|
||||
await self.stop_ttfb_metrics()
|
||||
frame = AudioRawFrame(
|
||||
frame = TTSAudioRawFrame(
|
||||
audio=msg["audio"],
|
||||
sample_rate=self._output_format["sample_rate"],
|
||||
num_channels=1
|
||||
|
||||
@@ -17,13 +17,13 @@ from loguru import logger
|
||||
from PIL import Image
|
||||
|
||||
from pipecat.frames.frames import (
|
||||
AudioRawFrame,
|
||||
ErrorFrame,
|
||||
Frame,
|
||||
LLMFullResponseEndFrame,
|
||||
LLMFullResponseStartFrame,
|
||||
LLMMessagesFrame,
|
||||
LLMModelUpdateFrame,
|
||||
TTSAudioRawFrame,
|
||||
TTSStartedFrame,
|
||||
TTSStoppedFrame,
|
||||
TextFrame,
|
||||
@@ -365,7 +365,7 @@ class OpenAITTSService(TTSService):
|
||||
async for chunk in r.iter_bytes(8192):
|
||||
if len(chunk) > 0:
|
||||
await self.stop_ttfb_metrics()
|
||||
frame = AudioRawFrame(chunk, self.sample_rate, 1)
|
||||
frame = TTSAudioRawFrame(chunk, self.sample_rate, 1)
|
||||
yield frame
|
||||
await self.push_frame(TTSStoppedFrame())
|
||||
except BadRequestError as e:
|
||||
|
||||
@@ -9,7 +9,11 @@ import struct
|
||||
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from pipecat.frames.frames import AudioRawFrame, Frame, TTSStartedFrame, TTSStoppedFrame
|
||||
from pipecat.frames.frames import (
|
||||
Frame,
|
||||
TTSAudioRawFrame,
|
||||
TTSStartedFrame,
|
||||
TTSStoppedFrame)
|
||||
from pipecat.services.ai_services import TTSService
|
||||
|
||||
from loguru import logger
|
||||
@@ -91,7 +95,7 @@ class PlayHTTTSService(TTSService):
|
||||
else:
|
||||
if len(chunk):
|
||||
await self.stop_ttfb_metrics()
|
||||
frame = AudioRawFrame(chunk, 16000, 1)
|
||||
frame = TTSAudioRawFrame(chunk, 16000, 1)
|
||||
yield frame
|
||||
await self.push_frame(TTSStoppedFrame())
|
||||
except Exception as e:
|
||||
|
||||
@@ -4,23 +4,19 @@
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
|
||||
import base64
|
||||
import json
|
||||
import io
|
||||
import copy
|
||||
from typing import List, Optional
|
||||
from dataclasses import dataclass
|
||||
from asyncio import CancelledError
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from typing import List
|
||||
from dataclasses import dataclass
|
||||
from asyncio import CancelledError
|
||||
|
||||
from pipecat.frames.frames import (
|
||||
Frame,
|
||||
LLMModelUpdateFrame,
|
||||
TextFrame,
|
||||
VisionImageRawFrame,
|
||||
UserImageRequestFrame,
|
||||
UserImageRawFrame,
|
||||
LLMMessagesFrame,
|
||||
LLMFullResponseStartFrame,
|
||||
LLMFullResponseEndFrame,
|
||||
|
||||
@@ -9,10 +9,10 @@ import aiohttp
|
||||
from typing import Any, AsyncGenerator, Dict
|
||||
|
||||
from pipecat.frames.frames import (
|
||||
AudioRawFrame,
|
||||
ErrorFrame,
|
||||
Frame,
|
||||
StartFrame,
|
||||
TTSAudioRawFrame,
|
||||
TTSStartedFrame,
|
||||
TTSStoppedFrame)
|
||||
from pipecat.services.ai_services import TTSService
|
||||
@@ -128,7 +128,7 @@ class XTTSService(TTSService):
|
||||
# Convert the numpy array back to bytes
|
||||
resampled_audio_bytes = resampled_audio.astype(np.int16).tobytes()
|
||||
# Create the frame with the resampled audio
|
||||
frame = AudioRawFrame(resampled_audio_bytes, 16000, 1)
|
||||
frame = TTSAudioRawFrame(resampled_audio_bytes, 16000, 1)
|
||||
yield frame
|
||||
|
||||
# Process any remaining data in the buffer
|
||||
@@ -136,7 +136,7 @@ class XTTSService(TTSService):
|
||||
audio_np = np.frombuffer(buffer, dtype=np.int16)
|
||||
resampled_audio = resampy.resample(audio_np, 24000, 16000)
|
||||
resampled_audio_bytes = resampled_audio.astype(np.int16).tobytes()
|
||||
frame = AudioRawFrame(resampled_audio_bytes, 16000, 1)
|
||||
frame = TTSAudioRawFrame(resampled_audio_bytes, 16000, 1)
|
||||
yield frame
|
||||
|
||||
await self.push_frame(TTSStoppedFrame())
|
||||
|
||||
@@ -10,9 +10,9 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||
from pipecat.frames.frames import (
|
||||
AudioRawFrame,
|
||||
BotInterruptionFrame,
|
||||
CancelFrame,
|
||||
InputAudioRawFrame,
|
||||
StartFrame,
|
||||
EndFrame,
|
||||
Frame,
|
||||
@@ -59,7 +59,7 @@ class BaseInputTransport(FrameProcessor):
|
||||
def vad_analyzer(self) -> VADAnalyzer | None:
|
||||
return self._params.vad_analyzer
|
||||
|
||||
async def push_audio_frame(self, frame: AudioRawFrame):
|
||||
async def push_audio_frame(self, frame: InputAudioRawFrame):
|
||||
if self._params.audio_in_enabled or self._params.vad_enabled:
|
||||
await self._audio_in_queue.put(frame)
|
||||
|
||||
@@ -151,7 +151,7 @@ class BaseInputTransport(FrameProcessor):
|
||||
vad_state: VADState = VADState.QUIET
|
||||
while True:
|
||||
try:
|
||||
frame: AudioRawFrame = await self._audio_in_queue.get()
|
||||
frame: InputAudioRawFrame = await self._audio_in_queue.get()
|
||||
|
||||
audio_passthrough = True
|
||||
|
||||
|
||||
@@ -15,17 +15,17 @@ from typing import List
|
||||
|
||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||
from pipecat.frames.frames import (
|
||||
AudioRawFrame,
|
||||
BotSpeakingFrame,
|
||||
BotStartedSpeakingFrame,
|
||||
BotStoppedSpeakingFrame,
|
||||
CancelFrame,
|
||||
MetricsFrame,
|
||||
OutputAudioRawFrame,
|
||||
OutputImageRawFrame,
|
||||
SpriteFrame,
|
||||
StartFrame,
|
||||
EndFrame,
|
||||
Frame,
|
||||
ImageRawFrame,
|
||||
StartInterruptionFrame,
|
||||
StopInterruptionFrame,
|
||||
SystemFrame,
|
||||
@@ -122,7 +122,7 @@ class BaseOutputTransport(FrameProcessor):
|
||||
async def send_metrics(self, frame: MetricsFrame):
|
||||
pass
|
||||
|
||||
async def write_frame_to_camera(self, frame: ImageRawFrame):
|
||||
async def write_frame_to_camera(self, frame: OutputImageRawFrame):
|
||||
pass
|
||||
|
||||
async def write_raw_audio_frames(self, frames: bytes):
|
||||
@@ -162,9 +162,9 @@ class BaseOutputTransport(FrameProcessor):
|
||||
await self._sink_queue.put(frame)
|
||||
await self.stop(frame)
|
||||
# Other frames.
|
||||
elif isinstance(frame, AudioRawFrame):
|
||||
elif isinstance(frame, OutputAudioRawFrame):
|
||||
await self._handle_audio(frame)
|
||||
elif isinstance(frame, ImageRawFrame) or isinstance(frame, SpriteFrame):
|
||||
elif isinstance(frame, OutputImageRawFrame) or isinstance(frame, SpriteFrame):
|
||||
await self._handle_image(frame)
|
||||
elif isinstance(frame, TransportMessageFrame) and frame.urgent:
|
||||
await self.send_message(frame)
|
||||
@@ -191,7 +191,7 @@ class BaseOutputTransport(FrameProcessor):
|
||||
if self._bot_speaking:
|
||||
await self._bot_stopped_speaking()
|
||||
|
||||
async def _handle_audio(self, frame: AudioRawFrame):
|
||||
async def _handle_audio(self, frame: OutputAudioRawFrame):
|
||||
if not self._params.audio_out_enabled:
|
||||
return
|
||||
|
||||
@@ -200,12 +200,14 @@ class BaseOutputTransport(FrameProcessor):
|
||||
else:
|
||||
self._audio_buffer.extend(frame.audio)
|
||||
while len(self._audio_buffer) >= self._audio_chunk_size:
|
||||
chunk = AudioRawFrame(bytes(self._audio_buffer[:self._audio_chunk_size]),
|
||||
sample_rate=frame.sample_rate, num_channels=frame.num_channels)
|
||||
chunk = OutputAudioRawFrame(
|
||||
bytes(self._audio_buffer[:self._audio_chunk_size]),
|
||||
sample_rate=frame.sample_rate, num_channels=frame.num_channels
|
||||
)
|
||||
await self._sink_queue.put(chunk)
|
||||
self._audio_buffer = self._audio_buffer[self._audio_chunk_size:]
|
||||
|
||||
async def _handle_image(self, frame: ImageRawFrame | SpriteFrame):
|
||||
async def _handle_image(self, frame: OutputImageRawFrame | SpriteFrame):
|
||||
if not self._params.camera_out_enabled:
|
||||
return
|
||||
|
||||
@@ -226,11 +228,11 @@ class BaseOutputTransport(FrameProcessor):
|
||||
self._sink_clock_task = loop.create_task(self._sink_clock_task_handler())
|
||||
|
||||
async def _sink_frame_handler(self, frame: Frame):
|
||||
if isinstance(frame, AudioRawFrame):
|
||||
if isinstance(frame, OutputAudioRawFrame):
|
||||
await self.write_raw_audio_frames(frame.audio)
|
||||
await self.push_frame(frame)
|
||||
await self.push_frame(BotSpeakingFrame(), FrameDirection.UPSTREAM)
|
||||
elif isinstance(frame, ImageRawFrame):
|
||||
elif isinstance(frame, OutputImageRawFrame):
|
||||
await self._set_camera_image(frame)
|
||||
elif isinstance(frame, SpriteFrame):
|
||||
await self._set_camera_images(frame.images)
|
||||
@@ -305,10 +307,10 @@ class BaseOutputTransport(FrameProcessor):
|
||||
# Camera out
|
||||
#
|
||||
|
||||
async def send_image(self, frame: ImageRawFrame | SpriteFrame):
|
||||
async def send_image(self, frame: OutputImageRawFrame | SpriteFrame):
|
||||
await self.process_frame(frame, FrameDirection.DOWNSTREAM)
|
||||
|
||||
async def _draw_image(self, frame: ImageRawFrame):
|
||||
async def _draw_image(self, frame: OutputImageRawFrame):
|
||||
desired_size = (self._params.camera_out_width, self._params.camera_out_height)
|
||||
|
||||
if frame.size != desired_size:
|
||||
@@ -316,14 +318,17 @@ class BaseOutputTransport(FrameProcessor):
|
||||
resized_image = image.resize(desired_size)
|
||||
logger.warning(
|
||||
f"{frame} does not have the expected size {desired_size}, resizing")
|
||||
frame = ImageRawFrame(resized_image.tobytes(), resized_image.size, resized_image.format)
|
||||
frame = OutputImageRawFrame(
|
||||
resized_image.tobytes(),
|
||||
resized_image.size,
|
||||
resized_image.format)
|
||||
|
||||
await self.write_frame_to_camera(frame)
|
||||
|
||||
async def _set_camera_image(self, image: ImageRawFrame):
|
||||
async def _set_camera_image(self, image: OutputImageRawFrame):
|
||||
self._camera_images = itertools.cycle([image])
|
||||
|
||||
async def _set_camera_images(self, images: List[ImageRawFrame]):
|
||||
async def _set_camera_images(self, images: List[OutputImageRawFrame]):
|
||||
self._camera_images = itertools.cycle(images)
|
||||
|
||||
async def _camera_out_task_handler(self):
|
||||
@@ -375,7 +380,7 @@ class BaseOutputTransport(FrameProcessor):
|
||||
# Audio out
|
||||
#
|
||||
|
||||
async def send_audio(self, frame: AudioRawFrame):
|
||||
async def send_audio(self, frame: OutputAudioRawFrame):
|
||||
await self.process_frame(frame, FrameDirection.DOWNSTREAM)
|
||||
|
||||
async def _audio_out_task_handler(self):
|
||||
|
||||
@@ -8,7 +8,7 @@ import asyncio
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from pipecat.frames.frames import AudioRawFrame, StartFrame
|
||||
from pipecat.frames.frames import InputAudioRawFrame, StartFrame
|
||||
from pipecat.processors.frame_processor import FrameProcessor
|
||||
from pipecat.transports.base_input import BaseInputTransport
|
||||
from pipecat.transports.base_output import BaseOutputTransport
|
||||
@@ -54,9 +54,9 @@ class LocalAudioInputTransport(BaseInputTransport):
|
||||
self._in_stream.close()
|
||||
|
||||
def _audio_in_callback(self, in_data, frame_count, time_info, status):
|
||||
frame = AudioRawFrame(audio=in_data,
|
||||
sample_rate=self._params.audio_in_sample_rate,
|
||||
num_channels=self._params.audio_in_channels)
|
||||
frame = InputAudioRawFrame(audio=in_data,
|
||||
sample_rate=self._params.audio_in_sample_rate,
|
||||
num_channels=self._params.audio_in_channels)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(self.push_audio_frame(frame), self.get_event_loop())
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
import numpy as np
|
||||
import tkinter as tk
|
||||
|
||||
from pipecat.frames.frames import AudioRawFrame, ImageRawFrame, StartFrame
|
||||
from pipecat.processors.frame_processor import FrameProcessor
|
||||
from pipecat.frames.frames import InputAudioRawFrame, OutputImageRawFrame, StartFrame
|
||||
from pipecat.transports.base_input import BaseInputTransport
|
||||
from pipecat.transports.base_output import BaseOutputTransport
|
||||
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
||||
@@ -64,9 +63,9 @@ class TkInputTransport(BaseInputTransport):
|
||||
self._in_stream.close()
|
||||
|
||||
def _audio_in_callback(self, in_data, frame_count, time_info, status):
|
||||
frame = AudioRawFrame(audio=in_data,
|
||||
sample_rate=self._params.audio_in_sample_rate,
|
||||
num_channels=self._params.audio_in_channels)
|
||||
frame = InputAudioRawFrame(audio=in_data,
|
||||
sample_rate=self._params.audio_in_sample_rate,
|
||||
num_channels=self._params.audio_in_channels)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(self.push_audio_frame(frame), self.get_event_loop())
|
||||
|
||||
@@ -108,10 +107,10 @@ class TkOutputTransport(BaseOutputTransport):
|
||||
async def write_raw_audio_frames(self, frames: bytes):
|
||||
await self.get_event_loop().run_in_executor(self._executor, self._out_stream.write, frames)
|
||||
|
||||
async def write_frame_to_camera(self, frame: ImageRawFrame):
|
||||
async def write_frame_to_camera(self, frame: OutputImageRawFrame):
|
||||
self.get_event_loop().call_soon(self._write_frame_to_tk, frame)
|
||||
|
||||
def _write_frame_to_tk(self, frame: ImageRawFrame):
|
||||
def _write_frame_to_tk(self, frame: OutputImageRawFrame):
|
||||
width = frame.size[0]
|
||||
height = frame.size[1]
|
||||
data = f"P6 {width} {height} 255 ".encode() + frame.image
|
||||
|
||||
@@ -12,8 +12,16 @@ import wave
|
||||
from typing import Awaitable, Callable
|
||||
from pydantic.main import BaseModel
|
||||
|
||||
from pipecat.frames.frames import AudioRawFrame, CancelFrame, EndFrame, Frame, StartFrame, StartInterruptionFrame
|
||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||
from pipecat.frames.frames import (
|
||||
AudioRawFrame,
|
||||
CancelFrame,
|
||||
EndFrame,
|
||||
Frame,
|
||||
InputAudioRawFrame,
|
||||
StartFrame,
|
||||
StartInterruptionFrame
|
||||
)
|
||||
from pipecat.processors.frame_processor import FrameDirection
|
||||
from pipecat.serializers.base_serializer import FrameSerializer
|
||||
from pipecat.transports.base_input import BaseInputTransport
|
||||
from pipecat.transports.base_output import BaseOutputTransport
|
||||
@@ -79,7 +87,11 @@ class FastAPIWebsocketInputTransport(BaseInputTransport):
|
||||
continue
|
||||
|
||||
if isinstance(frame, AudioRawFrame):
|
||||
await self.push_audio_frame(frame)
|
||||
await self.push_audio_frame(InputAudioRawFrame(
|
||||
audio=frame.audio,
|
||||
sample_rate=frame.sample_rate,
|
||||
num_channels=frame.num_channels)
|
||||
)
|
||||
|
||||
await self._callbacks.on_client_disconnected(self._websocket)
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ import wave
|
||||
from typing import Awaitable, Callable
|
||||
from pydantic.main import BaseModel
|
||||
|
||||
from pipecat.frames.frames import AudioRawFrame, CancelFrame, EndFrame, StartFrame
|
||||
from pipecat.processors.frame_processor import FrameProcessor
|
||||
from pipecat.frames.frames import AudioRawFrame, CancelFrame, EndFrame, InputAudioRawFrame, StartFrame
|
||||
from pipecat.serializers.base_serializer import FrameSerializer
|
||||
from pipecat.serializers.protobuf import ProtobufFrameSerializer
|
||||
from pipecat.transports.base_input import BaseInputTransport
|
||||
@@ -98,7 +97,11 @@ class WebsocketServerInputTransport(BaseInputTransport):
|
||||
continue
|
||||
|
||||
if isinstance(frame, AudioRawFrame):
|
||||
await self.queue_audio_frame(frame)
|
||||
await self.push_audio_frame(InputAudioRawFrame(
|
||||
audio=frame.audio,
|
||||
sample_rate=frame.sample_rate,
|
||||
num_channels=frame.num_channels)
|
||||
)
|
||||
else:
|
||||
await self.push_frame(frame)
|
||||
|
||||
|
||||
@@ -22,13 +22,14 @@ from daily import (
|
||||
from pydantic.main import BaseModel
|
||||
|
||||
from pipecat.frames.frames import (
|
||||
AudioRawFrame,
|
||||
CancelFrame,
|
||||
EndFrame,
|
||||
Frame,
|
||||
ImageRawFrame,
|
||||
InputAudioRawFrame,
|
||||
InterimTranscriptionFrame,
|
||||
MetricsFrame,
|
||||
OutputAudioRawFrame,
|
||||
OutputImageRawFrame,
|
||||
SpriteFrame,
|
||||
StartFrame,
|
||||
TranscriptionFrame,
|
||||
@@ -239,7 +240,7 @@ class DailyTransportClient(EventHandler):
|
||||
completion=completion_callback(future))
|
||||
await future
|
||||
|
||||
async def read_next_audio_frame(self) -> AudioRawFrame | None:
|
||||
async def read_next_audio_frame(self) -> InputAudioRawFrame | None:
|
||||
if not self._speaker:
|
||||
return None
|
||||
|
||||
@@ -252,7 +253,10 @@ class DailyTransportClient(EventHandler):
|
||||
audio = await future
|
||||
|
||||
if len(audio) > 0:
|
||||
return AudioRawFrame(audio=audio, sample_rate=sample_rate, num_channels=num_channels)
|
||||
return InputAudioRawFrame(
|
||||
audio=audio,
|
||||
sample_rate=sample_rate,
|
||||
num_channels=num_channels)
|
||||
else:
|
||||
# If we don't read any audio it could be there's no participant
|
||||
# connected. daily-python will return immediately if that's the
|
||||
@@ -268,7 +272,7 @@ class DailyTransportClient(EventHandler):
|
||||
self._mic.write_frames(frames, completion=completion_callback(future))
|
||||
await future
|
||||
|
||||
async def write_frame_to_camera(self, frame: ImageRawFrame):
|
||||
async def write_frame_to_camera(self, frame: OutputImageRawFrame):
|
||||
if not self._camera:
|
||||
return None
|
||||
|
||||
@@ -749,7 +753,7 @@ class DailyOutputTransport(BaseOutputTransport):
|
||||
async def write_raw_audio_frames(self, frames: bytes):
|
||||
await self._client.write_raw_audio_frames(frames)
|
||||
|
||||
async def write_frame_to_camera(self, frame: ImageRawFrame):
|
||||
async def write_frame_to_camera(self, frame: OutputImageRawFrame):
|
||||
await self._client.write_frame_to_camera(frame)
|
||||
|
||||
|
||||
@@ -829,11 +833,11 @@ class DailyTransport(BaseTransport):
|
||||
def participant_id(self) -> str:
|
||||
return self._client.participant_id
|
||||
|
||||
async def send_image(self, frame: ImageRawFrame | SpriteFrame):
|
||||
async def send_image(self, frame: OutputImageRawFrame | SpriteFrame):
|
||||
if self._output:
|
||||
await self._output.process_frame(frame, FrameDirection.DOWNSTREAM)
|
||||
|
||||
async def send_audio(self, frame: AudioRawFrame):
|
||||
async def send_audio(self, frame: OutputAudioRawFrame):
|
||||
if self._output:
|
||||
await self._output.process_frame(frame, FrameDirection.DOWNSTREAM)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user