diff --git a/CHANGELOG.md b/CHANGELOG.md index ec61847a7..7925b5b38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -145,6 +145,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Other +- Added `examples/daily-custom-tracks` to show how to send and receive Daily + custom tracks. + - Added `examples/daily-multi-translation` to showcase how to send multiple simulataneous translations with the same transport. diff --git a/examples/chatbot-audio-recording/runner.py b/examples/chatbot-audio-recording/runner.py index 50743fd09..ad39a3ac4 100644 --- a/examples/chatbot-audio-recording/runner.py +++ b/examples/chatbot-audio-recording/runner.py @@ -53,4 +53,3 @@ async def configure(aiohttp_session: aiohttp.ClientSession): token = await daily_rest_helper.get_token(url, expiry_time) return (url, token) - return (url, token) diff --git a/examples/daily-custom-tracks/README.md b/examples/daily-custom-tracks/README.md new file mode 100644 index 000000000..dfd870373 --- /dev/null +++ b/examples/daily-custom-tracks/README.md @@ -0,0 +1,39 @@ +# Daily Custom Tracks + +This example shows how to send and receive Daily custom tracks. We will run a simple `daily-python` application to send an audio file with a custom track (named "pipecat") to a room. Then, the Pipecat bot will mirror that custom track into another custom track (named "pipecat-mirror") in the same room. + +## Get started + +```python +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +## Run the bot + +Start the bot by giving it a Daily room URL. + +```bash +python bot.py -u ROOM_URL +``` + +The bot will wait for the first participant to join. Then, it will mirror a custom track named "pipecat" into a new custom track named "pipecat-mirror". + +## Run the sender + +Now, run the custom track sender. This is a simple `daily-python` application that opens and audio file and sends it as a custom track to the same Daily room. + +```bash +python custom_track_sender.py -u ROOM_URL -i office-ambience-mono-16000.mp3 +``` + +## Open client + +Finally, open the client so you can hear both custom tracks. + +```bash +open index.html +``` + +Once the client is opened, copy the URL of the Daily room and join it. You should be able to select which custom track you want to hear. diff --git a/examples/daily-custom-tracks/bot.py b/examples/daily-custom-tracks/bot.py new file mode 100644 index 000000000..9792f17c6 --- /dev/null +++ b/examples/daily-custom-tracks/bot.py @@ -0,0 +1,87 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import asyncio +import sys + +import aiohttp +from loguru import logger +from runner import configure + +from pipecat.frames.frames import Frame, InputAudioRawFrame, OutputAudioRawFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.frame_processor import FrameDirection, FrameProcessor +from pipecat.transports.services.daily import DailyParams, DailyTransport + +logger.remove(0) +logger.add(sys.stderr, level="DEBUG") + + +class CustomTrackMirrorProcessor(FrameProcessor): + def __init__(self, transport_destination: str, **kwargs): + super().__init__(**kwargs) + self._transport_destination = transport_destination + + async def process_frame(self, frame: Frame, direction: FrameDirection): + await super().process_frame(frame, direction) + + if isinstance(frame, InputAudioRawFrame) and frame.transport_source: + output_frame = OutputAudioRawFrame( + audio=frame.audio, + sample_rate=frame.sample_rate, + num_channels=frame.num_channels, + ) + output_frame.transport_destination = self._transport_destination + await self.push_frame(output_frame) + else: + await self.push_frame(frame, direction) + + +async def main(): + async with aiohttp.ClientSession() as session: + (room_url, _) = await configure(session) + + transport = DailyTransport( + room_url, + None, + "Custom tracks mirror", + DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + microphone_enabled=False, # Disable since we just use custom tracks + audio_out_destinations=["pipecat-mirror"], + ), + ) + + pipeline = Pipeline( + [ + transport.input(), # Transport user input + CustomTrackMirrorProcessor("pipecat-mirror"), + transport.output(), # Transport bot output + ] + ) + + task = PipelineTask( + pipeline, + params=PipelineParams( + audio_in_sample_rate=16000, + audio_out_sample_rate=16000, + ), + ) + + @transport.event_handler("on_first_participant_joined") + async def on_first_participant_joined(transport, participant): + await transport.capture_participant_audio(participant["id"], audio_source="pipecat") + + runner = PipelineRunner() + + await runner.run(task) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/daily-custom-tracks/custom_track_sender.py b/examples/daily-custom-tracks/custom_track_sender.py new file mode 100644 index 000000000..80c3cfbe6 --- /dev/null +++ b/examples/daily-custom-tracks/custom_track_sender.py @@ -0,0 +1,74 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import argparse +import time + +from daily import CallClient, CustomAudioSource, Daily +from pydub import AudioSegment + +parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample") +parser.add_argument("-u", "--url", type=str, required=True, help="URL of the Daily room to join") +parser.add_argument( + "-i", "--input", type=str, required=True, help="Input audio file (needs 16000 sample rate)" +) + +args, _ = parser.parse_known_args() + +audio = AudioSegment.from_mp3(args.input) + +raw_bytes = audio.raw_data +sample_rate = audio.frame_rate +channels = audio.channels + +print(f"Length: {len(raw_bytes)} bytes") +print(f"Sample rate: {sample_rate}, Channels: {channels}") + +# Initialize the Daily context & create call client +Daily.init() + +client = CallClient() + +# Join the room and indicate we have a custom track named "pipecat". +client.join( + args.url, + client_settings={ + "publishing": { + "camera": False, + "microphone": False, + "customAudio": {"pipecat": True}, + }, + }, +) + +# Just sleep for a couple of seconds. To do this well we should really use +# completions. +time.sleep(2) + +# Create the custom audio source. This is where we will write our audio. +audio_source = CustomAudioSource(sample_rate, channels) + +# Create an audio track and assign it our audio source. +client.add_custom_audio_track("pipecat", audio_source) + +# Just sleep for a second. To do this well we should really use completions. +time.sleep(1) + +try: + # Just write one second of audio until we have read all the file. + chunk_size = sample_rate * channels * 2 + while len(raw_bytes) > 0: + chunk = raw_bytes[:chunk_size] + raw_bytes = raw_bytes[chunk_size:] + audio_source.write_frames(chunk) + +except KeyboardInterrupt: + client.leave() + +# Just sleep for a second. To do this well we should really use completions. +time.sleep(1) + +client.release() diff --git a/examples/daily-custom-tracks/index.html b/examples/daily-custom-tracks/index.html new file mode 100644 index 000000000..4b3f693f6 --- /dev/null +++ b/examples/daily-custom-tracks/index.html @@ -0,0 +1,173 @@ + + + daily custom tracks + + + + + + + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+

