go back to using @dataclass since they can be inspected
This commit is contained in:
@@ -9,7 +9,7 @@ import aiohttp
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import daily
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
AppFrame,
|
AppFrame,
|
||||||
@@ -44,20 +44,13 @@ logger.remove(0)
|
|||||||
logger.add(sys.stderr, level="DEBUG")
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class MonthFrame(AppFrame):
|
class MonthFrame(AppFrame):
|
||||||
def __init__(self, month):
|
month: str
|
||||||
super().__init__()
|
|
||||||
self.metadata["month"] = month
|
|
||||||
|
|
||||||
@ property
|
|
||||||
def month(self) -> str:
|
|
||||||
return self.metadata["month"]
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}(month: {self.month})"
|
return f"{self.name}(month: {self.month})"
|
||||||
|
|
||||||
month: str
|
|
||||||
|
|
||||||
|
|
||||||
class MonthPrepender(FrameProcessor):
|
class MonthPrepender(FrameProcessor):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -69,7 +62,7 @@ class MonthPrepender(FrameProcessor):
|
|||||||
if isinstance(frame, MonthFrame):
|
if isinstance(frame, MonthFrame):
|
||||||
self.most_recent_month = frame.month
|
self.most_recent_month = frame.month
|
||||||
elif self.prepend_to_next_text_frame and isinstance(frame, TextFrame):
|
elif self.prepend_to_next_text_frame and isinstance(frame, TextFrame):
|
||||||
await self.push_frame(TextFrame(f"{self.most_recent_month}: {frame.data}"))
|
await self.push_frame(TextFrame(f"{self.most_recent_month}: {frame.text}"))
|
||||||
self.prepend_to_next_text_frame = False
|
self.prepend_to_next_text_frame = False
|
||||||
elif isinstance(frame, LLMResponseStartFrame):
|
elif isinstance(frame, LLMResponseStartFrame):
|
||||||
self.prepend_to_next_text_frame = True
|
self.prepend_to_next_text_frame = True
|
||||||
@@ -152,7 +145,7 @@ async def main(room_url):
|
|||||||
"content": f"Describe a nature photograph suitable for use in a calendar, for the month of {month}. Include only the image description with no preamble. Limit the description to one sentence, please.",
|
"content": f"Describe a nature photograph suitable for use in a calendar, for the month of {month}. Include only the image description with no preamble. Limit the description to one sentence, please.",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
frames.append(MonthFrame(month))
|
frames.append(MonthFrame(month=month))
|
||||||
frames.append(LLMMessagesFrame(messages))
|
frames.append(LLMMessagesFrame(messages))
|
||||||
|
|
||||||
frames.append(EndFrame())
|
frames.append(EndFrame())
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ async def main():
|
|||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
if isinstance(frame, AudioRawFrame):
|
if isinstance(frame, AudioRawFrame):
|
||||||
self.audio.extend(frame.data)
|
self.audio.extend(frame.audio)
|
||||||
self.frame = AudioRawFrame(
|
self.frame = AudioRawFrame(
|
||||||
bytes(self.audio), frame.sample_rate, frame.num_channels)
|
bytes(self.audio), frame.sample_rate, frame.num_channels)
|
||||||
|
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ class ImageSyncAggregator(FrameProcessor):
|
|||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
if not isinstance(frame, SystemFrame):
|
if not isinstance(frame, SystemFrame):
|
||||||
await self.push_frame(ImageRawFrame(self._speaking_image_bytes, (1024, 1024), self._speaking_image_format))
|
await self.push_frame(ImageRawFrame(image=self._speaking_image_bytes, size=(1024, 1024), format=self._speaking_image_format))
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
await self.push_frame(ImageRawFrame(self._waiting_image_bytes, (1024, 1024), self._waiting_image_format))
|
await self.push_frame(ImageRawFrame(image=self._waiting_image_bytes, size=(1024, 1024), format=self._waiting_image_format))
|
||||||
else:
|
else:
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ async def main(room_url: str):
|
|||||||
if isinstance(frame, TextFrame):
|
if isinstance(frame, TextFrame):
|
||||||
message += frame.text
|
message += frame.text
|
||||||
elif isinstance(frame, AudioFrame):
|
elif isinstance(frame, AudioFrame):
|
||||||
all_audio.extend(frame.data)
|
all_audio.extend(frame.audio)
|
||||||
|
|
||||||
return (message, all_audio)
|
return (message, all_audio)
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ for file in image_files:
|
|||||||
filename = os.path.splitext(os.path.basename(full_path))[0]
|
filename = os.path.splitext(os.path.basename(full_path))[0]
|
||||||
# Open the image and convert it to bytes
|
# Open the image and convert it to bytes
|
||||||
with Image.open(full_path) as img:
|
with Image.open(full_path) as img:
|
||||||
sprites[file] = ImageRawFrame(img.tobytes(), img.size, img.format)
|
sprites[file] = ImageRawFrame(image=img.tobytes(), size=img.size, format=img.format)
|
||||||
|
|
||||||
# When the bot isn't talking, show a static image of the cat listening
|
# When the bot isn't talking, show a static image of the cat listening
|
||||||
quiet_frame = sprites["sc-listen-1.png"]
|
quiet_frame = sprites["sc-listen-1.png"]
|
||||||
@@ -99,7 +99,7 @@ class NameCheckFilter(FrameProcessor):
|
|||||||
|
|
||||||
# TODO: split up transcription by participant
|
# TODO: split up transcription by participant
|
||||||
if isinstance(frame, TranscriptionFrame):
|
if isinstance(frame, TranscriptionFrame):
|
||||||
content = frame.data
|
content = frame.text
|
||||||
self._sentence += content
|
self._sentence += content
|
||||||
if self._sentence.endswith((".", "?", "!")):
|
if self._sentence.endswith((".", "?", "!")):
|
||||||
if any(name in self._sentence for name in self._names):
|
if any(name in self._sentence for name in self._names):
|
||||||
|
|||||||
@@ -4,214 +4,195 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
from typing import Any, List
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from pipecat.utils.utils import obj_count, obj_id
|
from pipecat.utils.utils import obj_count, obj_id
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class Frame:
|
class Frame:
|
||||||
def __init__(self, data=None):
|
id: int = field(init=False)
|
||||||
|
name: str = field(init=False)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
self.id: int = obj_id()
|
self.id: int = obj_id()
|
||||||
self.data: Any = data
|
|
||||||
self.metadata = {}
|
|
||||||
self.name: str = f"{self.__class__.__name__}#{obj_count(self)}"
|
self.name: str = f"{self.__class__.__name__}#{obj_count(self)}"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class DataFrame(Frame):
|
class DataFrame(Frame):
|
||||||
def __init__(self, data):
|
pass
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class AudioRawFrame(DataFrame):
|
class AudioRawFrame(DataFrame):
|
||||||
def __init__(self, data, sample_rate: int, num_channels: int):
|
"""A chunk of audio. Will be played by the transport if the transport's
|
||||||
super().__init__(data)
|
microphone has been enabled.
|
||||||
self.metadata["sample_rate"] = sample_rate
|
|
||||||
self.metadata["num_channels"] = num_channels
|
|
||||||
self.metadata["num_frames"] = int(len(data) / (num_channels * 2))
|
|
||||||
|
|
||||||
@property
|
"""
|
||||||
def num_frames(self) -> int:
|
audio: bytes
|
||||||
return self.metadata["num_frames"]
|
sample_rate: int
|
||||||
|
num_channels: int
|
||||||
|
|
||||||
@property
|
def __post_init__(self):
|
||||||
def sample_rate(self) -> int:
|
super().__post_init__()
|
||||||
return self.metadata["sample_rate"]
|
self.num_frames = int(len(self.audio) / (self.num_channels * 2))
|
||||||
|
|
||||||
@property
|
|
||||||
def num_channels(self) -> int:
|
|
||||||
return self.metadata["num_channels"]
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}(frames: {self.num_frames}, sample_rate: {self.sample_rate}, channels: {self.num_channels})"
|
return f"{self.name}(size: {len(self.audio)}, frames: {self.num_frames}, sample_rate: {self.sample_rate}, channels: {self.num_channels})"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class ImageRawFrame(DataFrame):
|
class ImageRawFrame(DataFrame):
|
||||||
def __init__(self, data, size: tuple[int, int], format: str):
|
"""An image. Will be shown by the transport if the transport's camera is
|
||||||
super().__init__(data)
|
enabled.
|
||||||
self.metadata["size"] = size
|
|
||||||
self.metadata["format"] = format
|
|
||||||
|
|
||||||
@property
|
"""
|
||||||
def image(self) -> bytes:
|
image: bytes
|
||||||
return self.data
|
size: Tuple[int, int]
|
||||||
|
format: str
|
||||||
@property
|
|
||||||
def size(self) -> tuple[int, int]:
|
|
||||||
return self.metadata["size"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def format(self) -> str:
|
|
||||||
return self.metadata["format"]
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}(size: {self.size}, format: {self.format})"
|
return f"{self.name}(size: {self.size}, format: {self.format})"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class URLImageRawFrame(ImageRawFrame):
|
class URLImageRawFrame(ImageRawFrame):
|
||||||
def __init__(self, url: str, data, size: tuple[int, int], format: str):
|
"""An image with an associated URL. Will be shown by the transport if the
|
||||||
super().__init__(data, size, format)
|
transport's camera is enabled.
|
||||||
self.metadata["url"] = url
|
|
||||||
|
|
||||||
@property
|
"""
|
||||||
def url(self) -> str:
|
url: str | None
|
||||||
return self.metadata["url"]
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}(url: {self.url}, size: {self.size}, format: {self.format})"
|
return f"{self.name}(url: {self.url}, size: {self.size}, format: {self.format})"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class VisionImageRawFrame(ImageRawFrame):
|
class VisionImageRawFrame(ImageRawFrame):
|
||||||
def __init__(self, text: str, data, size: tuple[int, int], format: str):
|
"""An image with an associated text to ask for a description of it. Will be
|
||||||
super().__init__(data, size, format)
|
shown by the transport if the transport's camera is enabled.
|
||||||
self.metadata["text"] = text
|
|
||||||
|
|
||||||
@property
|
"""
|
||||||
def text(self) -> str:
|
text: str | None
|
||||||
return self.metadata["text"]
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}(text: {self.text}, size: {self.size}, format: {self.format})"
|
return f"{self.name}(text: {self.text}, size: {self.size}, format: {self.format})"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class UserImageRawFrame(ImageRawFrame):
|
class UserImageRawFrame(ImageRawFrame):
|
||||||
def __init__(self, user_id: str, data, size: tuple[int, int], format: str):
|
"""An image associated to a user. Will be shown by the transport if the
|
||||||
super().__init__(data, size, format)
|
transport's camera is enabled.
|
||||||
self.metadata["user_id"] = user_id
|
|
||||||
|
|
||||||
@property
|
"""
|
||||||
def user_id(self) -> str:
|
user_id: str
|
||||||
return self.metadata["user_id"]
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}(user: {self.user_id}, size: {self.size}, format: {self.format})"
|
return f"{self.name}(user: {self.user_id}, size: {self.size}, format: {self.format})"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class SpriteFrame(Frame):
|
class SpriteFrame(Frame):
|
||||||
def __init__(self, data):
|
"""An animated sprite. Will be shown by the transport if the transport's
|
||||||
super().__init__(data)
|
camera is enabled. Will play at the framerate specified in the transport's
|
||||||
|
`fps` constructor parameter.
|
||||||
|
|
||||||
@property
|
"""
|
||||||
def images(self) -> List[ImageRawFrame]:
|
images: List[ImageRawFrame]
|
||||||
return self.data
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}(size: {len(self.images)})"
|
return f"{self.name}(size: {len(self.images)})"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class TextFrame(DataFrame):
|
class TextFrame(DataFrame):
|
||||||
def __init__(self, data):
|
"""A chunk of text. Emitted by LLM services, consumed by TTS services, can
|
||||||
super().__init__(data)
|
be used to send text through pipelines.
|
||||||
|
|
||||||
@property
|
"""
|
||||||
def text(self) -> str:
|
text: str
|
||||||
return self.data
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.name}: "{self.text}"'
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class TranscriptionFrame(TextFrame):
|
class TranscriptionFrame(TextFrame):
|
||||||
def __init__(self, data, user_id: str, timestamp: int):
|
"""A text frame with transcription-specific data. Will be placed in the
|
||||||
super().__init__(data)
|
transport's receive queue when a participant speaks.
|
||||||
self.metadata["user_id"] = user_id
|
|
||||||
self.metadata["timestamp"] = timestamp
|
|
||||||
|
|
||||||
@property
|
"""
|
||||||
def user_id(self) -> str:
|
user_id: str
|
||||||
return self.metadata["user_id"]
|
timestamp: str
|
||||||
|
|
||||||
@property
|
|
||||||
def timestamp(self) -> str:
|
|
||||||
return self.metadata["timestamp"]
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}(user: {self.user_id}, timestamp: {self.timestamp})"
|
return f"{self.name}(user: {self.user_id}, timestamp: {self.timestamp})"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class InterimTranscriptionFrame(TextFrame):
|
class InterimTranscriptionFrame(TextFrame):
|
||||||
def __init__(self, data, user_id: str, timestamp: int):
|
"""A text frame with interim transcription-specific data. Will be placed in
|
||||||
super().__init__(data)
|
the transport's receive queue when a participant speaks."""
|
||||||
self.metadata["user_id"] = user_id
|
user_id: str
|
||||||
self.metadata["timestamp"] = timestamp
|
timestamp: str
|
||||||
|
|
||||||
@property
|
|
||||||
def user_id(self) -> str:
|
|
||||||
return self.metadata["user_id"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def timestamp(self) -> str:
|
|
||||||
return self.metadata["timestamp"]
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}(user: {self.user_id}, timestamp: {self.timestamp})"
|
return f"{self.name}(user: {self.user_id}, timestamp: {self.timestamp})"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class LLMMessagesFrame(DataFrame):
|
class LLMMessagesFrame(DataFrame):
|
||||||
"""A frame containing a list of LLM messages. Used to signal that an LLM
|
"""A frame containing a list of LLM messages. Used to signal that an LLM
|
||||||
service should run a chat completion and emit an LLM started response event,
|
service should run a chat completion and emit an LLMStartFrames, TextFrames
|
||||||
text frames and an LLM stopped response event.
|
and an LLMEndFrame. Note that the messages property on this class is
|
||||||
"""
|
mutable, and will be be updated by various ResponseAggregator frame
|
||||||
|
processors.
|
||||||
|
|
||||||
def __init__(self, messages):
|
"""
|
||||||
super().__init__(messages)
|
messages: List[dict]
|
||||||
|
|
||||||
#
|
#
|
||||||
# App frames. Application user-defined frames.
|
# App frames. Application user-defined frames.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class AppFrame(Frame):
|
class AppFrame(Frame):
|
||||||
def __init__(self, data=None):
|
pass
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# System frames
|
# System frames
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class SystemFrame(Frame):
|
class SystemFrame(Frame):
|
||||||
def __init__(self, data=None):
|
pass
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class StartFrame(SystemFrame):
|
class StartFrame(SystemFrame):
|
||||||
def __init__(self):
|
"""This is the first frame that should be pushed down a pipeline."""
|
||||||
super().__init__()
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class CancelFrame(SystemFrame):
|
class CancelFrame(SystemFrame):
|
||||||
def __init__(self):
|
"""Indicates that a pipeline needs to stop right away."""
|
||||||
super().__init__()
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class ErrorFrame(SystemFrame):
|
class ErrorFrame(SystemFrame):
|
||||||
def __init__(self, data):
|
"""This is used notify upstream that an error has occurred downstream the
|
||||||
super().__init__(data)
|
pipeline."""
|
||||||
self.metadata["error"] = data
|
error: str | None
|
||||||
|
|
||||||
@property
|
|
||||||
def error(self) -> str:
|
|
||||||
return self.metadata["error"]
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}(error: {self.error})"
|
return f"{self.name}(error: {self.error})"
|
||||||
@@ -221,247 +202,75 @@ class ErrorFrame(SystemFrame):
|
|||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class ControlFrame(Frame):
|
class ControlFrame(Frame):
|
||||||
def __init__(self, data=None):
|
pass
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class EndFrame(ControlFrame):
|
class EndFrame(ControlFrame):
|
||||||
def __init__(self):
|
"""Indicates that a pipeline has ended and frame processors and pipelines
|
||||||
super().__init__()
|
should be shut down. If the transport receives this frame, it will stop
|
||||||
|
sending frames to its output channel(s) and close all its threads. Note,
|
||||||
|
that this is a control frame, which means it will received in the order it
|
||||||
|
was sent (unline system frames).
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class LLMResponseStartFrame(ControlFrame):
|
class LLMResponseStartFrame(ControlFrame):
|
||||||
"""Used to indicate the beginning of an LLM response. Following TextFrames
|
"""Used to indicate the beginning of an LLM response. Following TextFrames
|
||||||
are part of the LLM response until an LLMResponseEndFrame"""
|
are part of the LLM response until an LLMResponseEndFrame"""
|
||||||
|
pass
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class LLMResponseEndFrame(ControlFrame):
|
class LLMResponseEndFrame(ControlFrame):
|
||||||
"""Indicates the end of an LLM response."""
|
"""Indicates the end of an LLM response."""
|
||||||
|
pass
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class UserStartedSpeakingFrame(ControlFrame):
|
class UserStartedSpeakingFrame(ControlFrame):
|
||||||
def __init__(self):
|
"""Emitted by VAD to indicate that a user has started speaking. This can be
|
||||||
super().__init__()
|
used for interruptions or other times when detecting that someone is
|
||||||
|
speaking is more important than knowing what they're saying (as you will
|
||||||
|
with a TranscriptionFrame)
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class UserStoppedSpeakingFrame(ControlFrame):
|
class UserStoppedSpeakingFrame(ControlFrame):
|
||||||
def __init__(self):
|
"""Emitted by the VAD to indicate that a user stopped speaking."""
|
||||||
super().__init__()
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class TTSStartedFrame(ControlFrame):
|
class TTSStartedFrame(ControlFrame):
|
||||||
def __init__(self):
|
"""Used to indicate the beginning of a TTS response. Following
|
||||||
super().__init__()
|
AudioRawFrames are part of the TTS response until an TTSEndFrame. 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
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class TTSStoppedFrame(ControlFrame):
|
class TTSStoppedFrame(ControlFrame):
|
||||||
def __init__(self):
|
"""Indicates the end of a TTS response."""
|
||||||
super().__init__()
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class UserImageRequestFrame(ControlFrame):
|
class UserImageRequestFrame(ControlFrame):
|
||||||
def __init__(self, user_id):
|
"""A frame user to request an image from the given user."""
|
||||||
super().__init__()
|
user_id: str
|
||||||
self.metadata["user_id"] = user_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def user_id(self) -> str:
|
|
||||||
return self.metadata["user_id"]
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}, user: {self.user_id}"
|
return f"{self.name}, user: {self.user_id}"
|
||||||
|
|
||||||
|
|
||||||
# class StartFrame(ControlFrame):
|
|
||||||
# """Used (but not required) to start a pipeline, and is also used to
|
|
||||||
# indicate that an interruption has ended and the transport should start
|
|
||||||
# processing frames again."""
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
# class EndFrame(ControlFrame):
|
|
||||||
# """Indicates that a pipeline has ended and frame processors and pipelines
|
|
||||||
# should be shut down. If the transport receives this frame, it will stop
|
|
||||||
# sending frames to its output channel(s) and close all its threads."""
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
# class EndPipeFrame(ControlFrame):
|
|
||||||
# """Indicates that a pipeline has ended but that the transport should
|
|
||||||
# continue processing. This frame is used in parallel pipelines and other
|
|
||||||
# sub-pipelines."""
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
# class PipelineStartedFrame(ControlFrame):
|
|
||||||
# """
|
|
||||||
# Used by the transport to indicate that execution of a pipeline is starting
|
|
||||||
# (or restarting). It should be the first frame your app receives when it
|
|
||||||
# starts, or when an interruptible pipeline has been interrupted.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
# @dataclass()
|
|
||||||
# class URLImageFrame(ImageFrame):
|
|
||||||
# """An image with an associated URL. Will be shown by the transport if the
|
|
||||||
# transport's camera is enabled.
|
|
||||||
|
|
||||||
# """
|
|
||||||
# url: str | None
|
|
||||||
|
|
||||||
# def __init__(self, url, image, size):
|
|
||||||
# super().__init__(image, size)
|
|
||||||
# self.url = url
|
|
||||||
|
|
||||||
# def __str__(self):
|
|
||||||
# return f"{self.__class__.__name__}, url: {self.url}, image size:
|
|
||||||
# {self.size[0]}x{self.size[1]}, buffer size: {len(self.image)} B"
|
|
||||||
|
|
||||||
|
|
||||||
# @dataclass()
|
|
||||||
# class VisionImageFrame(ImageFrame):
|
|
||||||
# """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.
|
|
||||||
|
|
||||||
# """
|
|
||||||
# text: str | None
|
|
||||||
|
|
||||||
# def __init__(self, text, image, size):
|
|
||||||
# super().__init__(image, size)
|
|
||||||
# self.text = text
|
|
||||||
|
|
||||||
# def __str__(self):
|
|
||||||
# return f"{self.__class__.__name__}, text: {self.text}, image size:
|
|
||||||
# {self.size[0]}x{self.size[1]}, buffer size: {len(self.image)} B"
|
|
||||||
|
|
||||||
|
|
||||||
# @dataclass()
|
|
||||||
# class UserImageFrame(ImageFrame):
|
|
||||||
# """An image associated to a user. Will be shown by the transport if the transport's camera is
|
|
||||||
# enabled."""
|
|
||||||
# user_id: str
|
|
||||||
|
|
||||||
# def __init__(self, user_id, image, size):
|
|
||||||
# super().__init__(image, size)
|
|
||||||
# self.user_id = user_id
|
|
||||||
|
|
||||||
# def __str__(self):
|
|
||||||
# return f"{self.__class__.__name__}, user: {self.user_id}, image size:
|
|
||||||
# {self.size[0]}x{self.size[1]}, buffer size: {len(self.image)} B"
|
|
||||||
|
|
||||||
|
|
||||||
# @dataclass()
|
|
||||||
# class UserImageRequestFrame(Frame):
|
|
||||||
# """A frame user to request an image from the given user."""
|
|
||||||
# user_id: str
|
|
||||||
|
|
||||||
# def __str__(self):
|
|
||||||
# return f"{self.__class__.__name__}, user: {self.user_id}"
|
|
||||||
|
|
||||||
|
|
||||||
# @dataclass()
|
|
||||||
# class SpriteFrame(Frame):
|
|
||||||
# """An animated sprite. Will be shown by the transport if the transport's
|
|
||||||
# camera is enabled. Will play at the framerate specified in the transport's
|
|
||||||
# `fps` constructor parameter."""
|
|
||||||
# images: list[bytes]
|
|
||||||
|
|
||||||
# def __str__(self):
|
|
||||||
# return f"{self.__class__.__name__}, list size: {len(self.images)}"
|
|
||||||
|
|
||||||
|
|
||||||
# @dataclass()
|
|
||||||
# class TextFrame(Frame):
|
|
||||||
# """A chunk of text. Emitted by LLM services, consumed by TTS services, can
|
|
||||||
# be used to send text through pipelines."""
|
|
||||||
# text: str
|
|
||||||
|
|
||||||
# def __str__(self):
|
|
||||||
# return f'{self.__class__.__name__}: "{self.text}"'
|
|
||||||
|
|
||||||
|
|
||||||
# class TTSStartFrame(ControlFrame):
|
|
||||||
# """Used to indicate the beginning of a TTS response. Following AudioFrames
|
|
||||||
# are part of the TTS response until an TTEndFrame. 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
|
|
||||||
|
|
||||||
|
|
||||||
# class TTSEndFrame(ControlFrame):
|
|
||||||
# """Indicates the end of a TTS response."""
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
# @dataclass()
|
|
||||||
# class LLMMessagesFrame(Frame):
|
|
||||||
# """A frame containing a list of LLM messages. Used to signal that an LLM
|
|
||||||
# service should run a chat completion and emit an LLMStartFrames, TextFrames
|
|
||||||
# and an LLMEndFrame.
|
|
||||||
# Note that the messages property on this class is mutable, and will be
|
|
||||||
# be updated by various ResponseAggregator frame processors."""
|
|
||||||
# messages: List[dict]
|
|
||||||
|
|
||||||
|
|
||||||
# @dataclass()
|
|
||||||
# class ReceivedAppMessageFrame(Frame):
|
|
||||||
# message: Any
|
|
||||||
# sender: str
|
|
||||||
|
|
||||||
# def __str__(self):
|
|
||||||
# return f"ReceivedAppMessageFrame: sender: {self.sender}, message: {self.message}"
|
|
||||||
|
|
||||||
|
|
||||||
# @dataclass()
|
|
||||||
# class SendAppMessageFrame(Frame):
|
|
||||||
# message: Any
|
|
||||||
# participant_id: str | None
|
|
||||||
|
|
||||||
# def __str__(self):
|
|
||||||
# return f"SendAppMessageFrame: participant: {self.participant_id}, message: {self.message}"
|
|
||||||
|
|
||||||
|
|
||||||
# class UserStartedSpeakingFrame(Frame):
|
|
||||||
# """Emitted by VAD to indicate that a participant has started speaking.
|
|
||||||
# This can be used for interruptions or other times when detecting that
|
|
||||||
# someone is speaking is more important than knowing what they're saying
|
|
||||||
# (as you will with a TranscriptionFrame)"""
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
# class UserStoppedSpeakingFrame(Frame):
|
|
||||||
# """Emitted by the VAD to indicate that a user stopped speaking."""
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
# class BotStartedSpeakingFrame(Frame):
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
# class BotStoppedSpeakingFrame(Frame):
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
# @dataclass()
|
|
||||||
# class LLMFunctionStartFrame(Frame):
|
|
||||||
# """Emitted when the LLM receives the beginning of a function call
|
|
||||||
# completion. A frame processor can use this frame to indicate that it should
|
|
||||||
# start preparing to make a function call, if it can do so in the absence of
|
|
||||||
# any arguments."""
|
|
||||||
# function_name: str
|
|
||||||
|
|
||||||
|
|
||||||
# @dataclass()
|
|
||||||
# class LLMFunctionCallFrame(Frame):
|
|
||||||
# """Emitted when the LLM has received an entire function call completion."""
|
|
||||||
# function_name: str
|
|
||||||
# arguments: str
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
from pipecat.frames.frames import Frame
|
|
||||||
|
|
||||||
|
|
||||||
class OpenAILLMContextFrame(Frame):
|
|
||||||
"""Like an LLMMessagesFrame, but with extra context specific to the
|
|
||||||
OpenAI API."""
|
|
||||||
|
|
||||||
def __init__(self, data):
|
|
||||||
super().__init__(data)
|
|
||||||
@@ -26,8 +26,8 @@ class LLMResponseAggregator(FrameProcessor):
|
|||||||
role: str,
|
role: str,
|
||||||
start_frame,
|
start_frame,
|
||||||
end_frame,
|
end_frame,
|
||||||
accumulator_frame,
|
accumulator_frame: TextFrame,
|
||||||
interim_accumulator_frame=None
|
interim_accumulator_frame: TextFrame | None = None
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ class LLMResponseAggregator(FrameProcessor):
|
|||||||
send_aggregation = not self._aggregating
|
send_aggregation = not self._aggregating
|
||||||
elif isinstance(frame, self._accumulator_frame):
|
elif isinstance(frame, self._accumulator_frame):
|
||||||
if self._aggregating:
|
if self._aggregating:
|
||||||
self._aggregation += f" {frame.data}"
|
self._aggregation += f" {frame.text}"
|
||||||
# We have recevied a complete sentence, so if we have seen the
|
# We have recevied a complete sentence, so if we have seen the
|
||||||
# end frame and we were still aggregating, it means we should
|
# end frame and we were still aggregating, it means we should
|
||||||
# send the aggregation.
|
# send the aggregation.
|
||||||
@@ -181,7 +181,7 @@ class LLMFullResponseAggregator(FrameProcessor):
|
|||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
if isinstance(frame, TextFrame):
|
if isinstance(frame, TextFrame):
|
||||||
self._aggregation += frame.data
|
self._aggregation += frame.text
|
||||||
elif isinstance(frame, LLMResponseEndFrame):
|
elif isinstance(frame, LLMResponseEndFrame):
|
||||||
await self.push_frame(TextFrame(self._aggregation))
|
await self.push_frame(TextFrame(self._aggregation))
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from typing import AsyncGenerator, Callable, List
|
from typing import AsyncGenerator, Callable, List
|
||||||
|
|
||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
@@ -15,7 +17,6 @@ from pipecat.frames.frames import (
|
|||||||
UserStartedSpeakingFrame,
|
UserStartedSpeakingFrame,
|
||||||
UserStoppedSpeakingFrame,
|
UserStoppedSpeakingFrame,
|
||||||
)
|
)
|
||||||
from pipecat.frames.openai_frames import OpenAILLMContextFrame
|
|
||||||
from pipecat.processors.frame_processor import FrameProcessor
|
from pipecat.processors.frame_processor import FrameProcessor
|
||||||
|
|
||||||
from openai._types import NOT_GIVEN, NotGiven
|
from openai._types import NOT_GIVEN, NotGiven
|
||||||
@@ -162,3 +163,13 @@ class OpenAIAssistantContextAggregator(OpenAIContextAggregator):
|
|||||||
accumulator_frame=TextFrame,
|
accumulator_frame=TextFrame,
|
||||||
pass_through=True,
|
pass_through=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OpenAILLMContextFrame(Frame):
|
||||||
|
"""Like an LLMMessagesFrame, but with extra context specific to the OpenAI
|
||||||
|
API. The context in this message is also mutable, and will be changed by the
|
||||||
|
OpenAIContextAggregator frame processor.
|
||||||
|
|
||||||
|
"""
|
||||||
|
context: OpenAILLMContext
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ class SentenceAggregator(FrameProcessor):
|
|||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
if isinstance(frame, TextFrame):
|
if isinstance(frame, TextFrame):
|
||||||
m = re.search("(.*[?.!])(.*)", frame.data)
|
m = re.search("(.*[?.!])(.*)", frame.text)
|
||||||
if m:
|
if m:
|
||||||
await self.push_frame(TextFrame(self._aggregation + m.group(1)))
|
await self.push_frame(TextFrame(self._aggregation + m.group(1)))
|
||||||
self._aggregation = m.group(2)
|
self._aggregation = m.group(2)
|
||||||
else:
|
else:
|
||||||
self._aggregation += frame.data
|
self._aggregation += frame.text
|
||||||
elif isinstance(frame, EndFrame):
|
elif isinstance(frame, EndFrame):
|
||||||
if self._aggregation:
|
if self._aggregation:
|
||||||
await self.push_frame(TextFrame(self._aggregation))
|
await self.push_frame(TextFrame(self._aggregation))
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ class ResponseAggregator(FrameProcessor):
|
|||||||
*,
|
*,
|
||||||
start_frame,
|
start_frame,
|
||||||
end_frame,
|
end_frame,
|
||||||
accumulator_frame,
|
accumulator_frame: TextFrame,
|
||||||
interim_accumulator_frame=None
|
interim_accumulator_frame: TextFrame | None = None
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ class ResponseAggregator(FrameProcessor):
|
|||||||
send_aggregation = not self._aggregating
|
send_aggregation = not self._aggregating
|
||||||
elif isinstance(frame, self._accumulator_frame):
|
elif isinstance(frame, self._accumulator_frame):
|
||||||
if self._aggregating:
|
if self._aggregating:
|
||||||
self._aggregation += f" {frame.data}"
|
self._aggregation += f" {frame.text}"
|
||||||
# We have recevied a complete sentence, so if we have seen the
|
# We have recevied a complete sentence, so if we have seen the
|
||||||
# end frame and we were still aggregating, it means we should
|
# end frame and we were still aggregating, it means we should
|
||||||
# send the aggregation.
|
# send the aggregation.
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ class VisionImageFrameAggregator(FrameProcessor):
|
|||||||
elif isinstance(frame, ImageRawFrame):
|
elif isinstance(frame, ImageRawFrame):
|
||||||
if self._describe_text:
|
if self._describe_text:
|
||||||
frame = VisionImageRawFrame(
|
frame = VisionImageRawFrame(
|
||||||
self._describe_text, frame.image, frame.size, frame.format)
|
text=self._describe_text,
|
||||||
|
image=frame.image,
|
||||||
|
size=frame.size,
|
||||||
|
format=frame.format)
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
self._describe_text = None
|
self._describe_text = None
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class StatelessTextTransformer(FrameProcessor):
|
|||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
if isinstance(frame, TextFrame):
|
if isinstance(frame, TextFrame):
|
||||||
result = self._transform_fn(frame.data)
|
result = self._transform_fn(frame.text)
|
||||||
if isinstance(result, Coroutine):
|
if isinstance(result, Coroutine):
|
||||||
result = await result
|
result = await result
|
||||||
await self.push_frame(result)
|
await self.push_frame(result)
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ from pipecat.frames.frames import AudioRawFrame
|
|||||||
def maybe_split_audio_frame(frame: AudioRawFrame, largest_write_size: int) -> List[AudioRawFrame]:
|
def maybe_split_audio_frame(frame: AudioRawFrame, largest_write_size: int) -> List[AudioRawFrame]:
|
||||||
"""Subdivide large audio frames to enable interruption."""
|
"""Subdivide large audio frames to enable interruption."""
|
||||||
frames: List[AudioRawFrame] = []
|
frames: List[AudioRawFrame] = []
|
||||||
if len(frame.data) > largest_write_size:
|
if len(frame.audio) > largest_write_size:
|
||||||
for i in range(0, len(frame.data), largest_write_size):
|
for i in range(0, len(frame.audio), largest_write_size):
|
||||||
chunk = frame.data[i: i + largest_write_size]
|
chunk = frame.audio[i: i + largest_write_size]
|
||||||
frames.append(AudioRawFrame(chunk, frame.sample_rate, frame.num_channels))
|
frames.append(
|
||||||
|
AudioRawFrame(
|
||||||
|
audio=chunk,
|
||||||
|
sample_rate=frame.sample_rate,
|
||||||
|
num_channels=frame.num_channels))
|
||||||
else:
|
else:
|
||||||
frames.append(frame)
|
frames.append(frame)
|
||||||
return frames
|
return frames
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ class TTSService(AIService):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
async def say(self, text: str):
|
async def say(self, text: str):
|
||||||
await self.process_frame(TextFrame(text), FrameDirection.DOWNSTREAM)
|
await self.process_frame(TextFrame(text=text), FrameDirection.DOWNSTREAM)
|
||||||
|
|
||||||
async def _process_text_frame(self, frame: TextFrame):
|
async def _process_text_frame(self, frame: TextFrame):
|
||||||
text: str | None = None
|
text: str | None = None
|
||||||
if not self._aggregate_sentences:
|
if not self._aggregate_sentences:
|
||||||
text = frame.data
|
text = frame.text
|
||||||
else:
|
else:
|
||||||
self._current_sentence += frame.data
|
self._current_sentence += frame.text
|
||||||
if self._current_sentence.strip().endswith((".", "?", "!")):
|
if self._current_sentence.strip().endswith((".", "?", "!")):
|
||||||
text = self._current_sentence
|
text = self._current_sentence
|
||||||
self._current_sentence = ""
|
self._current_sentence = ""
|
||||||
@@ -78,11 +78,13 @@ class STTService(AIService):
|
|||||||
def __init__(self,
|
def __init__(self,
|
||||||
min_rms: int = 400,
|
min_rms: int = 400,
|
||||||
max_silence_frames: int = 3,
|
max_silence_frames: int = 3,
|
||||||
sample_rate: int = 16000):
|
sample_rate: int = 16000,
|
||||||
|
num_channels: int = 1):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._min_rms = min_rms
|
self._min_rms = min_rms
|
||||||
self._max_silence_frames = max_silence_frames
|
self._max_silence_frames = max_silence_frames
|
||||||
self._sample_rate = sample_rate
|
self._sample_rate = sample_rate
|
||||||
|
self._num_channels = num_channels
|
||||||
self._current_silence_frames = 0
|
self._current_silence_frames = 0
|
||||||
(self._content, self._wave) = self._new_wave()
|
(self._content, self._wave) = self._new_wave()
|
||||||
|
|
||||||
@@ -94,8 +96,8 @@ class STTService(AIService):
|
|||||||
def _new_wave(self):
|
def _new_wave(self):
|
||||||
content = io.BufferedRandom(io.BytesIO())
|
content = io.BufferedRandom(io.BytesIO())
|
||||||
ww = wave.open(content, "wb")
|
ww = wave.open(content, "wb")
|
||||||
ww.setnchannels(1)
|
|
||||||
ww.setsampwidth(2)
|
ww.setsampwidth(2)
|
||||||
|
ww.setnchannels(self._num_channels)
|
||||||
ww.setframerate(self._sample_rate)
|
ww.setframerate(self._sample_rate)
|
||||||
return (content, ww)
|
return (content, ww)
|
||||||
|
|
||||||
@@ -113,14 +115,14 @@ class STTService(AIService):
|
|||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
return
|
return
|
||||||
|
|
||||||
data = frame.data
|
audio = frame.audio
|
||||||
|
|
||||||
# Try to filter out empty background noise
|
# Try to filter out empty background noise
|
||||||
# (Very rudimentary approach, can be improved)
|
# (Very rudimentary approach, can be improved)
|
||||||
rms = self._get_volume(data)
|
rms = self._get_volume(audio)
|
||||||
if rms >= self._min_rms:
|
if rms >= self._min_rms:
|
||||||
# If volume is high enough, write new data to wave file
|
# If volume is high enough, write new data to wave file
|
||||||
self._wave.writeframesraw(data)
|
self._wave.writeframes(audio)
|
||||||
|
|
||||||
# If buffer is not empty and we detect a 3-frame pause in speech,
|
# If buffer is not empty and we detect a 3-frame pause in speech,
|
||||||
# transcribe the audio gathered so far.
|
# transcribe the audio gathered so far.
|
||||||
@@ -146,7 +148,7 @@ class ImageGenService(AIService):
|
|||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
if isinstance(frame, TextFrame):
|
if isinstance(frame, TextFrame):
|
||||||
await self.run_image_gen(frame.data)
|
await self.run_image_gen(frame.text)
|
||||||
else:
|
else:
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
|
|||||||
@@ -32,5 +32,5 @@ class DeepgramTTSService(TTSService):
|
|||||||
body = {"text": text}
|
body = {"text": text}
|
||||||
async with self._aiohttp_session.post(request_url, headers=headers, json=body) as r:
|
async with self._aiohttp_session.post(request_url, headers=headers, json=body) as r:
|
||||||
async for data in r.content:
|
async for data in r.content:
|
||||||
frame = AudioRawFrame(data, 16000, 1)
|
frame = AudioRawFrame(audio=data, sample_rate=16000, num_channels=1)
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|||||||
@@ -75,5 +75,9 @@ class FalImageGenService(ImageGenService):
|
|||||||
image_stream = io.BytesIO(await response.content.read())
|
image_stream = io.BytesIO(await response.content.read())
|
||||||
image = Image.open(image_stream)
|
image = Image.open(image_stream)
|
||||||
|
|
||||||
frame = URLImageRawFrame(image_url, image.tobytes(), image.size, image.format)
|
frame = URLImageRawFrame(
|
||||||
|
url=image_url,
|
||||||
|
image=image.tobytes(),
|
||||||
|
size=image.size,
|
||||||
|
format=image.format)
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class MoondreamService(VisionService):
|
|||||||
logger.debug(f"Analyzing image: {frame}")
|
logger.debug(f"Analyzing image: {frame}")
|
||||||
|
|
||||||
def get_image_description(frame: VisionImageRawFrame):
|
def get_image_description(frame: VisionImageRawFrame):
|
||||||
image = Image.frombytes(frame.format, (frame.size[0], frame.size[1]), frame.data)
|
image = Image.frombytes(frame.format, (frame.size[0], frame.size[1]), frame.image)
|
||||||
image_embeds = self._model.encode_image(image)
|
image_embeds = self._model.encode_image(image)
|
||||||
description = self._model.answer_question(
|
description = self._model.answer_question(
|
||||||
image_embeds=image_embeds,
|
image_embeds=image_embeds,
|
||||||
@@ -79,4 +79,4 @@ class MoondreamService(VisionService):
|
|||||||
|
|
||||||
description = await asyncio.to_thread(get_image_description, frame)
|
description = await asyncio.to_thread(get_image_description, frame)
|
||||||
|
|
||||||
await self.push_frame(TextFrame(description))
|
await self.push_frame(TextFrame(text=description))
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ from pipecat.frames.frames import (
|
|||||||
TextFrame,
|
TextFrame,
|
||||||
URLImageRawFrame
|
URLImageRawFrame
|
||||||
)
|
)
|
||||||
from pipecat.frames.openai_frames import OpenAILLMContextFrame
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext, OpenAILLMContextFrame
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
|
||||||
from pipecat.processors.frame_processor import FrameDirection
|
from pipecat.processors.frame_processor import FrameDirection
|
||||||
from pipecat.services.ai_services import LLMService, ImageGenService
|
from pipecat.services.ai_services import LLMService, ImageGenService
|
||||||
|
|
||||||
@@ -137,9 +136,9 @@ class BaseOpenAILLMService(LLMService):
|
|||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
context = None
|
context = None
|
||||||
if isinstance(frame, OpenAILLMContextFrame):
|
if isinstance(frame, OpenAILLMContextFrame):
|
||||||
context: OpenAILLMContext = frame.data
|
context: OpenAILLMContext = frame.context
|
||||||
elif isinstance(frame, LLMMessagesFrame):
|
elif isinstance(frame, LLMMessagesFrame):
|
||||||
context = OpenAILLMContext.from_messages(frame.data)
|
context = OpenAILLMContext.from_messages(frame.messages)
|
||||||
else:
|
else:
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
|
|||||||
@@ -54,11 +54,10 @@ class WhisperSTTService(STTService):
|
|||||||
"""Loads the Whisper model. Note that if this is the first time
|
"""Loads the Whisper model. Note that if this is the first time
|
||||||
this model is being run, it will take time to download."""
|
this model is being run, it will take time to download."""
|
||||||
logger.debug("Loading Whisper model...")
|
logger.debug("Loading Whisper model...")
|
||||||
model = WhisperModel(
|
self._model = WhisperModel(
|
||||||
self._model_name.value,
|
self._model_name.value,
|
||||||
device=self._device,
|
device=self._device,
|
||||||
compute_type=self._compute_type)
|
compute_type=self._compute_type)
|
||||||
self._model = model
|
|
||||||
logger.debug("Loaded Whisper model")
|
logger.debug("Loaded Whisper model")
|
||||||
|
|
||||||
async def run_stt(self, audio: BinaryIO):
|
async def run_stt(self, audio: BinaryIO):
|
||||||
|
|||||||
@@ -108,7 +108,10 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
try:
|
try:
|
||||||
audio_frames = self.read_raw_audio_frames(num_frames)
|
audio_frames = self.read_raw_audio_frames(num_frames)
|
||||||
if len(audio_frames) > 0:
|
if len(audio_frames) > 0:
|
||||||
frame = AudioRawFrame(audio_frames, sample_rate, num_channels)
|
frame = AudioRawFrame(
|
||||||
|
audio=audio_frames,
|
||||||
|
sample_rate=sample_rate,
|
||||||
|
num_channels=num_channels)
|
||||||
self._audio_in_queue.put(frame)
|
self._audio_in_queue.put(frame)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
logger.error(f"Error reading audio frames: {e}")
|
logger.error(f"Error reading audio frames: {e}")
|
||||||
@@ -124,7 +127,7 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
# Check VAD and push event if necessary. We just care about changes
|
# Check VAD and push event if necessary. We just care about changes
|
||||||
# from QUIET to SPEAKING and vice versa.
|
# from QUIET to SPEAKING and vice versa.
|
||||||
if self._params.vad_enabled:
|
if self._params.vad_enabled:
|
||||||
vad_state = self._handle_vad(frame.data, vad_state)
|
vad_state = self._handle_vad(frame.audio, vad_state)
|
||||||
audio_passthrough = self._params.vad_audio_passthrough
|
audio_passthrough = self._params.vad_audio_passthrough
|
||||||
|
|
||||||
# Push audio downstream if passthrough.
|
# Push audio downstream if passthrough.
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
future.result()
|
future.result()
|
||||||
elif isinstance(frame, AudioRawFrame):
|
elif isinstance(frame, AudioRawFrame):
|
||||||
if self._params.audio_out_enabled:
|
if self._params.audio_out_enabled:
|
||||||
buffer.extend(frame.data)
|
buffer.extend(frame.audio)
|
||||||
buffer = self._send_audio_truncated(buffer, bytes_size_10ms)
|
buffer = self._send_audio_truncated(buffer, bytes_size_10ms)
|
||||||
elif isinstance(frame, ImageRawFrame) and self._params.camera_out_enabled:
|
elif isinstance(frame, ImageRawFrame) and self._params.camera_out_enabled:
|
||||||
self._set_camera_image(frame)
|
self._set_camera_image(frame)
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class TkOutputTransport(BaseOutputTransport):
|
|||||||
async def _write_frame_to_tk(self, frame: ImageRawFrame):
|
async def _write_frame_to_tk(self, frame: ImageRawFrame):
|
||||||
width = frame.size[0]
|
width = frame.size[0]
|
||||||
height = frame.size[1]
|
height = frame.size[1]
|
||||||
data = f"P6 {width} {height} 255 ".encode() + frame.data
|
data = f"P6 {width} {height} 255 ".encode() + frame.image
|
||||||
photo = tk.PhotoImage(
|
photo = tk.PhotoImage(
|
||||||
width=width,
|
width=width,
|
||||||
height=height,
|
height=height,
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ class DailySession(EventHandler):
|
|||||||
self._mic.write_frames(frames)
|
self._mic.write_frames(frames)
|
||||||
|
|
||||||
def write_frame_to_camera(self, frame: ImageRawFrame):
|
def write_frame_to_camera(self, frame: ImageRawFrame):
|
||||||
self._camera.write_frame(frame.data)
|
self._camera.write_frame(frame.image)
|
||||||
|
|
||||||
async def join(self):
|
async def join(self):
|
||||||
# Transport already joined, ignore.
|
# Transport already joined, ignore.
|
||||||
@@ -498,7 +498,11 @@ class DailyInputTransport(BaseInputTransport):
|
|||||||
render_frame = True
|
render_frame = True
|
||||||
|
|
||||||
if render_frame:
|
if render_frame:
|
||||||
frame = UserImageRawFrame(participant_id, buffer, size, format)
|
frame = UserImageRawFrame(
|
||||||
|
user_id=participant_id,
|
||||||
|
image=buffer,
|
||||||
|
size=size,
|
||||||
|
format=format)
|
||||||
self._camera_in_queue.put(frame)
|
self._camera_in_queue.put(frame)
|
||||||
|
|
||||||
self._video_renderers[participant_id]["timestamp"] = curr_time
|
self._video_renderers[participant_id]["timestamp"] = curr_time
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class SileroVAD(FrameProcessor, VADAnalyzer):
|
|||||||
async def _analyze_audio(self, frame: AudioRawFrame):
|
async def _analyze_audio(self, frame: AudioRawFrame):
|
||||||
# Check VAD and push event if necessary. We just care about changes
|
# Check VAD and push event if necessary. We just care about changes
|
||||||
# from QUIET to SPEAKING and vice versa.
|
# from QUIET to SPEAKING and vice versa.
|
||||||
new_vad_state = self.analyze_audio(frame.data)
|
new_vad_state = self.analyze_audio(frame.audio)
|
||||||
if new_vad_state != self._processor_vad_state and new_vad_state != VADState.STARTING and new_vad_state != VADState.STOPPING:
|
if new_vad_state != self._processor_vad_state and new_vad_state != VADState.STARTING and new_vad_state != VADState.STOPPING:
|
||||||
new_frame = None
|
new_frame = None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user