examples: fix storytelling example
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
ELEVENLABS_API_KEY=
|
DAILY_API_KEY=7df...
|
||||||
ELEVENLABS_VOICE_ID=
|
ELEVENLABS_API_KEY=aeb...
|
||||||
FAL_KEY=
|
ELEVENLABS_VOICE_ID=7S...
|
||||||
DAILY_API_URL=api.daily.co/v1
|
FAL_KEY=8c...
|
||||||
DAILY_API_KEY=
|
OPENAI_API_KEY=sk-PL...
|
||||||
OPENAI_API_KEY=
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
dailyai[daily,openai,fal]==0.0.8
|
|
||||||
fastapi
|
fastapi
|
||||||
uvicorn
|
uvicorn
|
||||||
requests
|
requests
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
pipecat-ai[daily,openai,fal]
|
||||||
|
|||||||
@@ -1,37 +1,32 @@
|
|||||||
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import argparse
|
import sys
|
||||||
|
|
||||||
from dailyai.pipeline.pipeline import Pipeline
|
|
||||||
from dailyai.pipeline.frames import (
|
|
||||||
AudioFrame,
|
|
||||||
ImageFrame,
|
|
||||||
EndPipeFrame,
|
|
||||||
LLMMessagesFrame,
|
|
||||||
SendAppMessageFrame
|
|
||||||
)
|
|
||||||
from dailyai.pipeline.aggregators import (
|
|
||||||
LLMUserResponseAggregator,
|
|
||||||
LLMAssistantResponseAggregator,
|
|
||||||
)
|
|
||||||
from dailyai.transports.daily_transport import DailyTransport
|
|
||||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
|
||||||
from dailyai.services.open_ai_services import OpenAILLMService
|
|
||||||
from dailyai.services.fal_ai_services import FalImageGenService
|
|
||||||
|
|
||||||
|
from pipecat.frames.frames import LLMMessagesFrame, StopTaskFrame
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineTask
|
||||||
|
from pipecat.processors.aggregators.llm_response import LLMAssistantResponseAggregator, LLMUserResponseAggregator
|
||||||
|
from pipecat.services.elevenlabs import ElevenLabsTTSService
|
||||||
|
from pipecat.services.fal import FalImageGenService
|
||||||
|
from pipecat.services.openai import OpenAILLMService
|
||||||
|
from pipecat.transports.services.daily import DailyParams, DailyTransport, DailyTransportMessageFrame
|
||||||
|
|
||||||
|
from pipecat.vad.silero import SileroVAD
|
||||||
from processors import StoryProcessor, StoryImageProcessor
|
from processors import StoryProcessor, StoryImageProcessor
|
||||||
from prompts import LLM_BASE_PROMPT, LLM_INTRO_PROMPT, CUE_USER_TURN
|
from prompts import LLM_BASE_PROMPT, LLM_INTRO_PROMPT, CUE_USER_TURN
|
||||||
from utils.helpers import load_sounds, load_images
|
from utils.helpers import load_sounds, load_images
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
load_dotenv(override=True)
|
load_dotenv(override=True)
|
||||||
|
|
||||||
logging.basicConfig(format=f"[STORYBOT] %(levelno)s %(asctime)s %(message)s")
|
logger.remove(0)
|
||||||
logger = logging.getLogger("dailyai")
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
|
|
||||||
sounds = load_sounds(["listening.wav"])
|
sounds = load_sounds(["listening.wav"])
|
||||||
images = load_images(["book1.png", "book2.png"])
|
images = load_images(["book1.png", "book2.png"])
|
||||||
@@ -46,22 +41,23 @@ async def main(room_url, token=None):
|
|||||||
room_url,
|
room_url,
|
||||||
token,
|
token,
|
||||||
"Storytelling Bot",
|
"Storytelling Bot",
|
||||||
duration_minutes=5,
|
DailyParams(
|
||||||
start_transcription=True,
|
audio_in_enabled=True,
|
||||||
mic_enabled=True,
|
audio_out_enabled=True,
|
||||||
mic_sample_rate=16000,
|
camera_out_enabled=True,
|
||||||
vad_enabled=True,
|
camera_out_width=768,
|
||||||
camera_framerate=30,
|
camera_out_height=768,
|
||||||
camera_bitrate=680000,
|
transcription_enabled=True,
|
||||||
camera_enabled=True,
|
vad_enabled=True,
|
||||||
camera_width=768,
|
)
|
||||||
camera_height=768,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("Transport created for room:" + room_url)
|
logger.debug("Transport created for room:" + room_url)
|
||||||
|
|
||||||
# -------------- Services --------------- #
|
# -------------- Services --------------- #
|
||||||
|
|
||||||
|
# vad = SileroVAD()
|
||||||
|
|
||||||
llm_service = OpenAILLMService(
|
llm_service = OpenAILLMService(
|
||||||
api_key=os.getenv("OPENAI_API_KEY"),
|
api_key=os.getenv("OPENAI_API_KEY"),
|
||||||
model="gpt-4-turbo"
|
model="gpt-4-turbo"
|
||||||
@@ -103,68 +99,55 @@ async def main(room_url, token=None):
|
|||||||
|
|
||||||
# -------------- Story Loop ------------- #
|
# -------------- Story Loop ------------- #
|
||||||
|
|
||||||
|
runner = PipelineRunner()
|
||||||
|
|
||||||
|
# The intro pipeline is used to start
|
||||||
|
# the story (as per LLM_INTRO_PROMPT)
|
||||||
|
intro_pipeline = Pipeline([llm_service, tts_service, transport.output()])
|
||||||
|
|
||||||
|
intro_task = PipelineTask(intro_pipeline)
|
||||||
|
|
||||||
logger.debug("Waiting for participant...")
|
logger.debug("Waiting for participant...")
|
||||||
|
|
||||||
start_storytime_event = asyncio.Event()
|
@transport.event_handler("on_first_participant_joined")
|
||||||
|
async def on_first_participant_joined(transport, participant):
|
||||||
@transport.event_handler("on_first_other_participant_joined")
|
|
||||||
async def on_first_other_participant_joined(transport, participant):
|
|
||||||
logger.debug("Participant joined, storytime commence!")
|
logger.debug("Participant joined, storytime commence!")
|
||||||
start_storytime_event.set()
|
transport.capture_participant_transcription(participant["id"])
|
||||||
|
await intro_task.queue_frames(
|
||||||
# The storytime coroutine will wait for the start_storytime_event
|
|
||||||
# to be set before starting the storytime pipeline
|
|
||||||
async def storytime():
|
|
||||||
await start_storytime_event.wait()
|
|
||||||
|
|
||||||
# The intro pipeline is used to start
|
|
||||||
# the story (as per LLM_INTRO_PROMPT)
|
|
||||||
intro_pipeline = Pipeline(processors=[
|
|
||||||
llm_service,
|
|
||||||
tts_service,
|
|
||||||
], sink=transport.send_queue)
|
|
||||||
|
|
||||||
await intro_pipeline.queue_frames(
|
|
||||||
[
|
[
|
||||||
ImageFrame(images['book1'], (768, 768)),
|
images['book1'],
|
||||||
LLMMessagesFrame([LLM_INTRO_PROMPT]),
|
LLMMessagesFrame([LLM_INTRO_PROMPT]),
|
||||||
SendAppMessageFrame(CUE_USER_TURN, None),
|
DailyTransportMessageFrame(CUE_USER_TURN),
|
||||||
AudioFrame(sounds["listening"]),
|
sounds["listening"],
|
||||||
ImageFrame(images['book2'], (768, 768)),
|
images['book2'],
|
||||||
EndPipeFrame(),
|
StopTaskFrame()
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
# We start the pipeline as soon as the user joins
|
# We run the intro pipeline. This will start the transport. The intro
|
||||||
await intro_pipeline.run_pipeline()
|
# task will exit after StopTaskFrame is processed.
|
||||||
|
await runner.run(intro_task)
|
||||||
|
|
||||||
# The main story pipeline is used to continue the
|
# The main story pipeline is used to continue the story based on user
|
||||||
# story based on user input
|
# input.
|
||||||
pipeline = Pipeline(processors=[
|
main_pipeline = Pipeline([
|
||||||
user_responses,
|
transport.input(),
|
||||||
llm_service,
|
# vad,
|
||||||
story_processor,
|
user_responses,
|
||||||
image_processor,
|
llm_service,
|
||||||
tts_service,
|
story_processor,
|
||||||
llm_responses,
|
image_processor,
|
||||||
])
|
tts_service,
|
||||||
|
llm_responses,
|
||||||
|
transport.output()
|
||||||
|
])
|
||||||
|
|
||||||
await transport.run_pipeline(pipeline)
|
main_task = PipelineTask(main_pipeline)
|
||||||
|
|
||||||
transport.transcription_settings["extra"]["endpointing"] = True
|
|
||||||
transport.transcription_settings["extra"]["punctuate"] = True
|
|
||||||
|
|
||||||
try:
|
|
||||||
await asyncio.gather(transport.run(), storytime())
|
|
||||||
except (asyncio.CancelledError, KeyboardInterrupt):
|
|
||||||
transport.stop()
|
|
||||||
|
|
||||||
logger.debug("Pipeline finished. Exiting.")
|
|
||||||
|
|
||||||
|
await runner.run(main_task)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(description="Daily Storyteller Bot")
|
||||||
description="Daily Storyteller Bot")
|
|
||||||
parser.add_argument("-u", type=str, help="Room URL")
|
parser.add_argument("-u", type=str, help="Room URL")
|
||||||
parser.add_argument("-t", type=str, help="Token")
|
parser.add_argument("-t", type=str, help="Token")
|
||||||
config = parser.parse_args()
|
config = parser.parse_args()
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
from typing import AsyncGenerator
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from dailyai.pipeline.frames import TextFrame, Frame, AudioFrame
|
from async_timeout import timeout
|
||||||
from dailyai.pipeline.frame_processor import FrameProcessor
|
|
||||||
from dailyai.pipeline.frames import (
|
from pipecat.frames.frames import Frame, LLMResponseEndFrame, TextFrame, UserStoppedSpeakingFrame
|
||||||
Frame,
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
TextFrame,
|
from pipecat.transports.services.daily import DailyTransportMessageFrame
|
||||||
SendAppMessageFrame,
|
|
||||||
LLMResponseEndFrame,
|
|
||||||
UserStoppedSpeakingFrame,
|
|
||||||
)
|
|
||||||
|
|
||||||
from utils.helpers import load_sounds
|
from utils.helpers import load_sounds
|
||||||
from prompts import IMAGE_GEN_PROMPT, CUE_USER_TURN, CUE_ASSISTANT_TURN
|
from prompts import IMAGE_GEN_PROMPT, CUE_USER_TURN, CUE_ASSISTANT_TURN
|
||||||
import asyncio
|
|
||||||
|
|
||||||
sounds = load_sounds(["talking.wav", "listening.wav", "ding.wav"])
|
sounds = load_sounds(["talking.wav", "listening.wav", "ding.wav"])
|
||||||
|
|
||||||
@@ -42,7 +36,7 @@ class StoryImageProcessor(FrameProcessor):
|
|||||||
Processor for image prompt frames that will be sent to the FAL service.
|
Processor for image prompt frames that will be sent to the FAL service.
|
||||||
|
|
||||||
This processor is responsible for consuming frames of type `StoryImageFrame`.
|
This processor is responsible for consuming frames of type `StoryImageFrame`.
|
||||||
It processes the by passing it to the FAL service
|
It processes them by passing it to the FAL service.
|
||||||
The processed frames are then yielded back.
|
The processed frames are then yielded back.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
@@ -50,25 +44,26 @@ class StoryImageProcessor(FrameProcessor):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, fal_service):
|
def __init__(self, fal_service):
|
||||||
|
super().__init__()
|
||||||
self._fal_service = fal_service
|
self._fal_service = fal_service
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
if isinstance(frame, StoryImageFrame):
|
if isinstance(frame, StoryImageFrame):
|
||||||
try:
|
try:
|
||||||
async with asyncio.timeout(7):
|
async with timeout(7):
|
||||||
async for i in self._fal_service.process_frame(TextFrame(IMAGE_GEN_PROMPT % frame.text)):
|
async for i in self._fal_service.run_image_gen(IMAGE_GEN_PROMPT % frame.text):
|
||||||
yield i
|
await self.push_frame(i)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
yield frame
|
await self.push_frame(frame)
|
||||||
|
|
||||||
|
|
||||||
class StoryProcessor(FrameProcessor):
|
class StoryProcessor(FrameProcessor):
|
||||||
"""
|
"""
|
||||||
Primary frame processor. It takes the frames generated by the LLM
|
Primary frame processor. It takes the frames generated by the LLM
|
||||||
and processes them into image prompts and story pages (sentences.)
|
and processes them into image prompts and story pages (sentences).
|
||||||
For a clearer picture of how this works, reference prompts.py
|
For a clearer picture of how this works, reference prompts.py
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
@@ -81,15 +76,16 @@ class StoryProcessor(FrameProcessor):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, messages, story):
|
def __init__(self, messages, story):
|
||||||
|
super().__init__()
|
||||||
self._messages = messages
|
self._messages = messages
|
||||||
self._text = ""
|
self._text = ""
|
||||||
self._story = story
|
self._story = story
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
if isinstance(frame, UserStoppedSpeakingFrame):
|
if isinstance(frame, UserStoppedSpeakingFrame):
|
||||||
# Send an app message to the UI
|
# Send an app message to the UI
|
||||||
yield SendAppMessageFrame(CUE_ASSISTANT_TURN, None)
|
await self.push_frame(DailyTransportMessageFrame(CUE_ASSISTANT_TURN))
|
||||||
yield AudioFrame(sounds["talking"])
|
await self.push_frame(sounds["talking"])
|
||||||
|
|
||||||
elif isinstance(frame, TextFrame):
|
elif isinstance(frame, TextFrame):
|
||||||
# We want to look for sentence breaks in the text
|
# We want to look for sentence breaks in the text
|
||||||
@@ -111,7 +107,7 @@ class StoryProcessor(FrameProcessor):
|
|||||||
# Remove the image prompt from the text
|
# Remove the image prompt from the text
|
||||||
self._text = re.sub(r"<.*?>", '', self._text, count=1)
|
self._text = re.sub(r"<.*?>", '', self._text, count=1)
|
||||||
# Process the image prompt frame
|
# Process the image prompt frame
|
||||||
yield StoryImageFrame(image_prompt)
|
await self.push_frame(StoryImageFrame(image_prompt))
|
||||||
|
|
||||||
# STORY PAGE
|
# STORY PAGE
|
||||||
# Looking for: [break] in the LLM response
|
# Looking for: [break] in the LLM response
|
||||||
@@ -126,9 +122,9 @@ class StoryProcessor(FrameProcessor):
|
|||||||
if len(self._text) > 2:
|
if len(self._text) > 2:
|
||||||
# Append the sentence to the story
|
# Append the sentence to the story
|
||||||
self._story.append(self._text)
|
self._story.append(self._text)
|
||||||
yield StoryPageFrame(self._text)
|
await self.push_frame(StoryPageFrame(self._text))
|
||||||
# Assert that it's the LLMs turn, until we're finished
|
# Assert that it's the LLMs turn, until we're finished
|
||||||
yield SendAppMessageFrame(CUE_ASSISTANT_TURN, None)
|
await self.push_frame(DailyTransportMessageFrame(CUE_ASSISTANT_TURN))
|
||||||
# Clear the buffer
|
# Clear the buffer
|
||||||
self._text = ""
|
self._text = ""
|
||||||
|
|
||||||
@@ -136,13 +132,13 @@ class StoryProcessor(FrameProcessor):
|
|||||||
# Driven by the prompt, the LLM should have asked the user for input
|
# Driven by the prompt, the LLM should have asked the user for input
|
||||||
elif isinstance(frame, LLMResponseEndFrame):
|
elif isinstance(frame, LLMResponseEndFrame):
|
||||||
# We use a different frame type, as to avoid image generation ingest
|
# We use a different frame type, as to avoid image generation ingest
|
||||||
yield StoryPromptFrame(self._text)
|
await self.push_frame(StoryPromptFrame(self._text))
|
||||||
self._text = ""
|
self._text = ""
|
||||||
yield frame
|
await self.push_frame(frame)
|
||||||
# Send an app message to the UI
|
# Send an app message to the UI
|
||||||
yield SendAppMessageFrame(CUE_USER_TURN, None)
|
await self.push_frame(DailyTransportMessageFrame(CUE_USER_TURN))
|
||||||
yield AudioFrame(sounds["listening"])
|
await self.push_frame(sounds["listening"])
|
||||||
|
|
||||||
# Anything that is not a TextFrame pass through
|
# Anything that is not a TextFrame pass through
|
||||||
else:
|
else:
|
||||||
yield frame
|
await self.push_frame(frame)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ LLM_INTRO_PROMPT = {
|
|||||||
"content": "You are a creative storyteller who loves to tell whimsical, fantastical stories. \
|
"content": "You are a creative storyteller who loves to tell whimsical, fantastical stories. \
|
||||||
Your goal is to craft an engaging and fun story. \
|
Your goal is to craft an engaging and fun story. \
|
||||||
Start by asking the user what kind of story they'd like to hear. Don't provide any examples. \
|
Start by asking the user what kind of story they'd like to hear. Don't provide any examples. \
|
||||||
Keep your reponse to only a few sentences."
|
Keep your response to only a few sentences."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import Optional
|
|||||||
from fastapi import FastAPI, Request, HTTPException
|
from fastapi import FastAPI, Request, HTTPException
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
|
from fastapi.responses import FileResponse, JSONResponse
|
||||||
|
|
||||||
from utils.daily_helpers import create_room as _create_room, get_token, get_name_from_url
|
from utils.daily_helpers import create_room as _create_room, get_token, get_name_from_url
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from dotenv import load_dotenv
|
|||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
daily_api_path = os.getenv("DAILY_API_URL")
|
daily_api_path = os.getenv("DAILY_API_URL") or "api.daily.co/v1"
|
||||||
daily_api_key = os.getenv("DAILY_API_KEY")
|
daily_api_key = os.getenv("DAILY_API_KEY")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import os
|
|||||||
import wave
|
import wave
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
from pipecat.frames.frames import AudioRawFrame, ImageRawFrame
|
||||||
|
|
||||||
script_dir = os.path.dirname(__file__)
|
script_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
|
||||||
@@ -14,7 +16,7 @@ def load_images(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:
|
||||||
images[filename] = img.tobytes()
|
images[filename] = ImageRawFrame(image=img.tobytes(), size=img.size, format=img.format)
|
||||||
return images
|
return images
|
||||||
|
|
||||||
|
|
||||||
@@ -28,6 +30,8 @@ def load_sounds(sound_files):
|
|||||||
filename = os.path.splitext(os.path.basename(full_path))[0]
|
filename = os.path.splitext(os.path.basename(full_path))[0]
|
||||||
# Open the sound and convert it to bytes
|
# Open the sound and convert it to bytes
|
||||||
with wave.open(full_path) as audio_file:
|
with wave.open(full_path) as audio_file:
|
||||||
sounds[filename] = audio_file.readframes(-1)
|
sounds[filename] = AudioRawFrame(audio=audio_file.readframes(-1),
|
||||||
|
sample_rate=audio_file.getframerate(),
|
||||||
|
num_channels=audio_file.getnchannels())
|
||||||
|
|
||||||
return sounds
|
return sounds
|
||||||
|
|||||||
Reference in New Issue
Block a user