Merge pull request #2863 from pipecat-ai/vp-custom-frame-processor-ex
add 08-custom-frame-processor.py to foundational examples
This commit is contained in:
@@ -1,147 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
from pipecat.frames.frames import AudioFrame, EndFrame, ImageFrame, LLMContextFrame, TextFrame
|
|
||||||
from pipecat.pipeline.pipeline import Pipeline
|
|
||||||
from pipecat.processors.aggregators import SentenceAggregator
|
|
||||||
from pipecat.processors.aggregators.llm_context import LLMContext
|
|
||||||
from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair
|
|
||||||
from pipecat.runner.daily import configure
|
|
||||||
from pipecat.services.azure import AzureLLMService, AzureTTSService
|
|
||||||
from pipecat.services.elevenlabs import ElevenLabsTTSService
|
|
||||||
from pipecat.services.fal import FalImageGenService
|
|
||||||
from pipecat.transports.daily.transport import DailyTransport
|
|
||||||
|
|
||||||
load_dotenv(override=True)
|
|
||||||
|
|
||||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
|
||||||
logger = logging.getLogger("pipecat")
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
(room_url, _) = await configure(session)
|
|
||||||
|
|
||||||
transport = DailyTransport(
|
|
||||||
room_url,
|
|
||||||
None,
|
|
||||||
"Respond bot",
|
|
||||||
duration_minutes=10,
|
|
||||||
mic_enabled=True,
|
|
||||||
mic_sample_rate=16000,
|
|
||||||
camera_enabled=True,
|
|
||||||
camera_width=1024,
|
|
||||||
camera_height=1024,
|
|
||||||
)
|
|
||||||
|
|
||||||
llm = AzureLLMService(
|
|
||||||
api_key=os.getenv("AZURE_CHATGPT_API_KEY"),
|
|
||||||
endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"),
|
|
||||||
model=os.getenv("AZURE_CHATGPT_MODEL"),
|
|
||||||
)
|
|
||||||
tts1 = AzureTTSService(
|
|
||||||
api_key=os.getenv("AZURE_SPEECH_API_KEY"),
|
|
||||||
region=os.getenv("AZURE_SPEECH_REGION"),
|
|
||||||
)
|
|
||||||
tts2 = ElevenLabsTTSService(
|
|
||||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
|
||||||
voice_id="jBpfuIE2acCO8z3wKNLl",
|
|
||||||
)
|
|
||||||
dalle = FalImageGenService(
|
|
||||||
params=FalImageGenService.InputParams(image_size="1024x1024"),
|
|
||||||
aiohttp_session=session,
|
|
||||||
key=os.getenv("FAL_KEY"),
|
|
||||||
)
|
|
||||||
|
|
||||||
bot1_messages = [
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": "You are a stern librarian. You strongly believe that a hot dog is a sandwich. Start by stating this fact in a few sentences, then be prepared to debate this with the user. You shouldn't ever compromise on the fundamental truth that a hot dog is a sandwich. Your responses should only be a few sentences long.",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
bot2_messages = [
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": "You are a silly cat, and you strongly believe that a hot dog is not a sandwich. Debate this with the user, only responding with a few sentences. Don't ever accept that a hot dog is a sandwich.",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_text_and_audio(messages) -> Tuple[str, bytearray]:
|
|
||||||
"""This function streams text from the LLM and uses the TTS service to convert
|
|
||||||
that text to speech as it's received.
|
|
||||||
"""
|
|
||||||
source_queue = asyncio.Queue()
|
|
||||||
sink_queue = asyncio.Queue()
|
|
||||||
sentence_aggregator = SentenceAggregator()
|
|
||||||
pipeline = Pipeline([llm, sentence_aggregator, tts1], source_queue, sink_queue)
|
|
||||||
|
|
||||||
await source_queue.put(LLMContextFrame(LLMContext(messages)))
|
|
||||||
await source_queue.put(EndFrame())
|
|
||||||
await pipeline.run_pipeline()
|
|
||||||
|
|
||||||
message = ""
|
|
||||||
all_audio = bytearray()
|
|
||||||
while sink_queue.qsize():
|
|
||||||
frame = sink_queue.get_nowait()
|
|
||||||
if isinstance(frame, TextFrame):
|
|
||||||
message += frame.text
|
|
||||||
elif isinstance(frame, AudioFrame):
|
|
||||||
all_audio.extend(frame.audio)
|
|
||||||
|
|
||||||
return (message, all_audio)
|
|
||||||
|
|
||||||
async def get_bot1_statement():
|
|
||||||
message, audio = await get_text_and_audio(bot1_messages)
|
|
||||||
|
|
||||||
bot1_messages.append({"role": "assistant", "content": message})
|
|
||||||
bot2_messages.append({"role": "user", "content": message})
|
|
||||||
|
|
||||||
return audio
|
|
||||||
|
|
||||||
async def get_bot2_statement():
|
|
||||||
message, audio = await get_text_and_audio(bot2_messages)
|
|
||||||
|
|
||||||
bot2_messages.append({"role": "assistant", "content": message})
|
|
||||||
bot1_messages.append({"role": "user", "content": message})
|
|
||||||
|
|
||||||
return audio
|
|
||||||
|
|
||||||
async def argue():
|
|
||||||
for i in range(100):
|
|
||||||
print(f"In iteration {i}")
|
|
||||||
|
|
||||||
bot1_description = "A woman conservatively dressed as a librarian in a library surrounded by books, cartoon, serious, highly detailed"
|
|
||||||
|
|
||||||
(audio1, image_data1) = await asyncio.gather(
|
|
||||||
get_bot1_statement(), dalle.run_image_gen(bot1_description)
|
|
||||||
)
|
|
||||||
await transport.send_queue.put(
|
|
||||||
[
|
|
||||||
ImageFrame(image_data1[1], image_data1[2]),
|
|
||||||
AudioFrame(audio1),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
bot2_description = "A cat dressed in a hot dog costume, cartoon, bright colors, funny, highly detailed"
|
|
||||||
|
|
||||||
(audio2, image_data2) = await asyncio.gather(
|
|
||||||
get_bot2_statement(), dalle.run_image_gen(bot2_description)
|
|
||||||
)
|
|
||||||
await transport.send_queue.put(
|
|
||||||
[
|
|
||||||
ImageFrame(image_data2[1], image_data2[2]),
|
|
||||||
AudioFrame(audio2),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
await asyncio.gather(transport.run(), argue())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
170
examples/foundational/08-custom-frame-processor.py
Normal file
170
examples/foundational/08-custom-frame-processor.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams
|
||||||
|
from pipecat.audio.turn.smart_turn.local_smart_turn_v3 import LocalSmartTurnAnalyzerV3
|
||||||
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
|
from pipecat.audio.vad.vad_analyzer import VADParams
|
||||||
|
from pipecat.frames.frames import (
|
||||||
|
Frame,
|
||||||
|
LLMRunFrame,
|
||||||
|
MetricsFrame,
|
||||||
|
)
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
|
from pipecat.processors.aggregators.llm_context import LLMContext
|
||||||
|
from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair
|
||||||
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
|
from pipecat.runner.types import RunnerArguments
|
||||||
|
from pipecat.runner.utils import create_transport
|
||||||
|
from pipecat.services.cartesia.tts import CartesiaTTSService
|
||||||
|
from pipecat.services.deepgram.stt import DeepgramSTTService
|
||||||
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
|
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
||||||
|
from pipecat.transports.daily.transport import DailyParams
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
|
||||||
|
def format_metrics(metrics, indent=0):
|
||||||
|
lines = []
|
||||||
|
tab = "\t" * indent
|
||||||
|
|
||||||
|
for metric in metrics:
|
||||||
|
lines.append(tab + type(metric).__name__)
|
||||||
|
for field, value in vars(metric).items():
|
||||||
|
if hasattr(value, "__dict__") and not isinstance(
|
||||||
|
value, (str, int, float, bool, type(None))
|
||||||
|
):
|
||||||
|
lines.append(f"{tab}\t{field}={type(value).__name__}")
|
||||||
|
for k, v in vars(value).items():
|
||||||
|
lines.append(f"{tab}\t\t{k}={repr(v)}")
|
||||||
|
else:
|
||||||
|
lines.append(f"{tab}\t{field}={repr(value)}")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsFrameLogger(FrameProcessor):
|
||||||
|
"""MetricsFrameLogger formats and logs all MetericsFrames"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
|
if isinstance(frame, MetricsFrame):
|
||||||
|
logger.info(f"{frame.name}\n {format_metrics(frame.data)}")
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
|
# ALWAYS push all frames
|
||||||
|
else:
|
||||||
|
# SUPER IMPORTANT: always push every frame!
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
|
|
||||||
|
# We store functions so objects (e.g. SileroVADAnalyzer) don't get
|
||||||
|
# instantiated. The function will be called when the desired transport gets
|
||||||
|
# selected.
|
||||||
|
transport_params = {
|
||||||
|
"daily": lambda: DailyParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.2)),
|
||||||
|
turn_analyzer=LocalSmartTurnAnalyzerV3(params=SmartTurnParams()),
|
||||||
|
),
|
||||||
|
"webrtc": lambda: TransportParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
video_out_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.2)),
|
||||||
|
turn_analyzer=LocalSmartTurnAnalyzerV3(params=SmartTurnParams()),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
||||||
|
logger.info(f"Starting bot")
|
||||||
|
|
||||||
|
stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"))
|
||||||
|
|
||||||
|
tts = CartesiaTTSService(
|
||||||
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
|
voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady
|
||||||
|
)
|
||||||
|
|
||||||
|
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio so don't include special characters in your answers. Respond to what the user said in a creative and helpful way.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
context = LLMContext(messages)
|
||||||
|
context_aggregator = LLMContextAggregatorPair(context)
|
||||||
|
|
||||||
|
metrics_frame_processor = MetricsFrameLogger()
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(),
|
||||||
|
stt,
|
||||||
|
context_aggregator.user(),
|
||||||
|
llm,
|
||||||
|
tts,
|
||||||
|
transport.output(),
|
||||||
|
context_aggregator.assistant(),
|
||||||
|
metrics_frame_processor, # pretty print metrics frames
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
enable_metrics=True,
|
||||||
|
enable_usage_metrics=True,
|
||||||
|
),
|
||||||
|
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
|
||||||
|
)
|
||||||
|
|
||||||
|
@transport.event_handler("on_client_connected")
|
||||||
|
async def on_client_connected(transport, client):
|
||||||
|
logger.info(f"Client connected: {client}")
|
||||||
|
# Kick off the conversation.
|
||||||
|
messages.append({"role": "system", "content": "Please introduce yourself to the user."})
|
||||||
|
await task.queue_frames([LLMRunFrame()])
|
||||||
|
|
||||||
|
@transport.event_handler("on_client_disconnected")
|
||||||
|
async def on_client_disconnected(transport, client):
|
||||||
|
logger.info(f"Client disconnected")
|
||||||
|
await task.cancel()
|
||||||
|
|
||||||
|
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
|
|
||||||
|
|
||||||
|
async def bot(runner_args: RunnerArguments):
|
||||||
|
"""Main bot entry point compatible with Pipecat Cloud."""
|
||||||
|
transport = await create_transport(runner_args, transport_params)
|
||||||
|
await run_bot(transport, runner_args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from pipecat.runner.run import main
|
||||||
|
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user