Hoppa till navigering

LiveKit-integration

Koppla ett LiveKit-rum till Speech Engine med en LiveKit Agents-arbetare.

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.

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

Brain-servern är oförändrad från snabbstarten för 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 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 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 för att skapa en och köra brain-servern.
  • Python 3.9+ eller Node.js 18+.

Node-bryggarbetaren använder @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.

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

PCM i Speech Engine är genomgående signerad 16-bitars little-endian. Se referensen för ljudformat 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.

1

Installera beroenden

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

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.

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

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.

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

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

Starta arbetaren

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

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)

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

FormatSamplingsfrekvensKodningKommentarer
pcm_80008 kHzSignerad 16-bitars LE PCMEndast ASR-indata.
pcm_1600016 kHzSignerad 16-bitars LE PCMRekommenderas för LiveKit-användarindata.
pcm_2205022,05 kHzSignerad 16-bitars LE PCM
pcm_2400024 kHzSignerad 16-bitars LE PCMRekommenderas för LiveKit-agentutdata.
pcm_4410044,1 kHzSignerad 16-bitars LE PCMTTS-utdata kräver nivån Independent Publisher eller högre.
pcm_4800048 kHzSignerad 16-bitars LE PCMEndast ASR-indata.
ulaw_80008 kHzμ-lawAnvä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:
    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