diff --git a/examples/podcast-chatbot/README.md b/examples/podcast-chatbot/README.md new file mode 100644 index 000000000..cac0764a7 --- /dev/null +++ b/examples/podcast-chatbot/README.md @@ -0,0 +1,24 @@ +# podcastai + +### Have a conversation about any technology topics + +podcastai is a fast conversational AI built using [Daily](https://www.daily.co/) for real-time media transport and [Cartesia](https://cartesia.ai) for text-to-speech. Everything is orchestrated together (VAD -> STT -> LLM -> TTS) using [Pipecat](https://www.pipecat.ai/). + +## Get started + +```python +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt + +cp env.example .env # and add your credentials + +``` + +## Run the bot + +```bash +python bot.py +``` + +While the app is running, go to the `https://.daily.co/` set in `DAILY_SAMPLE_ROOM_URL` and talk to podcaster! diff --git a/examples/podcast-chatbot/bot.py b/examples/podcast-chatbot/bot.py new file mode 100644 index 000000000..228730572 --- /dev/null +++ b/examples/podcast-chatbot/bot.py @@ -0,0 +1,100 @@ +import aiohttp +import asyncio +import os +import sys +from loguru import logger +from dotenv import load_dotenv + +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import LLMMessagesFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext +from pipecat.services.cartesia import CartesiaTTSService +from pipecat.services.openai import OpenAILLMService +from pipecat.transports.services.daily import DailyParams, DailyTransport + +from runner import configure + +load_dotenv(override=True) + +# Run this script directly from your command line. +# This project was adapted from +# https://github.com/pipecat-ai/pipecat/blob/main/examples/foundational/07d-interruptible-cartesia.py + +logger.remove(0) +logger.add(sys.stderr, level="DEBUG") + + +# This is the main function that handles STT -> LLM -> TTS + + +async def main(): + async with aiohttp.ClientSession() as session: + (room_url, token) = await configure(session) + + transport = DailyTransport( + room_url, + token, + "Tech Podcast", + DailyParams( + audio_out_sample_rate=44100, + audio_out_enabled=True, + transcription_enabled=True, + vad_enabled=True, + vad_analyzer=SileroVADAnalyzer(), + ), + ) + + tts = CartesiaTTSService( + api_key=os.getenv("CARTESIA_API_KEY"), + voice_id=os.getenv("CARTESIA_VOICE_ID", "4d2fd738-3b3d-4368-957a-bb4805275bd9"), # British Narration Lady + params=CartesiaTTSService.InputParams( + sample_rate=44100, + ), + ) + + llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4") + + messages = [ + { + "role": "system", + "content": "You are an AI assistant, the podcast host. You will engage with the participants by asking about tech topics, keeping responses short, clear, and conversational." + } + ] + + context = OpenAILLMContext(messages) + context_aggregator = llm.create_context_aggregator(context) + + pipeline = Pipeline( + [ + transport.input(), + context_aggregator.user(), + llm, + tts, + transport.output(), + context_aggregator.assistant(), + ] + ) + + task = PipelineTask(pipeline, PipelineParams(allow_interruptions=True, enable_metrics=True)) + + @transport.event_handler("on_first_participant_joined") + async def on_first_participant_joined(transport, participant): + transport.capture_participant_transcription(participant["id"]) + messages.append( + { + "role": "system", + "content": "Hey, You are Keith. Start introducing yourself and welcoming participants to the tech podcast! Ask them `What topic are you interested in today?`", + } + ) + await task.queue_frames([LLMMessagesFrame(messages)]) + + runner = PipelineRunner() + + await runner.run(task) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/podcast-chatbot/env.example b/examples/podcast-chatbot/env.example new file mode 100644 index 000000000..69245a3d6 --- /dev/null +++ b/examples/podcast-chatbot/env.example @@ -0,0 +1,5 @@ +DAILY_SAMPLE_ROOM_URL= # Follow instructions here and put your https://YOURDOMAIN.daily.co/YOURROOM (Instructions: https://docs.pipecat.ai/quickstart#preparing-your-environment) +DAILY_API_KEY= # Create here: https://dashboard.daily.co/developers +OPENAI_API_KEY= # Create here: https://platform.openai.com/docs/overview +CARTESIA_API_KEY= # Create here: https://play.cartesia.ai/console +CARTESIA_VOICE_ID= # Find here: https://play.cartesia.ai/ \ No newline at end of file diff --git a/examples/podcast-chatbot/requirements.txt b/examples/podcast-chatbot/requirements.txt new file mode 100644 index 000000000..13569f65c --- /dev/null +++ b/examples/podcast-chatbot/requirements.txt @@ -0,0 +1,4 @@ +pypdf==4.3.1 +loguru +pipecat-ai[daily,cartesia,openai,silero]==0.0.46 +python-dotenv==1.0.1 diff --git a/examples/podcast-chatbot/runner.py b/examples/podcast-chatbot/runner.py new file mode 100644 index 000000000..13c4ff076 --- /dev/null +++ b/examples/podcast-chatbot/runner.py @@ -0,0 +1,62 @@ +# +# Copyright (c) 2024, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import aiohttp +import argparse +import os + +from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper + + +async def configure(aiohttp_session: aiohttp.ClientSession): + (url, token, _) = await configure_with_args(aiohttp_session) + return (url, token) + + +async def configure_with_args( + aiohttp_session: aiohttp.ClientSession, parser: argparse.ArgumentParser | None = None +): + if not parser: + parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample") + parser.add_argument( + "-u", "--url", type=str, required=False, help="URL of the Daily room to join" + ) + parser.add_argument( + "-k", + "--apikey", + type=str, + required=False, + help="Daily API Key (needed to create an owner token for the room)", + ) + + args, unknown = parser.parse_known_args() + + url = args.url or os.getenv("DAILY_SAMPLE_ROOM_URL") + key = args.apikey or os.getenv("DAILY_API_KEY") + + if not url: + raise Exception( + "No Daily room specified. use the -u/--url option from the command line, or set DAILY_SAMPLE_ROOM_URL in your environment to specify a Daily room URL." + ) + + if not key: + raise Exception( + "No Daily API key specified. use the -k/--apikey option from the command line, or set DAILY_API_KEY in your environment to specify a Daily API key, available from https://dashboard.daily.co/developers." + ) + + daily_rest_helper = DailyRESTHelper( + daily_api_key=key, + daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"), + aiohttp_session=aiohttp_session, + ) + + # Create a meeting token for the given room with an expiration 1 hour in + # the future. + expiry_time: float = 60 * 60 + + token = await daily_rest_helper.get_token(url, expiry_time) + + return (url, token, args)