+
+
+ + diff --git a/examples/daily-custom-tracks/office-ambience-mono-16000.mp3 b/examples/daily-custom-tracks/office-ambience-mono-16000.mp3 new file mode 100644 index 000000000..ea98082c7 Binary files /dev/null and b/examples/daily-custom-tracks/office-ambience-mono-16000.mp3 differ diff --git a/examples/daily-custom-tracks/requirements.txt b/examples/daily-custom-tracks/requirements.txt new file mode 100644 index 000000000..b3d2deec3 --- /dev/null +++ b/examples/daily-custom-tracks/requirements.txt @@ -0,0 +1,2 @@ +pydub +pipecat-ai[daily] diff --git a/examples/daily-custom-tracks/runner.py b/examples/daily-custom-tracks/runner.py new file mode 100644 index 000000000..ad39a3ac4 --- /dev/null +++ b/examples/daily-custom-tracks/runner.py @@ -0,0 +1,55 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import argparse +import os + +import aiohttp + +from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper + + +async def configure(aiohttp_session: aiohttp.ClientSession): + 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) diff --git a/examples/daily-multi-translation/index.html b/examples/daily-multi-translation/index.html index 52fd3d488..c8ca0832d 100644 --- a/examples/daily-multi-translation/index.html +++ b/examples/daily-multi-translation/index.html @@ -1,6 +1,6 @@ - video layers demo + daily multi translation