examples: added daily-custom-tracks
This commit is contained in:
@@ -145,6 +145,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Other
|
### 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
|
- Added `examples/daily-multi-translation` to showcase how to send multiple
|
||||||
simulataneous translations with the same transport.
|
simulataneous translations with the same transport.
|
||||||
|
|
||||||
|
|||||||
@@ -53,4 +53,3 @@ async def configure(aiohttp_session: aiohttp.ClientSession):
|
|||||||
token = await daily_rest_helper.get_token(url, expiry_time)
|
token = await daily_rest_helper.get_token(url, expiry_time)
|
||||||
|
|
||||||
return (url, token)
|
return (url, token)
|
||||||
return (url, token)
|
|
||||||
|
|||||||
39
examples/daily-custom-tracks/README.md
Normal file
39
examples/daily-custom-tracks/README.md
Normal file
@@ -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.
|
||||||
87
examples/daily-custom-tracks/bot.py
Normal file
87
examples/daily-custom-tracks/bot.py
Normal file
@@ -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())
|
||||||
74
examples/daily-custom-tracks/custom_track_sender.py
Normal file
74
examples/daily-custom-tracks/custom_track_sender.py
Normal file
@@ -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()
|
||||||
173
examples/daily-custom-tracks/index.html
Normal file
173
examples/daily-custom-tracks/index.html
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>daily custom tracks</title>
|
||||||
|
</head>
|
||||||
|
<script crossorigin src="https://unpkg.com/@daily-co/daily-js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.6/semantic.min.js"></script>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
type="text/css"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.6/semantic.min.css"
|
||||||
|
/>
|
||||||
|
<script>
|
||||||
|
function enableButton(buttonId, enable) {
|
||||||
|
const button = document.getElementById(buttonId);
|
||||||
|
button.disabled = !enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableJoinButton(enable) {
|
||||||
|
enableButton("join-button", enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableLeaveButton(enable) {
|
||||||
|
enableButton("leave-button", enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyPlayers(query) {
|
||||||
|
const items = document.querySelectorAll(query);
|
||||||
|
if (items) {
|
||||||
|
for (const item of items) {
|
||||||
|
item.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyParticipantPlayers(participantId) {
|
||||||
|
destroyPlayers(`audio[data-participant-id="${participantId}"]`);
|
||||||
|
destroyPlayers(`button[data-participant-id="${participantId}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startPlayer(player, track) {
|
||||||
|
player.muted = false;
|
||||||
|
player.autoplay = true;
|
||||||
|
if (track != null) {
|
||||||
|
player.srcObject = new MediaStream([track]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildAudioPlayer(track, participantId) {
|
||||||
|
const audioContainer = document.getElementById("audio-container");
|
||||||
|
const player = document.createElement("audio");
|
||||||
|
player.dataset.participantId = participantId;
|
||||||
|
|
||||||
|
// Create a new button for controlling audio
|
||||||
|
const audioControlButton = document.createElement("button");
|
||||||
|
audioControlButton.className = "ui primary green button"
|
||||||
|
audioControlButton.innerText = track._mediaTag == "cam-audio" ? "english" : track._mediaTag;
|
||||||
|
audioControlButton.dataset.participantId = participantId;
|
||||||
|
audioControlButton.onclick = () => {
|
||||||
|
if (player.paused) {
|
||||||
|
|
||||||
|
player.play();
|
||||||
|
audioControlButton.className = "ui primary red button"
|
||||||
|
} else {
|
||||||
|
player.pause();
|
||||||
|
audioControlButton.className = "ui primary green button"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
audioContainer.appendChild(player);
|
||||||
|
audioContainer.appendChild(audioControlButton);
|
||||||
|
|
||||||
|
await startPlayer(player, track);
|
||||||
|
player.pause()
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribeToTracks(participantId) {
|
||||||
|
console.log(`subscribing to track`);
|
||||||
|
|
||||||
|
if (participantId === "local") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callObject.updateParticipant(participantId, {
|
||||||
|
setSubscribedTracks: {
|
||||||
|
audio: true,
|
||||||
|
video: false,
|
||||||
|
custom: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDaily() {
|
||||||
|
enableJoinButton(true);
|
||||||
|
enableLeaveButton(false);
|
||||||
|
|
||||||
|
window.callObject = window.DailyIframe.createCallObject({});
|
||||||
|
|
||||||
|
callObject.on("participant-joined", (e) => {
|
||||||
|
if (!e.participant.local) {
|
||||||
|
console.log("participant-joined", e.participant);
|
||||||
|
subscribeToTracks(e.participant.session_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
callObject.on("participant-left", (e) => {
|
||||||
|
console.log("participant-left", e.participant.session_id);
|
||||||
|
destroyParticipantPlayers(e.participant.session_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
callObject.on("track-started", async (e) => {
|
||||||
|
console.log("track-started", e.track);
|
||||||
|
if (e.track.kind === "audio") {
|
||||||
|
await buildAudioPlayer(e.track, e.participant.session_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function joinRoom() {
|
||||||
|
enableJoinButton(false);
|
||||||
|
enableLeaveButton(true);
|
||||||
|
|
||||||
|
const meetingUrl = document.getElementById("meeting-url").value;
|
||||||
|
|
||||||
|
callObject.join({
|
||||||
|
url: meetingUrl,
|
||||||
|
startVideoOff: true,
|
||||||
|
startAudioOff: true,
|
||||||
|
subscribeToTracksAutomatically: false,
|
||||||
|
receiveSettings: {
|
||||||
|
base: { video: { layer: 0 } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function leaveRoom() {
|
||||||
|
enableJoinButton(true);
|
||||||
|
enableLeaveButton(false);
|
||||||
|
|
||||||
|
callObject.leave();
|
||||||
|
|
||||||
|
const audioContainer = document.getElementById("audio-container");
|
||||||
|
audioContainer.replaceChildren();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<body onload="startDaily()">
|
||||||
|
<div class="ui centered page grid" style="margin-top: 30px">
|
||||||
|
<div class="ten wide column">
|
||||||
|
<div class="ui form" style="margin-top: 30px">
|
||||||
|
<div class="field">
|
||||||
|
<label>Meeting URL</label>
|
||||||
|
<input id="meeting-url" value="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui centered aligned header" style="margin-top: 30px">
|
||||||
|
<button id="join-button" class="ui primary button" onclick="joinRoom()">
|
||||||
|
Join
|
||||||
|
</button>
|
||||||
|
<button id="leave-button" class="ui button" onclick="leaveRoom()">
|
||||||
|
Leave
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="tile" class="ui container" style="margin-top: 30px">
|
||||||
|
<div id="tile" class="ui center aligned grid">
|
||||||
|
<div id="audio-container"></div><br/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
examples/daily-custom-tracks/office-ambience-mono-16000.mp3
Normal file
BIN
examples/daily-custom-tracks/office-ambience-mono-16000.mp3
Normal file
Binary file not shown.
2
examples/daily-custom-tracks/requirements.txt
Normal file
2
examples/daily-custom-tracks/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pydub
|
||||||
|
pipecat-ai[daily]
|
||||||
55
examples/daily-custom-tracks/runner.py
Normal file
55
examples/daily-custom-tracks/runner.py
Normal file
@@ -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)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>video layers demo</title>
|
<title>daily multi translation</title>
|
||||||
</head>
|
</head>
|
||||||
<script crossorigin src="https://unpkg.com/@daily-co/daily-js"></script>
|
<script crossorigin src="https://unpkg.com/@daily-co/daily-js"></script>
|
||||||
<script
|
<script
|
||||||
|
|||||||
@@ -53,4 +53,3 @@ async def configure(aiohttp_session: aiohttp.ClientSession):
|
|||||||
token = await daily_rest_helper.get_token(url, expiry_time)
|
token = await daily_rest_helper.get_token(url, expiry_time)
|
||||||
|
|
||||||
return (url, token)
|
return (url, token)
|
||||||
return (url, token)
|
|
||||||
|
|||||||
@@ -53,4 +53,3 @@ async def configure(aiohttp_session: aiohttp.ClientSession):
|
|||||||
token = await daily_rest_helper.get_token(url, expiry_time)
|
token = await daily_rest_helper.get_token(url, expiry_time)
|
||||||
|
|
||||||
return (url, token)
|
return (url, token)
|
||||||
return (url, token)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user