> This is a page from the ElevenLabs documentation. For a complete page index, fetch https://elevenlabs.io/docs/llms.txt. For the full documentation in a single file, fetch https://elevenlabs.io/docs/llms-full.txt.

# LiveKit-integratie

Deze handleiding laat zien hoe je ElevenLabs Speech Engine gebruikt als spraaklaag voor een LiveKit-room. Een LiveKit Agents-worker neemt als deelnemer deel aan de room, abonneert zich op de audiotrack van de gebruiker, opent een WebSocket naar Speech Engine en publiceert de gesynthetiseerde audio van Speech Engine als eigen track terug naar de room.

## Architectuur

Speech Engine accepteert twee soorten WebSocket-verbindingen:

* De **brain WebSocket** waarmee de ElevenLabs API verbinding maakt. Je server voert deze uit met de Speech Engine SDK (`engine.serve()` / `engine.attach()`) en ontvangt transcripts waarop je kunt reageren.
* De **conversation WebSocket** waarmee clients verbinding maken. Browsers maken verbinding via een WebRTC-token; clients die geen browser zijn (zoals een LiveKit Agents-worker) maken verbinding via een ondertekende URL en streamen onbewerkte PCM-audio in beide richtingen.

De LiveKit-worker gebruikt de tweede verbinding. Deze werkt namens de deelnemers in de LiveKit-room als een "client" van Speech Engine.

```mermaid
sequenceDiagram
    participant Browser
    participant LK as LiveKit Room
    participant Worker as Agents Worker
    participant EL as ElevenLabs (conversation WS)
    participant Brain as Brain Server

    Browser->>LK: Join room (LiveKit token)
    Worker->>LK: Join room (dispatched)
    Worker->>EL: Open conversation WebSocket (signed URL)

    loop Conversation
        Browser->>LK: Microphone audio (Opus)
        LK->>Worker: Decoded PCM frames
        Worker->>EL: user_audio_chunk (base64 PCM)
        EL->>Brain: user_transcript
        Brain-->>EL: agent_response (streamed)
        EL->>Worker: audio (base64 PCM)
        Worker->>LK: Publish PCM frames
        LK->>Browser: Audio (Opus)
    end
```

De brain-server is ongewijzigd ten opzichte van de [snelle start voor Speech Engine](/docs/nl/eleven-api/guides/cookbooks/speech-engine) — de LiveKit-worker vervangt de browser als audiobron, maar de LLM-logica blijft hetzelfde.

## Wanneer je dit patroon gebruikt

Gebruik de LiveKit-bridge wanneer de room zelf onderdeel is van de ervaring:

* Sessies met meerdere deelnemers waarin gebruikers naast elkaar met de agent spreken
* Bestaande LiveKit-implementaties waarbij het wisselen van transport clients zou verstoren
* Spraakagenten die een room delen met schermdeling, video of tekstchat
* Via SIP naar LiveKit doorgestuurde gesprekken die een AI-agent in het gesprek nodig hebben

