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

# Integracja z LiveKit

Ten przewodnik pokazuje, jak używać ElevenLabs Speech Engine jako warstwy głosowej pokoju LiveKit. Worker LiveKit Agents dołącza do pokoju jako uczestnik, subskrybuje ścieżkę audio użytkownika, otwiera WebSocket ze Speech Engine i publikuje zsyntetyzowane audio Speech Engine z powrotem w pokoju jako własną ścieżkę.

## Architektura

Speech Engine obsługuje dwa rodzaje połączeń WebSocket:

* **WebSocket mózgu**, z którym łączy się API ElevenLabs. Twój serwer uruchamia go za pomocą SDK Speech Engine (`engine.serve()` / `engine.attach()`) i otrzymuje transkrypcje, na które ma odpowiadać.
* **WebSocket rozmowy**, z którym łączą się klienci. Przeglądarki łączą się przez token WebRTC; klienci spoza przeglądarki (np. worker LiveKit Agents) łączą się przez podpisany URL i strumieniują surowe audio PCM w obu kierunkach.

Worker LiveKit używa drugiego połączenia. Działa jako „klient” Speech Engine w imieniu uczestników pokoju LiveKit.

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

Serwer mózgu pozostaje bez zmian względem [krótkiego wprowadzenia do Speech Engine](/docs/pl/eleven-api/guides/cookbooks/speech-engine) — worker LiveKit zastępuje przeglądarkę jako źródło audio, ale logika LLM pozostaje taka sama.

## Kiedy użyć tego wzorca

Skorzystaj z mostu LiveKit, gdy sam pokój jest częścią doświadczenia:

* Sesje z wieloma uczestnikami, podczas których użytkownicy rozmawiają z agentem razem
* Istniejące wdrożenia LiveKit, w których zmiana transportu zepsułaby klientów
* Agenci głosowi współdzielący pokój z udostępnianiem ekranu, wideo lub czatem tekstowym
* Kierowane połączenia SIP-to-LiveKit, które wymagają agenta AI na linii

