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

# Integração com LiveKit

Este guia mostra como usar o Speech Engine da ElevenLabs como a camada de voz de uma sala do LiveKit. Um worker do LiveKit Agents entra na sala como participante, assina a faixa de áudio do usuário, abre um WebSocket para o Speech Engine e publica o áudio sintetizado pelo Speech Engine de volta na sala como sua própria faixa.

## Arquitetura

O Speech Engine aceita dois tipos de conexões WebSocket:

* O **WebSocket do cérebro**, ao qual a API da ElevenLabs se conecta. Seu servidor o executa com o SDK do Speech Engine (`engine.serve()` / `engine.attach()`) e recebe transcrições para responder.
* O **WebSocket da conversa**, ao qual os clientes se conectam. Os navegadores se conectam por um token WebRTC; clientes que não são navegadores (como um worker do LiveKit Agents) se conectam por uma URL assinada e transmitem áudio PCM bruto em ambas as direções.

O worker do LiveKit usa a segunda conexão. Ele atua como um "cliente" do Speech Engine em nome dos participantes da sala do 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
```

O servidor do cérebro não muda em relação ao [guia rápido do Speech Engine](/docs/pt/eleven-api/guides/cookbooks/speech-engine) — o worker do LiveKit substitui o navegador como fonte de áudio, mas a lógica do LLM continua a mesma.

## Quando usar este padrão

Use a ponte com o LiveKit quando a própria sala fizer parte da experiência:

* Sessões com vários participantes, em que os usuários conversam com o agente ao mesmo tempo
* Implantações existentes do LiveKit, nas quais trocar o transporte interromperia os clientes
* Agentes de voz que compartilham uma sala com compartilhamento de tela, vídeo ou chat de texto
* Chamadas SIP para LiveKit distribuídas que precisam de um agente de IA na chamada

Se você só precisa de um loop de voz do navegador para o Speech Engine, sem outros participantes, o cliente WebRTC no [guia rápido do Speech Engine](/docs/pt/eleven-api/guides/cookbooks/speech-engine#client-setup) é mais simples — o Speech Engine se comunica diretamente com o navegador via WebRTC, sem precisar de uma sala do LiveKit.

## Pré-requisitos

* Um projeto do LiveKit ([LiveKit Cloud](https://cloud.livekit.io/) ou um servidor auto-hospedado). O worker precisa de `LIVEKIT_URL`, `LIVEKIT_API_KEY` e `LIVEKIT_API_SECRET`.
* Um Speech Engine da ElevenLabs. Siga o [guia rápido do Speech Engine](/docs/pt/eleven-api/guides/cookbooks/speech-engine) para criar um e executar o servidor do cérebro.
* Python 3.9+ ou Node.js 18+.

> **Note**
>
> O worker de ponte em Node usa
> [`@livekit/rtc-node`](https://www.npmjs.com/package/@livekit/rtc-node), que está atualmente em
> Developer Preview. Para implantações em produção, prefira o worker em Python.

## Configure os formatos de áudio do Speech Engine

O `AudioStream` do LiveKit reamostra faixas Opus recebidas para qualquer taxa de amostragem PCM que você solicitar, então é possível corresponder diretamente à entrada do Speech Engine. Atualize o Speech Engine para aceitar PCM de 16 kHz como entrada de ASR e gerar PCM de 24 kHz como saída de 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" },
});
```

O PCM do Speech Engine é sempre assinado, de 16 bits e little-endian. Consulte a [referência de formatos de áudio](#audio-format-reference) para ver outras taxas compatíveis.

## Crie o worker de ponte

O worker é um processo de longa execução que se conecta ao seu servidor LiveKit, aguarda tarefas, entra nas salas atribuídas e faz a ponte de áudio entre a sala e o Speech Engine.

#### Instale as dependências

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

#### Gere uma URL assinada do Speech Engine

O worker solicita uma URL assinada de curta duração para o WebSocket de conversa do Speech Engine. A URL assinada inclui o ID do mecanismo e uma assinatura de uso único, para que o worker possa abrir o WebSocket sem expor sua chave de 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;
}
```

#### Defina o ponto de entrada do worker

Sempre que o worker é enviado a uma sala, seu ponto de entrada é executado. O ponto de entrada se conecta à sala, abre um WebSocket de conversa do Speech Engine e inicia duas pontes de áudio: uma para o áudio do interlocutor que vai para o Speech Engine e outra para o áudio sintetizado que retorna.

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

O worker filtra o próprio áudio publicado no manipulador `track_subscribed`, comparando-o à identidade do participante local. Sem essa verificação, o worker tentaria enviar seu próprio áudio sintetizado de volta ao Speech Engine.

Dois detalhes de ordem são importantes para o funcionamento correto:

* **Momento do listener**: `TrackSubscribed` é registrado antes de `ctx.connect()`. O LiveKit assina automaticamente as faixas existentes durante o handshake de conexão, e um listener registrado depois pode não receber o evento. A bomba de áudio aguarda um `Future` / `Promise` pelo WebSocket do Speech Engine para poder assinar imediatamente e encaminhar o áudio assim que a conexão é aberta.
* **Somente TypeScript — serialização de captura**: o `AudioSource.captureFrame` de `@livekit/rtc-node` lança `InvalidState` se for chamado simultaneamente. O manipulador TypeScript serializa as capturas com uma cadeia de promises. O loop único `async for el_to_room` do Python é naturalmente sequencial e não precisa disso.

#### Inicie o worker

**`Python`**

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

**`Node`**

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

`dev` ativa a recarga automática e logs coloridos. Use `start` em produção para logs JSON e encerramento seguro.

O worker se conecta ao seu servidor LiveKit e aguarda atribuições de tarefas. Ele não entra em nenhuma sala até ser enviado a uma.

## Envie o worker para uma sala

Como o worker tem um `agent_name`, ele usa envio explícito — só entra em salas quando seu backend solicita. O padrão mais simples é incluir um `RoomAgentDispatch` no token de acesso do LiveKit usado pelo navegador para se conectar.

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

Quando um navegador usa esse token para criar ou entrar em uma sala, o LiveKit envia automaticamente o worker de ponte para a mesma sala.

## Conecte-se pelo navegador

O navegador só precisa do cliente padrão do LiveKit — ele não interage diretamente com o 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>;
}
```

