> 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-integration

Den här guiden visar hur du använder ElevenLabs Speech Engine som röstlager för ett LiveKit-rum. En LiveKit Agents-arbetare ansluter till rummet som deltagare, prenumererar på användarens ljudspår, öppnar en WebSocket till Speech Engine och publicerar Speech Engines syntetiserade ljud tillbaka till rummet som ett eget spår.

## Arkitektur

Speech Engine accepterar två typer av WebSocket-anslutningar:

* **Brain WebSocket** som ElevenLabs API ansluter till. Din server kör detta med Speech Engine SDK (`engine.serve()` / `engine.attach()`) och tar emot transkriptioner att svara på.
* **Conversation WebSocket** som klienter ansluter till. Webbläsare ansluter via en WebRTC-token; klienter som inte är webbläsare (till exempel en LiveKit Agents-arbetare) ansluter via en signerad URL och strömmar rått PCM-ljud i båda riktningarna.

LiveKit-arbetaren använder den andra anslutningen. Den fungerar som en "klient" till Speech Engine för deltagarna i LiveKit-rummet.

```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
```

Brain-servern är oförändrad från [snabbstarten för Speech Engine](/docs/sv/eleven-api/guides/cookbooks/speech-engine) — LiveKit-arbetaren ersätter webbläsaren som ljudkälla, men LLM-logiken är densamma.

## När du ska använda det här mönstret

Använd LiveKit-bryggan när själva rummet är en del av upplevelsen:

* Sessioner med flera deltagare där användare talar med agenten tillsammans
* Befintliga LiveKit-distributioner där ett byte av transport skulle störa klienter
* Röstagenter som delar rum med skärmdelning, video eller textchatt
* SIP-till-LiveKit-dirigerade samtal som behöver en AI-agent i samtalet

