quick-cartoon-22764
02/23/2026, 9:56 PMtall-airline-16756
02/23/2026, 9:56 PMtall-airline-16756
02/23/2026, 9:57 PMELEVEN_API_KEY.
• Create an audio source and local audio track in the room.
• Instantiate an elevenlabs.TTS stream and push text into it.
• Capture audio frames from the TTS stream and publish them.
Minimal implementation (conceptual Python outline)
1. Create audio source & publish track for participants to hear:
◦ Create rtc.AudioSource(sample_rate, channels) and a LocalAudioTrack.
◦ Publish the track from the local participant.
1. Instantiate ElevenLabs TTS and stream:
◦ Create tts = elevenlabs.TTS(model=<model_name>, voice_id=<voice>, ...)
◦ Use tts.stream() to get an async audio stream.
1. Forward audio frames into the audio source:
◦ Consume the async audio_stream and call audio_source.capture_frame(audio_frame) for each frame.
1. Provide text input:
◦ Push text into the tts stream (e.g., await tts_stream.push_text("Your message")).
◦ End input when done (await tts_stream.end_input()).
Example flow (pseudo-code)
# create audio source and publish track
audio_source = rtc.AudioSource(24000, 1)
track = rtc.LocalAudioTrack.create_audio_track("agent-tts", audio_source)
await room.local_participant.publish_track(track)
# create TTS stream
tts = elevenlabs.TTS(model="eleven_turbo_v2_5", voice_id="your_voice_id")
tts_stream = tts.stream()
# forward audio frames in background
async def forward_audio():
async for frame in tts_stream:
await audio_source.capture_frame(frame)
asyncio.create_task(forward_audio())
# push text (no STT/LLM)
await tts_stream.push_text("Hello, this will be relayed via ElevenLabs.")
await tts_stream.end_input()
Key points and options
• No STT or LLM used: You provide the text directly; the system will not transcribe or generate text.
• Real-time streaming: Speech can be emitted incrementally as you push text.
• Voice and model customization: You can set voice_id, language, stability, etc., in the TTS constructor.
• Triggering text: Text can come from an API, UI button, scheduled task, or any service you control.
Next questions to tailor this for you
• Do you want the agent to accept live typed text, messages via an API, or pre-recorded text files?
• Which ElevenLabs voice/model and sample rate do you plan to use (so I can show exact constructor options)?
• Do you want to support multiple participants hearing the same audio or individualized streams per participant?
If you tell me which of the above fits your use case, I’ll provide a concrete code sample (including exact package install command, environment variables, and any room/participant wiring) tailored to your environment.
Sources: ElevenLabs TTS plugin guide | LiveKit Documentation | examples/voice_agents | livekit/livekit_composite | DeepWiki | Text-to-speech (TTS) models overview | LiveKit Documentationquick-cartoon-22764
02/23/2026, 10:00 PMtall-airline-16756
02/23/2026, 10:00 PMpnpm add @livekit/agents @livekit/agents-plugin-elevenlabs
• Set environment variable:
ELEVEN_API_KEY=your_elevenlabs_api_key_here
TypeScript example (standalone ElevenLabs TTS agent)
• Save as e.g. elevenlabs_tts_agent.ts
import { WorkerOptions, JobContext } from "@livekit/agents";
import { voice } from "@livekit/agents";
import { ElevenLabs } from "@livekit/agents-plugin-elevenlabs";
/**
* Worker that uses only ElevenLabs TTS to synthesize and publish audio.
* No STT or LLM involved.
*/
export const worker: WorkerOptions = {
entry: async (ctx: JobContext) => {
// Connect to the room (ctx.connect() will use job/agent config)
await ctx.connect();
// Create an audio source (sampleRate, channels). Match ElevenLabs output sample rate if known.
// Common choices: 24000 or 44100. Here we use 24000, mono.
const SAMPLE_RATE = 24000;
const CHANNELS = 1;
const audioSource = new voice.AudioSource(SAMPLE_RATE, CHANNELS);
// Create and publish a local audio track so participants can hear the TTS
const localTrack = new voice.LocalAudioTrack(audioSource, "eleven-tts-track");
await ctx.room.localParticipant.publishTrack(localTrack);
// Initialize ElevenLabs TTS (voice/model optional fields)
const tts = new ElevenLabs.TTS({
model: "eleven_multilingual_v2", // adjust if you prefer another model
voice: { id: "default" }, // replace with a specific voice id if desired
// You can provide other settings supported by the plugin here
});
// Helper: consume async audio frames from the TTS stream and forward to the audio source
const forwardAudio = async (audioIterable: AsyncIterable<any>) => {
try {
for await (const audioChunk of audioIterable) {
// audioChunk should include audio frame(s) in the plugin's AudioFrame format
// Capture frame into audio source so it is published to the room
await audioSource.captureFrame(audioChunk.audio.frame);
}
} catch (err) {
console.error("Error forwarding audio:", err);
}
};
// Function to speak arbitrary text (creates a fresh TTS stream each time)
const speakText = async (text: string) => {
const ttsStream = tts.stream();
// `run()` gives you the async audio iterable produced by the stream
const audioIterable = ttsStream.run();
// Start forwarding audio in background
ctx.task(forwardAudio(audioIterable));
// Push the text (verbatim) and then end input to finish generation
await ttsStream.pushText(text);
await ttsStream.endInput();
};
// Example: speak a static phrase immediately
await speakText("Hello — this audio is relayed using ElevenLabs TTS only.");
// Optional: listen for data messages from participants to trigger TTS dynamically
ctx.room.on("data_received", async (packet) => {
try {
// Example: expect a data message with topic "tts" and payload as UTF-8 text
// Adjust parsing based on how you send data messages from clients
const payload = packet.data; // Buffer
const topic = (packet as any).topic ?? undefined;
if (topic === "tts") {
const inputText = payload.toString("utf8");
await speakText(inputText);
}
} catch (err) {
console.error("Error handling data message:", err);
}
});
// Keep the worker running (or exit when job ends). ctx will manage lifecycle.
},
};
Notes, tips and common adjustments
• Sample rate: Match the AudioSource sample rate with the actual audio frames produced by ElevenLabs for best results (24000 and 44100 are common).
• Voice/model: Replace voice.id with a specific ElevenLabs voice ID if you want a particular voice.
• Input sources: Replace the static speakText(...) call with any input mechanism — HTTP endpoint, UI button, scheduled messages, or forwarded room data messages (example included).
• Concurrency: Each speakText creates a separate TTS stream. If you expect overlapping utterances, queue or mix audio appropriately.
• Latency: The streaming approach emits audio incrementally as synthesis completes, supporting near-real-time playback.
• Error handling: Add additional retry/backoff for transient network or API errors when calling ElevenLabs.
If you want, I can:
• Convert this into plain JavaScript (no TypeScript types).
• Show an example client snippet that sends data messages with topic "tts" to trigger speech.
• Add sample rate detection or conversion if your environment needs a different sample rate.
Which of those would you like next?quick-cartoon-22764
02/23/2026, 10:06 PMtall-airline-16756
02/23/2026, 10:07 PMpnpm add @livekit/agents @livekit/agents-plugin-elevenlabs @livekit/rtc
(If you prefer npm: replace pnpm add with npm install.)
• Set environment variables:
◦ ELEVEN_API_KEY=your_elevenlabs_key
◦ LIVEKIT_API_KEY and LIVEKIT_API_SECRET or use your worker/job configuration as appropriate for your LiveKit Agents runtime.
Notes
• This example is written as a LiveKit Agent worker style (TypeScript). If you want plain JS, I can convert it.
• Sample rate: uses 24000 Hz (common with TTS). Adjust if your ElevenLabs output differs.
Code: dummy participant + static message playback Save as e.g. eleven_tts_dummy_agent.ts
import { WorkerOptions, JobContext } from "@livekit/agents";
import { voice } from "@livekit/agents";
import { ElevenLabs } from "@livekit/agents-plugin-elevenlabs";
/**
* Worker that:
* - Connects/join a room as an agent (dummy participant).
* - Keeps the room alive by staying connected.
* - Uses ElevenLabs TTS in standalone mode to synthesize a static message.
* - Publishes the synthesized audio to the room as a local audio track.
*
* No STT or LLM is used.
*/
export const worker: WorkerOptions = {
entry: async (ctx: JobContext) => {
// connect to the room (job config provides tokens/room)
await ctx.connect();
// Audio settings
const SAMPLE_RATE = 24000;
const CHANNELS = 1;
// Create an audio source and publish it so the room stays alive and participants hear audio
const audioSource = new voice.AudioSource(SAMPLE_RATE, CHANNELS);
const localTrack = new voice.LocalAudioTrack(audioSource, "eleven-tts-track");
await ctx.room.localParticipant.publishTrack(localTrack);
// Initialize ElevenLabs TTS (standalone)
const tts = new ElevenLabs.TTS({
model: "eleven_multilingual_v2",
// Optionally set a specific voice ID:
// voice: { id: "your_voice_id" },
});
// Helper to forward audio frames from the TTS stream into the LiveKit audio source
const forwardAudio = async (audioIterable: AsyncIterable<any>) => {
try {
for await (const chunk of audioIterable) {
// The plugin yields audio chunks containing a frame in the plugin's expected format.
// Capture the frame into the audio source so it is published to the room.
await audioSource.captureFrame(chunk.audio.frame);
}
} catch (err) {
console.error("forwardAudio error:", err);
}
};
// Function to speak a text string (creates and runs a TTS stream)
const speakText = async (text: string) => {
const ttsStream = tts.stream();
const audioIterable = ttsStream.run();
// Forward audio asynchronously (non-blocking)
ctx.task(forwardAudio(audioIterable));
// Send the text verbatim and end input
await ttsStream.pushText(text);
await ttsStream.endInput();
};
// Keep the room alive by staying connected.
// Immediately play a static message once connected:
await speakText("Hello — this room is kept alive by a dummy participant. This is a static ElevenLabs text-to-speech message.");
// Optionally keep running and accept further triggers via data messages:
ctx.room.on("data_received", async (packet) => {
try {
const topic = (packet as any).topic ?? undefined;
// If clients send data messages with topic "tts", the payload is used as text
if (topic === "tts") {
const payload = packet.data; // Buffer
const text = payload.toString("utf8");
if (text && text.trim().length > 0) {
await speakText(text);
}
}
} catch (err) {
console.error("data_received handler error:", err);
}
});
// The worker remains connected; ctx lifecycle controls shutdown.
},
};
How it works (brief)
• The worker connects to a LiveKit room and publishes a local audio track backed by AudioSource.
• ElevenLabs.TTS.stream() provides an async audio iterable when .run() is called.
• We read audio frames from the iterable and call audioSource.captureFrame(...) to publish them.
• The dummy participant (the agent) stays connected for the job lifetime, keeping the room alive while playback occurs.
Next steps I can help with
• Convert this TypeScript sample to plain JavaScript.
• Show a small client snippet that sends a data message topic "tts" to trigger the agent.
• Add queuing to handle multiple TTS triggers so they don't overlap.
• Adjust sample rate or do on-the-fly resampling if needed.
Which of these would you like next?
Sources: ElevenLabs TTS | LiveKit Documentationquick-cartoon-22764
02/23/2026, 10:21 PMtall-airline-16756
02/23/2026, 10:21 PMeleven_tts_5min_agent.ts
import { WorkerOptions, JobContext } from "@livekit/agents";
import { voice } from "@livekit/agents";
import { ElevenLabs } from "@livekit/agents-plugin-elevenlabs";
const ROOM_LIFETIME_MS = 5 * 60 * 1000; // 5 minutes
export const worker: WorkerOptions = {
entry: async (ctx: JobContext) => {
// Connect to the room (ctx.connect() uses job config)
await ctx.connect();
// Audio source settings
const SAMPLE_RATE = 24000;
const CHANNELS = 1;
// Create audio source and publish track so the room stays alive
const audioSource = new voice.AudioSource(SAMPLE_RATE, CHANNELS);
const localTrack = new voice.LocalAudioTrack(audioSource, "eleven-tts-track");
await ctx.room.localParticipant.publishTrack(localTrack);
// Initialize ElevenLabs TTS
const tts = new ElevenLabs.TTS({
model: "eleven_multilingual_v2",
// voice: { id: "your_voice_id" },
});
// Forward audio frames into the audio source
const forwardAudio = async (audioIterable: AsyncIterable<any>) => {
try {
for await (const chunk of audioIterable) {
await audioSource.captureFrame(chunk.audio.frame);
}
} catch (err) {
console.error("forwardAudio error:", err);
}
};
// Track whether we're still allowed to speak (stop after timeout)
let active = true;
// Speak text helper
const speakText = async (text: string) => {
if (!active) return;
const ttsStream = tts.stream();
const audioIterable = ttsStream.run();
ctx.task(forwardAudio(audioIterable));
await ttsStream.pushText(text);
await ttsStream.endInput();
};
// Play static message immediately
await speakText("Hello — this room will remain active for five minutes while this dummy participant is connected.");
// Optionally accept data messages to trigger additional TTS while active
ctx.room.on("data_received", async (packet) => {
try {
const topic = (packet as any).topic ?? undefined;
if (!active) return;
if (topic === "tts") {
const payload = packet.data;
const inputText = payload.toString("utf8");
if (inputText && inputText.trim().length > 0) {
await speakText(inputText);
}
}
} catch (err) {
console.error("data_received handler error:", err);
}
});
// Schedule shutdown after ROOM_LIFETIME_MS
setTimeout(async () => {
try {
active = false; // stop handling further speak requests
// Unpublish the track and disconnect gracefully
try {
await ctx.room.localParticipant.unpublishTrack(localTrack);
} catch (e) {
console.warn("unpublishTrack error:", e);
}
// Allow small delay to flush any last frames
await new Promise((res) => setTimeout(res, 500));
await ctx.disconnect();
} catch (err) {
console.error("Error during graceful shutdown:", err);
}
}, ROOM_LIFETIME_MS);
// Keep worker alive for job lifecycle; ctx manages termination.
},
};
Notes and optional enhancements
• Graceful stop: This code sets active = false and unpublishes the track before disconnecting so playback finishes cleanly.
• Immediate disconnect at 5 minutes: The agent disconnects exactly after 5 minutes regardless of ongoing speech. If you prefer to finish the currently playing utterance before disconnecting, add a short wait or a flag to let the current stream finish.
• Server-side room TTL: If you need stronger guarantees about room lifetime independent of participants, you can configure a room-level TTL or automated cleanup on your LiveKit server (not shown here). The agent timer is sufficient to guarantee the dummy participant leaves after 5 minutes.
• Convert to plain JavaScript or change sample rate: Tell me if you want the JS version or a different sample rate.
Would you like the plain JavaScript version or to add “finish current speech before disconnect” behavior?