Quando o botão é clicado, o navegador busca um token do LiveKit, entra na sala com o microfone ativado e começa a receber a faixa de áudio do agente. O worker é enviado, abre sua sessão do Speech Engine e faz a ponte de áudio nas duas direções.

## Referência de formatos de áudio

O Speech Engine é compatível com os seguintes formatos de áudio. Configure-os no mecanismo usando `asr.user_input_audio_format` e `tts.agent_output_audio_format`.

| Formato     | Taxa de amostragem | Codificação                | Observações                                                      |
| ----------- | ------------------ | -------------------------- | ---------------------------------------------------------------- |
| `pcm_8000`  | 8 kHz              | PCM LE assinado de 16 bits | Somente entrada de ASR.                                          |
| `pcm_16000` | 16 kHz             | PCM LE assinado de 16 bits | Recomendado para entrada de usuário do LiveKit.                  |
| `pcm_22050` | 22,05 kHz          | PCM LE assinado de 16 bits |                                                                  |
| `pcm_24000` | 24 kHz             | PCM LE assinado de 16 bits | Recomendado para saída de agente do LiveKit.                     |
| `pcm_44100` | 44,1 kHz           | PCM LE assinado de 16 bits | A saída de TTS requer o nível Independent Publisher ou superior. |
| `pcm_48000` | 48 kHz             | PCM LE assinado de 16 bits | Somente entrada de ASR.                                          |
| `ulaw_8000` | 8 kHz              | μ-law                      | Usado pelo Twilio Media Streams.                                 |

`AudioStream` e `AudioSource` no LiveKit fazem a reamostragem para você — é possível solicitar qualquer taxa de amostragem ao `AudioStream`, e o SDK converte a partir da faixa Opus subjacente de 48 kHz.

## Considerações para produção

* **Despacho explícito**: Sempre defina `agent_name` / `agentName` em `WorkerOptions`. O despacho automático aciona o worker para cada sala criada no seu projeto LiveKit, o que raramente é o desejado.
* **Autenticação do servidor de raciocínio**: Defina um segredo compartilhado no Speech Engine e verifique-o no seu servidor de raciocínio para que apenas o Speech Engine possa acessar seu endpoint:
  ```python
  await elevenlabs.speech_engine.update(
      speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
      speech_engine={"request_headers": {"x-api-key": os.environ["SHARED_SECRET"]}},
  )
  ```
  Em seguida, o servidor de raciocínio verifica `request.headers["x-api-key"]` antes de aceitar a atualização do WebSocket.
* **Servidor de tokens**: Gere tokens do LiveKit e do Speech Engine no servidor. Nunca exponha `LIVEKIT_API_SECRET` nem `ELEVENLABS_API_KEY` no navegador.
* **Higiene do loop de eventos**: Mantenha tarefas que exigem CPU fora do loop de eventos do worker. A iteração de `AudioSource.capture_frame` e `AudioStream` é sensível ao tempo; chamadas síncronas longas atrasarão ou descartarão eventos de interrupção. Use `asyncio.to_thread()` (Python) ou `worker_threads` (Node) para tarefas bloqueantes.
* **Encerramento**: Registre `ctx.add_shutdown_callback` / `ctx.addShutdownCallback` para fechar corretamente o WebSocket da ElevenLabs. Por padrão, a sala (e o trabalho) é encerrada quando o último participante que não é agente sai.

## Próximas etapas

#### [Guia rápido do Speech Engine](/docs/pt/eleven-api/guides/cookbooks/speech-engine)

Crie o servidor de raciocínio que responde às transcrições.

#### [Integração com Pipecat](/docs/pt/eleven-api/guides/how-to/speech-engine/pipecat-integration)

Use o Pipecat como pipeline de LLM por trás do Speech Engine.

#### [Referência do SDK Python](/docs/pt/eleven-api/resources/libraries/speech-engine/python-sdk-reference)

Classes, métodos e eventos do SDK Python do Speech Engine.

#### [Referência do SDK JavaScript](/docs/pt/eleven-api/resources/libraries/speech-engine/javascript-sdk-reference)

Classes, métodos e eventos do SDK JavaScript do Speech Engine.