Om du bara behöver en röstloop från webbläsare till Speech Engine utan andra deltagare är WebRTC-klienten i [snabbstarten för Speech Engine](/docs/sv/eleven-api/guides/cookbooks/speech-engine#client-setup) enklare — Speech Engine kommunicerar direkt med webbläsaren via WebRTC och inget LiveKit-rum behövs.

## Förutsättningar

* Ett LiveKit-projekt (antingen [LiveKit Cloud](https://cloud.livekit.io/) eller en server som du hostar själv). Arbetaren behöver `LIVEKIT_URL`, `LIVEKIT_API_KEY` och `LIVEKIT_API_SECRET`.
* En ElevenLabs Speech Engine. Följ [snabbstarten för Speech Engine](/docs/sv/eleven-api/guides/cookbooks/speech-engine) för att skapa en och köra brain-servern.
* Python 3.9+ eller Node.js 18+.

> **Note**
>
> Node-bryggarbetaren använder
> [`@livekit/rtc-node`](https://www.npmjs.com/package/@livekit/rtc-node), som för närvarande är i
> Developer Preview. För produktionsdistributioner rekommenderar vi Python-arbetaren.

## Konfigurera ljudformat för Speech Engine

LiveKits `AudioStream` omsamplar inkommande Opus-spår till den PCM-samplingsfrekvens du begär, så du kan matcha Speech Engines indata direkt. Uppdatera Speech Engine så att den accepterar 16 kHz PCM för ASR-indata och skickar ut 24 kHz PCM för TTS-utdata.

**`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" },
});
```

PCM i Speech Engine är genomgående signerad 16-bitars little-endian. Se [referensen för ljudformat](#audio-format-reference) för andra samplingsfrekvenser som stöds.

## Bygg bryggarbetaren

Arbetaren är en långvarig process som ansluter till din LiveKit-server, väntar på jobb, ansluter till tilldelade rum och bryggar ljud mellan rummet och Speech Engine.

#### Installera beroenden

**`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
```

#### Skapa en signerad URL för Speech Engine

Arbetaren begär en kortlivad signerad URL för Speech Engines Conversation WebSocket. Den signerade URL:en innehåller engine-ID:t och en engångssignatur, så att arbetaren kan öppna WebSocket-anslutningen utan att exponera din API-nyckel.

**`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;
}
```

#### Definiera arbetarens startpunkt

Varje gång arbetaren skickas till ett rum körs dess startpunkt. Startpunkten ansluter till rummet, öppnar en Conversation WebSocket för Speech Engine och startar två ljudbryggor: en för samtalsljud som går till Speech Engine och en för syntetiserat ljud som kommer tillbaka.

**`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",
}));
```

Arbetaren filtrerar bort sitt eget publicerade ljud i hanteraren `track_subscribed` genom att jämföra med den lokala deltagarens identitet. Utan denna kontroll skulle arbetaren försöka skicka sitt eget syntetiserade ljud tillbaka till Speech Engine.

Två detaljer kring ordningen är viktiga för korrekt funktion:

* **Tidpunkt för lyssnaren**: `TrackSubscribed` registreras före `ctx.connect()`. LiveKit prenumererar automatiskt på befintliga spår under anslutningshandskakningen, och en lyssnare som registreras senare kan missa händelsen. Ljudpumpen väntar på ett `Future` / `Promise` för Speech Engine WebSocket så att den kan prenumerera direkt och vidarebefordra ljud så snart anslutningen är öppen.
* **Endast TypeScript — serialisering av inspelning**: `AudioSource.captureFrame` i `@livekit/rtc-node` kastar `InvalidState` om det anropas samtidigt. TypeScript-hanteraren serialiserar inspelningar med en promise-kedja. Pythons enda `async for el_to_room`-loop är naturligt sekventiell och behöver inte detta.

#### Starta arbetaren

**`Python`**

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

**`Node`**

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

`dev` aktiverar hot reload och färgade loggar. Använd `start` i produktion för JSON-loggar och smidig avstängning.

Arbetaren ansluter till din LiveKit-server och väntar på jobbtilldelningar. Den ansluter inte till några rum förrän den skickas dit.

## Skicka arbetaren till ett rum

Eftersom arbetaren har ett `agent_name` använder den explicit dirigering — den ansluter bara till rum när din backend säger åt den att göra det. Det enklaste mönstret är att inkludera en `RoomAgentDispatch` i LiveKit-åtkomsttoken som webbläsaren använder för att ansluta.

**`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");
});
```

När en webbläsare använder denna token för att skapa eller ansluta till ett rum skickar LiveKit automatiskt bryggarbetaren till samma rum.

## Anslut från webbläsaren

Webbläsaren behöver bara standardklienten för LiveKit — den interagerar inte direkt med 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>;
}
```

När knappen klickas hämtar webbläsaren en LiveKit-token, ansluter till rummet med mikrofonen aktiverad och börjar ta emot agentens ljudspår. Arbetaren skickas dit, öppnar sin Speech Engine-session och bryggar ljud i båda riktningarna.

## Referens för ljudformat

Speech Engine har stöd för följande ljudformat. Konfigurera dem på motorn via `asr.user_input_audio_format` och `tts.agent_output_audio_format`.

| Format      | Samplingsfrekvens | Kodning                   | Kommentarer                                                |
| ----------- | ----------------- | ------------------------- | ---------------------------------------------------------- |
| `pcm_8000`  | 8 kHz             | Signerad 16-bitars LE PCM | Endast ASR-indata.                                         |
| `pcm_16000` | 16 kHz            | Signerad 16-bitars LE PCM | Rekommenderas för LiveKit-användarindata.                  |
| `pcm_22050` | 22,05 kHz         | Signerad 16-bitars LE PCM |                                                            |
| `pcm_24000` | 24 kHz            | Signerad 16-bitars LE PCM | Rekommenderas för LiveKit-agentutdata.                     |
| `pcm_44100` | 44,1 kHz          | Signerad 16-bitars LE PCM | TTS-utdata kräver nivån Independent Publisher eller högre. |
| `pcm_48000` | 48 kHz            | Signerad 16-bitars LE PCM | Endast ASR-indata.                                         |
| `ulaw_8000` | 8 kHz             | μ-law                     | Används av Twilio Media Streams.                           |

`AudioStream` och `AudioSource` i LiveKit hanterar omsampling åt dig — du kan begära vilken samplingsfrekvens som helst från `AudioStream` och SDK:n konverterar från det underliggande Opus-spåret på 48 kHz.

## Att tänka på i produktion

* **Explicit dirigering**: Ange alltid `agent_name` / `agentName` i `WorkerOptions`. Automatisk dirigering aktiverar arbetaren för varje rum som skapas i ditt LiveKit-projekt, vilket sällan är vad du vill.
* **Autentisering av brain-servern**: Ange en delad hemlighet i Speech Engine och verifiera den i din brain-server, så att endast Speech Engine kan nå din slutpunkt:
  ```python
  await elevenlabs.speech_engine.update(
      speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
      speech_engine={"request_headers": {"x-api-key": os.environ["SHARED_SECRET"]}},
  )
  ```
  Brain-servern kontrollerar sedan `request.headers["x-api-key"]` innan den accepterar WebSocket-uppgraderingen.
* **Tokenserver**: Skapa LiveKit- och Speech Engine-token på serversidan. Exponera aldrig `LIVEKIT_API_SECRET` eller `ELEVENLABS_API_KEY` för webbläsaren.
* **Hygien för händelseloopen**: Håll CPU-bundet arbete borta från arbetarens händelseloop. `AudioSource.capture_frame` och iteration över `AudioStream` är tidskänsliga; långa synkrona anrop fördröjer eller tappar avbrottshändelser. Använd `asyncio.to_thread()` (Python) eller `worker_threads` (Node) för blockerande arbete.
* **Avstängning**: Registrera `ctx.add_shutdown_callback` / `ctx.addShutdownCallback` för att stänga ElevenLabs WebSocket-anslutningen korrekt. Som standard avslutas rummet (och jobbet) när den sista deltagaren som inte är en agent lämnar.

## Nästa steg

#### [Snabbstart för Speech Engine](/docs/sv/eleven-api/guides/cookbooks/speech-engine)

Bygg brain-servern som svarar på transkriptioner.

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

Använd Pipecat som LLM-pipeline bakom Speech Engine.

#### [Referens för Python SDK](/docs/sv/eleven-api/resources/libraries/speech-engine/python-sdk-reference)

Klasser, metoder och händelser för Speech Engine Python SDK.

#### [Referens för JavaScript SDK](/docs/sv/eleven-api/resources/libraries/speech-engine/javascript-sdk-reference)

Klasser, metoder och händelser för Speech Engine JavaScript SDK.