Als je alleen een spraaklus van browser naar Speech Engine nodig hebt zonder andere deelnemers, is de WebRTC-client in de [snelle start voor Speech Engine](/docs/nl/eleven-api/guides/cookbooks/speech-engine#client-setup) eenvoudiger — Speech Engine spreekt rechtstreeks WebRTC met de browser, zonder LiveKit-room.

## Vereisten

* Een LiveKit-project ([LiveKit Cloud](https://cloud.livekit.io/) of een zelfgehoste server). De worker heeft `LIVEKIT_URL`, `LIVEKIT_API_KEY` en `LIVEKIT_API_SECRET` nodig.
* Een ElevenLabs Speech Engine. Volg de [snelle start voor Speech Engine](/docs/nl/eleven-api/guides/cookbooks/speech-engine) om er een te maken en de brain-server uit te voeren.
* Python 3.9+ of Node.js 18+.

> **Note**
>
> De Node bridge-worker gebruikt
> [`@livekit/rtc-node`](https://www.npmjs.com/package/@livekit/rtc-node), dat momenteel in
> Developer Preview is. Gebruik voor productie-implementaties bij voorkeur de Python-worker.

## Audio-indelingen voor Speech Engine configureren

De `AudioStream` van LiveKit resampelt binnenkomende Opus-tracks naar elke gewenste PCM-samplerate, zodat je deze rechtstreeks kunt afstemmen op de invoer van Speech Engine. Werk Speech Engine bij om PCM van 16 kHz voor ASR-invoer te accepteren en PCM van 24 kHz voor TTS-uitvoer te leveren.

**`configure_engine.py`**

```python title="configure_engine.py"
import asyncio
import os
from elevenlabs import AsyncElevenLabs

elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])


async def update_engine():
    await elevenlabs.speech_engine.update(
        speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
        asr={"user_input_audio_format": "pcm_16000"},
        tts={"agent_output_audio_format": "pcm_24000"},
    )


asyncio.run(update_engine())
```

**`configure-engine.mts`**

```typescript title="configure-engine.mts"
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import "dotenv/config";

const elevenlabs = new ElevenLabsClient({
  apiKey: process.env.ELEVENLABS_API_KEY,
});

await elevenlabs.speechEngine.update("seng_8k3m9xr4hjnfg983brhmhkd98n6", {
  asr: { userInputAudioFormat: "pcm_16000" },
  tts: { agentOutputAudioFormat: "pcm_24000" },
});
```

De PCM van Speech Engine is overal signed 16-bit little-endian. Raadpleeg de [referentie voor audio-indelingen](#audio-format-reference) voor andere ondersteunde rates.

## De bridge-worker bouwen

De worker is een langlopend proces dat verbinding maakt met je LiveKit-server, op taken wacht, toegewezen rooms betreedt en audio overbrugt tussen de room en Speech Engine.

#### Afhankelijkheden installeren

**`Python`**

```bash title="Python"
pip install "livekit-agents" "livekit-api" "elevenlabs" "aiohttp" "python-dotenv"
```

**`Node`**

```bash title="Node"
npm install @livekit/agents @livekit/rtc-node @elevenlabs/elevenlabs-js ws dotenv
```

#### Een ondertekende Speech Engine-URL maken

De worker vraagt een ondertekende URL met een korte geldigheidsduur aan voor de conversation WebSocket van Speech Engine. De ondertekende URL bevat de engine-ID en een eenmalige handtekening, zodat de worker de WebSocket kan openen zonder je API-sleutel bloot te geven.

**`bridge.py`**

```python title="bridge.py"
from elevenlabs import AsyncElevenLabs

elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])

async def signed_url() -> str:
    response = await elevenlabs.conversational_ai.conversations.get_signed_url(
        agent_id=os.environ["SPEECH_ENGINE_ID"],
    )
    return response.signed_url
```

**`bridge.mts`**

```typescript title="bridge.mts"
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";

const elevenlabs = new ElevenLabsClient({
  apiKey: process.env.ELEVENLABS_API_KEY,
});

async function signedUrl(): Promise<string> {
  const response = await elevenlabs.conversationalAi.conversations.getSignedUrl({
    agentId: process.env.SPEECH_ENGINE_ID!,
  });
  return response.signedUrl;
}
```

#### Het entrypoint van de worker definiëren

Elke keer dat de worker naar een room wordt gestuurd, wordt het entrypoint uitgevoerd. Het entrypoint maakt verbinding met de room, opent een conversation WebSocket van Speech Engine en start twee audio-bridges: één voor audio van de beller naar Speech Engine en één voor gesynthetiseerde audio die terugkomt.

**`bridge.py`**

```python title="bridge.py" maxLines=0
import asyncio
import base64
import json
import os

import aiohttp
from dotenv import load_dotenv
from elevenlabs import AsyncElevenLabs
from livekit import agents, rtc
from livekit.agents import JobContext, WorkerOptions, cli

load_dotenv()

elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
SPEECH_ENGINE_ID = os.environ["SPEECH_ENGINE_ID"]

USER_INPUT_RATE = 16000
AGENT_OUTPUT_RATE = 24000


async def signed_url() -> str:
    response = await elevenlabs.conversational_ai.conversations.get_signed_url(
        agent_id=SPEECH_ENGINE_ID,
    )
    return response.signed_url


async def entrypoint(ctx: JobContext):
    el_ws_ready: asyncio.Future[aiohttp.ClientWebSocketResponse] = (
        asyncio.get_running_loop().create_future()
    )

    async def pump_user_audio(track: rtc.Track):
        el_ws = await el_ws_ready
        stream = rtc.AudioStream(
            track, sample_rate=USER_INPUT_RATE, num_channels=1,
        )
        async for event in stream:
            payload = base64.b64encode(bytes(event.frame.data)).decode()
            await el_ws.send_str(json.dumps({"user_audio_chunk": payload}))

    # Register the subscriber BEFORE ctx.connect() so we don't miss tracks
    # that get auto-subscribed during the connection handshake.
    @ctx.room.on("track_subscribed")
    def on_track_subscribed(track, publication, participant):
        if track.kind != rtc.TrackKind.KIND_AUDIO:
            return
        if participant.identity == ctx.room.local_participant.identity:
            return
        asyncio.create_task(pump_user_audio(track))

    await ctx.connect()

    # Publish a track for the agent's synthesized audio.
    source = rtc.AudioSource(sample_rate=AGENT_OUTPUT_RATE, num_channels=1)
    track = rtc.LocalAudioTrack.create_audio_track("elevenlabs-agent", source)
    await ctx.room.local_participant.publish_track(
        track,
        rtc.TrackPublishOptions(source=rtc.TrackSource.SOURCE_MICROPHONE),
    )

    # Open the Speech Engine conversation WebSocket.
    http = aiohttp.ClientSession()
    el_ws = await http.ws_connect(await signed_url())
    await el_ws.send_str(json.dumps({"type": "conversation_initiation_client_data"}))
    el_ws_ready.set_result(el_ws)

    async def el_to_room():
        async for msg in el_ws:
            if msg.type != aiohttp.WSMsgType.TEXT:
                continue
            event = json.loads(msg.data)
            etype = event.get("type")
            if etype == "audio":
                pcm = base64.b64decode(event["audio_event"]["audio_base_64"])
                samples_per_channel = len(pcm) // 2
                frame = rtc.AudioFrame(
                    pcm, AGENT_OUTPUT_RATE, 1, samples_per_channel,
                )
                await source.capture_frame(frame)
            elif etype == "interruption":
                source.clear_queue()
            elif etype == "ping":
                event_id = event.get("ping_event", {}).get("event_id")
                await el_ws.send_str(json.dumps({
                    "type": "pong", "event_id": event_id,
                }))

    pump_task = asyncio.create_task(el_to_room())

    async def cleanup():
        pump_task.cancel()
        await el_ws.close()
        await http.close()

    ctx.add_shutdown_callback(cleanup)


if __name__ == "__main__":
    cli.run_app(WorkerOptions(
        entrypoint_fnc=entrypoint,
        agent_name="elevenlabs-bridge",
    ))
```

**`bridge.mts`**

```typescript title="bridge.mts" maxLines=0
import {
  type JobContext,
  WorkerOptions,
  cli,
  defineAgent,
} from "@livekit/agents";
import {
  AudioFrame,
  AudioSource,
  AudioStream,
  LocalAudioTrack,
  RoomEvent,
  TrackKind,
  TrackPublishOptions,
  TrackSource,
} from "@livekit/rtc-node";
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import WebSocket from "ws";
import { fileURLToPath } from "node:url";
import "dotenv/config";

const elevenlabs = new ElevenLabsClient({
  apiKey: process.env.ELEVENLABS_API_KEY,
});
const SPEECH_ENGINE_ID = process.env.SPEECH_ENGINE_ID!;

const USER_INPUT_RATE = 16000;
const AGENT_OUTPUT_RATE = 24000;

async function signedUrl(): Promise<string> {
  const response = await elevenlabs.conversationalAi.conversations.getSignedUrl({
    agentId: SPEECH_ENGINE_ID,
  });
  return response.signedUrl;
}

export default defineAgent({
  entry: async (ctx: JobContext) => {
    let resolveElReady: (ws: WebSocket) => void;
    const elReady = new Promise<WebSocket>((resolve) => {
      resolveElReady = resolve;
    });

    // Register the subscriber BEFORE ctx.connect() so we don't miss
    // tracks that get auto-subscribed during the connection handshake.
    ctx.room.on(RoomEvent.TrackSubscribed, (track, _pub, participant) => {
      if (track.kind !== TrackKind.KIND_AUDIO) return;
      if (participant.identity === ctx.room.localParticipant?.identity) return;

      (async () => {
        const ws = await elReady;
        const stream = new AudioStream(track, {
          sampleRate: USER_INPUT_RATE,
          numChannels: 1,
        });
        for await (const frame of stream) {
          const payload = Buffer.from(
            frame.data.buffer,
            frame.data.byteOffset,
            frame.data.byteLength,
          ).toString("base64");
          ws.send(JSON.stringify({ user_audio_chunk: payload }));
        }
      })();
    });

    await ctx.connect();

    const source = new AudioSource(AGENT_OUTPUT_RATE, 1);
    const track = LocalAudioTrack.createAudioTrack("elevenlabs-agent", source);
    const publishOptions = new TrackPublishOptions();
    publishOptions.source = TrackSource.SOURCE_MICROPHONE;
    await ctx.room.localParticipant!.publishTrack(track, publishOptions);

    const ws = new WebSocket(await signedUrl());
    await new Promise<void>((resolve, reject) => {
      ws.once("open", () => resolve());
      ws.once("error", reject);
    });
    ws.send(JSON.stringify({ type: "conversation_initiation_client_data" }));
    resolveElReady!(ws);

    // Serialize captureFrame calls — concurrent captures throw
    // InvalidState in the rtc-node native layer.
    let captureChain: Promise<unknown> = Promise.resolve();

    ws.on("message", (raw) => {
      const event = JSON.parse(raw.toString());
      if (event.type === "audio") {
        const pcm = Buffer.from(event.audio_event.audio_base_64, "base64");
        const samples = new Int16Array(
          pcm.buffer, pcm.byteOffset, pcm.byteLength / 2,
        );
        const frame = new AudioFrame(
          samples, AGENT_OUTPUT_RATE, 1, samples.length,
        );
        captureChain = captureChain
          .then(() => source.captureFrame(frame))
          .catch((err) => console.warn("captureFrame:", err.message));
      } else if (event.type === "interruption") {
        source.clearQueue();
      } else if (event.type === "ping") {
        ws.send(JSON.stringify({
          type: "pong", event_id: event.ping_event?.event_id,
        }));
      }
    });

    ctx.addShutdownCallback(async () => {
      ws.close();
    });
  },
});

cli.runApp(new WorkerOptions({
  agent: fileURLToPath(import.meta.url),
  agentName: "elevenlabs-bridge",
}));
```

De worker filtert zijn eigen gepubliceerde audio uit in de handler `track_subscribed` door deze te vergelijken met de identiteit van de lokale deelnemer. Zonder deze controle zou de worker proberen zijn eigen gesynthetiseerde audio terug te sturen naar Speech Engine.

Twee details over de volgorde zijn belangrijk voor de juiste werking:

* **Timing van listener**: `TrackSubscribed` wordt geregistreerd vóór `ctx.connect()`. LiveKit abonneert zich tijdens de verbindingshandshake automatisch op bestaande tracks en een listener die daarna wordt geregistreerd, kan het event missen. De audiopomp wacht op een `Future` / `Promise` voor de Speech Engine WebSocket, zodat deze zich onmiddellijk kan abonneren en audio kan doorsturen zodra de verbinding open is.
* **Alleen TypeScript — serialisatie van capture**: `AudioSource.captureFrame` van `@livekit/rtc-node` genereert `InvalidState` wanneer deze gelijktijdig wordt aangeroepen. De TypeScript-handler serializeert captures met een promise-keten. De enkele `async for el_to_room`-lus van Python is van nature sequentieel en heeft dit niet nodig.

#### De worker starten

**`Python`**

```bash title="Python"
python bridge.py dev
```

**`Node`**

```bash title="Node"
npx tsx bridge.mts dev
```

`dev` schakelt hot reload en gekleurde logs in. Gebruik in productie `start` voor JSON-logs en netjes afsluiten.

De worker maakt verbinding met je LiveKit-server en wacht op taaktoewijzingen. Deze treedt pas rooms binnen wanneer hij wordt aangestuurd.

## De worker naar een room sturen

Omdat de worker een `agent_name` heeft, gebruikt deze expliciete dispatch — hij treedt alleen rooms binnen wanneer je backend hem dat opdraagt. Het eenvoudigste patroon is een `RoomAgentDispatch` op te nemen in het LiveKit-toegangstoken dat de browser gebruikt om verbinding te maken.

**`token_server.py`**

```python title="token_server.py"
import os

from dotenv import load_dotenv
from flask import Flask, jsonify, request
from livekit.api import AccessToken, RoomAgentDispatch, VideoGrants

load_dotenv()

app = Flask(**name**)

@app.route("/api/livekit-token")
def get_token():
room_name = request.args.get("room", "demo-room")
identity = request.args.get("identity", "web-user")

    token = (
        AccessToken(
            os.environ["LIVEKIT_API_KEY"],
            os.environ["LIVEKIT_API_SECRET"],
        )
        .with_identity(identity)
        .with_grants(VideoGrants(room_join=True, room=room_name))
        .with_room_config(
            room_configuration={
                "agents": [RoomAgentDispatch(agent_name="elevenlabs-bridge")],
            },
        )
    )

    return jsonify(token=token.to_jwt(), url=os.environ["LIVEKIT_URL"])

if **name** == "**main**":
app.run(port=3002)

```

**`token-server.mts`**

```typescript title="token-server.mts"
import express from "express";
import { AccessToken } from "livekit-server-sdk";
import "dotenv/config";

const app = express();

app.get("/api/livekit-token", async (req, res) => {
  const room = (req.query.room as string) ?? "demo-room";
  const identity = (req.query.identity as string) ?? "web-user";

  const token = new AccessToken(
    process.env.LIVEKIT_API_KEY!,
    process.env.LIVEKIT_API_SECRET!,
    { identity },
  );
  token.addGrant({ roomJoin: true, room });
  token.roomConfig = {
    agents: [{ agentName: "elevenlabs-bridge" }],
  };

  res.json({
    token: await token.toJwt(),
    url: process.env.LIVEKIT_URL,
  });
});

app.listen(3002, () => {
  console.log("Token server listening on port 3002");
});
```

Wanneer een browser dit token gebruikt om een room te maken of te betreden, stuurt LiveKit de bridge-worker automatisch naar dezelfde room.

## Verbinden vanuit de browser

De browser heeft alleen de standaard LiveKit-client nodig — deze communiceert niet rechtstreeks met Speech Engine.

**`App.tsx`**

```typescript title="App.tsx"
import { Room, RoomEvent, Track } from "livekit-client";
import { useCallback, useState } from "react";

export default function App() {
  const [room] = useState(() => new Room());

  const join = useCallback(async () => {
    const response = await fetch("/api/livekit-token");
    const { token, url } = await response.json();

    room.on(RoomEvent.TrackSubscribed, (track) => {
      if (track.kind === Track.Kind.Audio) {
        document.body.appendChild(track.attach());
      }
    });

    await room.connect(url, token);
    await room.localParticipant.setMicrophoneEnabled(true);
  }, [room]);

  return <button onClick={join}>Start conversation</button>;
}
```

Wanneer er op de knop wordt geklikt, haalt de browser een LiveKit-token op, treedt de room binnen met de microfoon ingeschakeld en begint de audiotrack van de agent te ontvangen. De worker wordt aangestuurd, opent de Speech Engine-sessie en overbrugt audio in beide richtingen.

## Referentie voor audio-indelingen

Speech Engine ondersteunt de volgende audio-indelingen. Configureer ze op de engine via `asr.user_input_audio_format` en `tts.agent_output_audio_format`.

| Indeling    | Samplerate | Encoding             | Opmerkingen                                              |
| ----------- | ---------- | -------------------- | -------------------------------------------------------- |
| `pcm_8000`  | 8 kHz      | Signed 16-bit LE PCM | Alleen ASR-invoer.                                       |
| `pcm_16000` | 16 kHz     | Signed 16-bit LE PCM | Aanbevolen voor LiveKit-gebruikersinvoer.                |
| `pcm_22050` | 22,05 kHz  | Signed 16-bit LE PCM |                                                          |
| `pcm_24000` | 24 kHz     | Signed 16-bit LE PCM | Aanbevolen voor LiveKit-agentuitvoer.                    |
| `pcm_44100` | 44,1 kHz   | Signed 16-bit LE PCM | TTS-uitvoer vereist Independent Publisher-tier of hoger. |
| `pcm_48000` | 48 kHz     | Signed 16-bit LE PCM | Alleen ASR-invoer.                                       |
| `ulaw_8000` | 8 kHz      | μ-law                | Gebruikt door Twilio Media Streams.                      |

`AudioStream` en `AudioSource` in LiveKit verwerken resampling voor je — je kunt elke samplerate aanvragen bij `AudioStream` en de SDK converteert vanuit de onderliggende Opus-track van 48 kHz.

## Aandachtspunten voor productie

* **Expliciete dispatch**: Stel altijd `agent_name` / `agentName` in op `WorkerOptions`. Auto-dispatch activeert de worker voor elke room die in je LiveKit-project wordt gemaakt, wat je zelden wilt.
* **Authenticatie van brain-server**: Stel een gedeeld geheim in voor Speech Engine en verifieer dit in je brain-server, zodat alleen Speech Engine je endpoint kan bereiken:
  ```python
  await elevenlabs.speech_engine.update(
      speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
      speech_engine={"request_headers": {"x-api-key": os.environ["SHARED_SECRET"]}},
  )
  ```
  De brain-server controleert vervolgens `request.headers["x-api-key"]` voordat de WebSocket-upgrade wordt geaccepteerd.
* **Tokenserver**: Maak LiveKit- en Speech Engine-tokens aan de serverzijde. Geef `LIVEKIT_API_SECRET` of `ELEVENLABS_API_KEY` nooit bloot aan de browser.
* **Eventloophygiëne**: Houd CPU-gebonden werk buiten de eventloop van de worker. `AudioSource.capture_frame` en iteratie van `AudioStream` zijn tijdgevoelig; lange synchrone aanroepen vertragen of laten onderbrekingsevents wegvallen. Gebruik `asyncio.to_thread()` (Python) of `worker_threads` (Node) voor blokkerend werk.
* **Afsluiten**: Registreer `ctx.add_shutdown_callback` / `ctx.addShutdownCallback` om de ElevenLabs WebSocket netjes te sluiten. Standaard wordt de room (en de taak) beëindigd wanneer de laatste deelnemer die geen agent is vertrekt.

## Volgende stappen

#### [Snelle start voor Speech Engine](/docs/nl/eleven-api/guides/cookbooks/speech-engine)

Bouw de brain-server die op transcripts reageert.

#### [Pipecat-integratie](/docs/nl/eleven-api/guides/how-to/speech-engine/pipecat-integration)

Gebruik Pipecat als LLM-pipeline achter Speech Engine.

#### [Python SDK-referentie](/docs/nl/eleven-api/resources/libraries/speech-engine/python-sdk-reference)

Klassen, methoden en events voor de Speech Engine Python SDK.

#### [JavaScript SDK-referentie](/docs/nl/eleven-api/resources/libraries/speech-engine/javascript-sdk-reference)

Klassen, methoden en events voor de Speech Engine JavaScript SDK.