I want to build an agent that just uses eleven lab...
# ask-ai
q
I want to build an agent that just uses eleven labs text to speech model and relay the speech as it is without any other llm or stt model. How do you propose should we go ahead with this
t
Thanks for reaching out, Harsh. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — this is a great, clear goal. You want an agent that uses only ElevenLabs TTS to synthesize and relay speech (no STT, no LLM). Below is a concise plan and a runnable approach you can adopt with LiveKit Agents. Acknowledgement • I’m answering how to build a LiveKit agent that accepts text you provide and relays synthesized audio using ElevenLabs TTS only. High-level approach • Use the ElevenLabs TTS plugin in standalone mode (no STT or LLM components). • Create a TTS stream, push text into it, and publish the synthesized audio frames as a LiveKit audio track so participants hear the speech in real time. • Control the input text directly (API/webhook/UI) so the agent never performs transcription or language-model generation. Quick checklist • Install the LiveKit Agents package with ElevenLabs support. • Set your
ELEVEN_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)
Copy 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 Documentation
q
can you give me the code for this in node js
t
Thanks — I’ll provide a clear Node.js (TypeScript) example that joins a LiveKit room, uses ElevenLabs TTS in standalone mode, and relays the synthesized audio into the room. This example uses only ElevenLabs TTS (no STT or LLM). I’ll also include installation and environment notes and a short snippet showing how to trigger speech from incoming data messages. Summary of what this does • Connects to a LiveKit room. • Creates a local audio source/track. • Streams ElevenLabs TTS audio and captures frames into the audio source so participants hear the speech. • Accepts text you supply (static or via data messages) and relays it verbatim. Prerequisites • Node 18+ (or compatible runtime) • Install packages:
Copy code
pnpm add @livekit/agents @livekit/agents-plugin-elevenlabs
• Set environment variable:
Copy code
ELEVEN_API_KEY=your_elevenlabs_api_key_here
TypeScript example (standalone ElevenLabs TTS agent) • Save as e.g.
elevenlabs_tts_agent.ts
Copy code
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?
q
let's have a dummy participant join the room so that the room would stay alive for the entirety of its duration. Then we can have a static message played.
t
Thanks — clear request. Below is a focused Node.js (TypeScript) example that: • Joins a LiveKit room as a dummy participant (keeps the room alive). • Uses ElevenLabs TTS (standalone) to synthesize a static message. • Publishes the synthesized audio as a local audio track so other participants hear it. • Uses only ElevenLabs TTS — no STT or LLM. Prerequisites • Node 18+ runtime. • Install packages:
Copy code
pnpm 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
Copy code
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 Documentation
q
can we ensure that the room is alive for only 5 mins
t
Acknowledged — I'll show how to ensure the room stays alive for exactly 5 minutes. Below is a concise TypeScript Node.js agent example that: • Joins a LiveKit room as a dummy participant (keeps the room alive). • Plays a static ElevenLabs TTS message immediately. • Keeps the room alive for 5 minutes, then disconnects the dummy participant and stops the agent. Key behavior • The agent stays connected for 5 minutes (300,000 ms). After the timer expires, it gracefully stops publishing and disconnects, which will allow the room to end if no other participants remain. • You can still trigger extra TTS while the agent is connected; anything after the 5-minute timeout will be ignored. Prerequisites • Node 18+ • Packages: pnpm add @livekit/agents @livekit/agents-plugin-elevenlabs • Environment: ELEVEN_API_KEY set • This is written as a LiveKit Agent worker (TypeScript). I can convert to plain JS if you prefer. Code: dummy participant, play static message, disconnect after 5 minutes Save as e.g.
eleven_tts_5min_agent.ts
Copy code
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?