Jeśli potrzebujesz tylko pętli głosowej przeglądarka–Speech Engine bez innych uczestników, klient WebRTC w [krótkim wprowadzeniu do Speech Engine](/docs/pl/eleven-api/guides/cookbooks/speech-engine#client-setup) będzie prostszy — Speech Engine komunikuje się z przeglądarką bezpośrednio przez WebRTC, bez pokoju LiveKit.

## Wymagania wstępne

* Projekt LiveKit ([LiveKit Cloud](https://cloud.livekit.io/) lub serwer hostowany samodzielnie). Worker potrzebuje `LIVEKIT_URL`, `LIVEKIT_API_KEY` i `LIVEKIT_API_SECRET`.
* ElevenLabs Speech Engine. Postępuj zgodnie z [krótkim wprowadzeniem do Speech Engine](/docs/pl/eleven-api/guides/cookbooks/speech-engine), aby go utworzyć i uruchomić serwer mózgu.
* Python 3.9+ lub Node.js 18+.

> **Note**
>
> Worker mostu Node używa
> [`@livekit/rtc-node`](https://www.npmjs.com/package/@livekit/rtc-node), który jest obecnie w
> wersji Developer Preview. W środowiskach produkcyjnych wybierz workera Python.

## Skonfiguruj formaty audio Speech Engine

`AudioStream` LiveKit zmienia częstotliwość próbkowania przychodzących ścieżek Opus na dowolną żądaną częstotliwość PCM, więc możesz bezpośrednio dopasować wejście Speech Engine. Zaktualizuj Speech Engine, aby przyjmował PCM 16 kHz na wejściu ASR i generował PCM 24 kHz na wyjściu TTS.

**`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 Speech Engine ma w całym przepływie format 16-bitowy ze znakiem i kolejnością little-endian. Inne obsługiwane częstotliwości znajdziesz w [dokumentacji formatów audio](#audio-format-reference).

## Zbuduj worker mostka

Worker to długotrwały proces, który łączy się z serwerem LiveKit, czeka na zadania, dołącza do przypisanych pokoi i przekazuje audio między pokojem a Speech Engine.

#### Zainstaluj zależności

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

#### Utwórz podpisany URL Speech Engine

Worker żąda krótkotrwałego podpisanego URL-a do WebSocketu rozmowy Speech Engine. Podpisany URL zawiera ID silnika i jednorazowy podpis, więc worker może otworzyć WebSocket bez ujawniania klucza API.

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

#### Zdefiniuj punkt wejścia workera

Za każdym razem, gdy worker zostaje wysłany do pokoju, uruchamia się jego punkt wejścia. Łączy się on z pokojem, otwiera WebSocket rozmowy Speech Engine i uruchamia dwa mostki audio: jeden dla audio rozmówcy wysyłanego do Speech Engine, a drugi dla syntezowanego audio wracającego z powrotem.

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

Worker odfiltrowuje własne opublikowane audio w obsłudze `track_subscribed`, porównując je z tożsamością lokalnego uczestnika. Bez tego sprawdzenia próbowałby wysłać własne syntezowane audio z powrotem do Speech Engine.

Dwa szczegóły kolejności są kluczowe dla poprawności:

* **Czas rejestracji nasłuchiwania**: `TrackSubscribed` jest rejestrowany przed `ctx.connect()`. LiveKit automatycznie subskrybuje istniejące ścieżki podczas uzgadniania połączenia, a nasłuchiwanie zarejestrowane później może pominąć zdarzenie. Pompa audio czeka na `Future` / `Promise` dla WebSocketu Speech Engine, dzięki czemu może subskrybować od razu i przekazywać audio zaraz po otwarciu połączenia.
* **Tylko TypeScript — serializacja przechwytywania**: `AudioSource.captureFrame` z `@livekit/rtc-node` zgłasza `InvalidState`, jeśli jest wywoływane równocześnie. Obsługa TypeScript serializuje przechwytywanie za pomocą łańcucha promise. Pętla `async for el_to_room` w Pythonie jest naturalnie sekwencyjna i tego nie potrzebuje.

#### Uruchom workera

**`Python`**

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

**`Node`**

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

`dev` włącza hot reload i kolorowe logi. Na produkcji użyj `start`, aby mieć logi JSON i łagodne zamykanie.

Worker łączy się z serwerem LiveKit i czeka na przydzielenie zadań. Nie dołącza do żadnych pokoi, dopóki nie zostanie wysłany.

## Wyślij workera do pokoju

Ponieważ worker ma `agent_name`, używa jawnego wysyłania — dołącza do pokoi tylko wtedy, gdy nakaże mu to backend. Najprostszy sposób to umieszczenie `RoomAgentDispatch` w tokenie dostępu LiveKit, którego przeglądarka używa do połączenia.

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

Gdy przeglądarka użyje tego tokena, aby utworzyć pokój lub do niego dołączyć, LiveKit automatycznie wyśle workera mostka do tego samego pokoju.

## Połącz z przeglądarki

Przeglądarka potrzebuje tylko standardowego klienta LiveKit — nie komunikuje się bezpośrednio ze 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>;
}
```

Po kliknięciu przycisku przeglądarka pobiera token LiveKit, dołącza do pokoju z włączonym mikrofonem i zaczyna odbierać ścieżkę audio agenta. Worker zostaje wysłany, otwiera sesję Speech Engine i przekazuje audio w obu kierunkach.

## Informacje o formatach audio

Speech Engine obsługuje poniższe formaty audio. Skonfiguruj je w silniku za pomocą `asr.user_input_audio_format` i `tts.agent_output_audio_format`.

| Format      | Częstotliwość próbkowania | Kodowanie                    | Uwagi                                                        |
| ----------- | ------------------------- | ---------------------------- | ------------------------------------------------------------ |
| `pcm_8000`  | 8 kHz                     | PCM LE ze znakiem, 16-bitowe | Tylko wejście ASR.                                           |
| `pcm_16000` | 16 kHz                    | PCM LE ze znakiem, 16-bitowe | Zalecane dla wejścia użytkownika LiveKit.                    |
| `pcm_22050` | 22,05 kHz                 | PCM LE ze znakiem, 16-bitowe |                                                              |
| `pcm_24000` | 24 kHz                    | PCM LE ze znakiem, 16-bitowe | Zalecane dla wyjścia agenta LiveKit.                         |
| `pcm_44100` | 44,1 kHz                  | PCM LE ze znakiem, 16-bitowe | Wyjście TTS wymaga planu Independent Publisher lub wyższego. |
| `pcm_48000` | 48 kHz                    | PCM LE ze znakiem, 16-bitowe | Tylko wejście ASR.                                           |
| `ulaw_8000` | 8 kHz                     | μ-law                        | Używane przez Twilio Media Streams.                          |

`AudioStream` i `AudioSource` w LiveKit obsługują resampling za ciebie — możesz zażądać dowolnej częstotliwości próbkowania od `AudioStream`, a SDK przekonwertuje ją z bazowej ścieżki Opus 48 kHz.

## Kwestie produkcyjne

* **Jawne wysyłanie**: Zawsze ustawiaj `agent_name` / `agentName` w `WorkerOptions`. Automatyczne wysyłanie uruchamia workera dla każdego pokoju utworzonego w projekcie LiveKit, co rzadko jest tym, czego chcesz.
* **Uwierzytelnianie serwera brain**: Ustaw wspólny sekret w Speech Engine i weryfikuj go na serwerze brain, aby tylko Speech Engine mógł dotrzeć do endpointu:
  ```python
  await elevenlabs.speech_engine.update(
      speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
      speech_engine={"request_headers": {"x-api-key": os.environ["SHARED_SECRET"]}},
  )
  ```
  Serwer brain sprawdza następnie `request.headers["x-api-key"]` przed zaakceptowaniem przejścia na WebSocket.
* **Serwer tokenów**: Twórz tokeny LiveKit i Speech Engine po stronie serwera. Nigdy nie ujawniaj w przeglądarce `LIVEKIT_API_SECRET` ani `ELEVENLABS_API_KEY`.
* **Higiena pętli zdarzeń**: Nie uruchamiaj zadań intensywnie korzystających z CPU w pętli zdarzeń workera. `AudioSource.capture_frame` i iteracja `AudioStream` są wrażliwe na czas; długie synchroniczne wywołania opóźnią lub pominą zdarzenia przerwań. Do blokujących zadań używaj `asyncio.to_thread()` (Python) lub `worker_threads` (Node).
* **Zamykanie**: Zarejestruj `ctx.add_shutdown_callback` / `ctx.addShutdownCallback`, aby poprawnie zamknąć WebSocket ElevenLabs. Domyślnie pokój (i zadanie) jest kończony, gdy wyjdzie ostatni uczestnik niebędący agentem.

## Kolejne kroki

#### [Szybki start ze Speech Engine](/docs/pl/eleven-api/guides/cookbooks/speech-engine)

Zbuduj serwer brain, który odpowiada na transkrypcje.

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

Użyj Pipecat jako pipeline'u LLM za Speech Engine.

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

Klasy, metody i zdarzenia Python SDK Speech Engine.

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

Klasy, metody i zdarzenia JavaScript SDK Speech Engine.