Skip to navigation

LiveKit-integratie

Koppel een LiveKit-room aan Speech Engine met een LiveKit Agents-worker.

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.

loop [Conversation] Join room (LiveKit token) Join room (dispatched) Open conversation WebSocket (signed URL) Microphone audio (Opus) Decoded PCM frames user_audio_chunk (base64 PCM) user_transcript agent_response (streamed) audio (base64 PCM) Publish PCM frames Audio (Opus) Browser LiveKit Room Agents Worker ElevenLabs (conversation WS) Brain Server

De brain-server is ongewijzigd ten opzichte van de snelle start voor 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 eenvoudiger — Speech Engine spreekt rechtstreeks WebRTC met de browser, zonder LiveKit-room.

Vereisten

  • Een LiveKit-project (LiveKit Cloud 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 om er een te maken en de brain-server uit te voeren.
  • Python 3.9+ of Node.js 18+.

De Node bridge-worker gebruikt @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.

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())

De PCM van Speech Engine is overal signed 16-bit little-endian. Raadpleeg de referentie voor audio-indelingen 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.

1

Afhankelijkheden installeren

pip install "livekit-agents" "livekit-api" "elevenlabs" "aiohttp" "python-dotenv"
2

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.

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
3

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.

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",
))

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

De worker starten

python bridge.py 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.

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)

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

IndelingSamplerateEncodingOpmerkingen
pcm_80008 kHzSigned 16-bit LE PCMAlleen ASR-invoer.
pcm_1600016 kHzSigned 16-bit LE PCMAanbevolen voor LiveKit-gebruikersinvoer.
pcm_2205022,05 kHzSigned 16-bit LE PCM
pcm_2400024 kHzSigned 16-bit LE PCMAanbevolen voor LiveKit-agentuitvoer.
pcm_4410044,1 kHzSigned 16-bit LE PCMTTS-uitvoer vereist Independent Publisher-tier of hoger.
pcm_4800048 kHzSigned 16-bit LE PCMAlleen ASR-invoer.
ulaw_80008 kHzμ-lawGebruikt 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:
    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