SDK de voix en temps réel
Ce guide montre comment ouvrir une session vocale en direct (brique F-08) depuis un navigateur ou un serveur,
diffuser de l'audio, lire les transcriptions et les réponses parlées, puis fermer la session. Le client TypeScript
est le paquet @enterprise-ai/realtime (WebRTC par LiveKit avec repli sur le protocole WebSocket, navigateur et
Node.js) ; le SDK Python (enterprise-ai[realtime]) et le paquet Go realtime exposent le même modèle
d'événements. Source : sdks/typescript/packages/realtime/src.
English version: realtime.md
Vue d'ensemble
Une session vit sur le Control Plane : elle porte le pipeline (echo ou stt), l'agent, la langue, une réservation
de quota et une durée de vie. Les clients se connectent avec un ws_token court à usage unique, jamais avec la clé
API, et échangent des événements JSON avec le serveur.
Prérequis et compatibilité
| Client | Prérequis |
|---|---|
TypeScript @enterprise-ai/realtime | Un WebSocket standard (navigateurs, Node.js 22+), ou le paquet ws passé en WebSocket sur Node.js 18 et 20 ; dépendance paire optionnelle livekit-client 2 ou plus pour WebRTC |
| Python | l'extra realtime de la wheel récupérée dans Harbor (websockets, livekit-api), asyncio |
| Go | le paquet realtime du SDK Go (WebSocket de golang.org/x/net) |
Installation
-
Installez les paquets depuis les archives récupérées dans Harbor (voir le guide TypeScript).
npm install ./sdk-typescript/enterprise-ai-sdk-1.0.2.tgz ./sdk-typescript/enterprise-ai-realtime-1.0.2.tgzRésultat attendu : les deux paquets figurent dans
package.json. Les utilisateurs de Python et de Go installent le SDK de leur langage (Python, Go).
Cycle de vie d'une session
- Votre backend crée la session. Il appelle
POST /v1/realtime/sessionsavec sa clé API et renvoie{session_id, ws_url, ws_token, livekit: {url, token, room, configured}}au navigateur. La clé API ne quitte jamais le serveur. - Le navigateur se connecte avec le jeton de courte durée.
RealtimeClient.connect(session)ouvre WebRTC quand LiveKit est configuré, sinon le WebSocket/v1/realtime/ws/{session_id}. - Les événements circulent sur le canal de données ou sur le WebSocket (tableau ci-dessous).
- La session se termine.
POST /v1/realtime/sessions/{id}/endla ferme et confirme la réservation de quota.
// server
import { EnterpriseAI } from "@enterprise-ai/sdk";
const client = new EnterpriseAI({ baseUrl: "<control-plane-url>", apiKey: process.env.EAI_API_KEY, locale: "en" });
const session = await client.realtime.createSession({ pipeline: "stt", ttl_seconds: 900, config: { agent_id: "agt_support_voice", language: "fr" } });
// browser
import { RealtimeClient } from "@enterprise-ai/realtime";
const rtClient = new RealtimeClient({ baseUrl: "<control-plane-url>", locale: "fr" });
const rt = await rtClient.connect(session);
rt.on("transcript.delta", (e) => ui.caption(e.delta, e.final));
rt.on("response.audio.delta", () => ui.speaking(true));
rt.on("usage.update", (e) => ui.budget(e.usage));
rt.on("error", (e) => ui.error(e.error.message));
rt.updateSession({ instructions: "Réponds en français", voice: "fr_FR-siwis-medium" }); // session.update
rt.close(1000, "user_hangup");
Configuration du client
new RealtimeClient(options: RealtimeClientOptions) :
| Nom | Type | Défaut | Description |
|---|---|---|---|
baseUrl | string | aucun, obligatoire | URL du Control Plane. |
apiKey | string | aucun | Clé API, pour un client côté serveur uniquement. |
token | string | (() => string | Promise<string>) | aucun | Jeton OIDC ou fournisseur. |
tenant, project, environment | string | aucun | En-têtes de tenancy, comme dans le SDK. |
locale | "en" | "fr" | "en" | Langue des messages d'erreur. |
fetch | FetchLike | fetch global | Appels HTTP (session, jeton, fin). |
WebSocket | WebSocketCtor | globalThis.WebSocket | Implémentation du WebSocket ; passez ws sur Node.js 18 et 20. |
webSocketOptions | Record<string, unknown> | aucun | Troisième argument du constructeur du WebSocket (par exemple { headers } avec ws). |
reconnect | { enabled?, maxAttempts?, baseDelayMs?, maxDelayMs? } | { enabled: true, maxAttempts: 5, baseDelayMs: 500, maxDelayMs: 10000 } | Politique de reconnexion après une perte du réseau. |
maxQueue | number | 500 | Événements sortants mis en file pendant une déconnexion. |
now | () => number | Date.now | Horloge des mesures de latence, injectable dans les tests. |
RealtimeClient a aussi createSession, getSession, endSession, refreshToken, wsUrl et start (créer
puis connecter). Une RealtimeConnection a send, updateSession, appendAudio, commitAudio, createResponse,
cancelResponse, speechStarted, metrics et close.
Événements
| Événement | Sens | Contenu |
|---|---|---|
session.update | client vers serveur | instructions, voix, vad, langue |
input_audio.append | client vers serveur | audio (PCM16 20 ms, base64) sur WebSocket |
transcript.delta | serveur vers client | texte, final, locuteur |
response.audio.delta | serveur vers client | morceau d'audio |
usage.update | serveur vers client | audio_seconds, input_tokens, output_tokens, audio_tokens, coût estimé |
error | serveur vers client | code, message, message_i18n |
response.create | client vers serveur | {text, voice?} : le serveur prononce le texte (rt.createResponse(text, voice)) |
response.cancel | client vers serveur | arrête la réponse en cours (rt.cancelResponse()), erreur no_active_response si aucune ne tourne |
input_audio.speech_started | client vers serveur | interruption depuis un VAD client (rt.speechStarted()) ; tout input_audio.append pendant une réponse l'annule aussi |
input_audio.commit | client vers serveur | clôt le tour de l'utilisateur (rt.commitAudio()), enregistre l'audio quand config.record est posé |
response.created | serveur vers client | turn_id, trace_id |
response.done | serveur vers client | turn_id, chunks_sent, latency_ms {tts_ms, first_audio_ms, total_ms} |
response.cancelled | serveur vers client | turn_id, reason (barge_in ou client_cancel), chunks_sent |
recording.saved | serveur vers client | recording_id, sha256, bytes, expires_at |
rt.on("response.created", (e) => ui.turn(e.turn_id));
rt.on("response.cancelled", (e) => ui.stopSpeaking(e.reason));
rt.on("response.done", (e) => metrics.record(e.latency_ms));
rt.on("recording.saved", (e) => ui.saved(e.recording_id));
rt.createResponse("Your order ships tomorrow.", "fr_FR-siwis-medium");
mic.onSpeech(() => rt.speechStarted()); // barge-in
La transcription et l'enregistrement exigent un consentement actif : enregistrez-le avec
client.realtime.recordConsent(sessionId, {purpose, retention_days, recording_allowed, transcription_allowed}),
sinon le serveur répond par un événement error de code consent_required. client.realtime.turns(id) et
client.realtime.transcripts(id) lisent les tours de parole et les transcriptions filtrées par la DLP.
Erreurs et résilience
- Les erreurs des appels HTTP et de la connexion sont des
RealtimeError(code,status,details), avec des messages dans la langue du client ; les événementserrordu serveur portentcode,messageetmessage_i18n. quota_exceededpendant un appel : unusage.updated'avertissement à 90 % de la réservation, puiserroret une fin propre.- Changement de réseau : reconnexion avec le même identifiant de session et un nouveau
ws_tokentant queexpires_atn'est pas atteint, selon la politiquereconnect; les événements sortants attendent dans une file demaxQueueévénements. - WebRTC bloqué : TURN sur TLS 443, puis repli sur WebSocket.
Python et Go
Python : client.realtime.connect(session) est un gestionnaire de contexte asynchrone ; events() produit les
événements du serveur et lève RealtimeError sur un événement error sauf avec raise_on_error=False.
session = client.realtime.create_session("stt", config={"agent_id": "agt_support_voice", "language": "fr"})
async with client.realtime.connect(session) as s:
await s.append_audio(pcm16_frame)
async for ev in s.events():
if ev.type == "transcript.delta":
print(ev)
Go : realtime.Dial demande un nouveau jeton à usage unique, puis ouvre le WebSocket ; Recv renvoie l'Event
suivant (Type, Raw) et Close termine la connexion.
sess, err := realtime.Dial(ctx, client, sessionID)
defer sess.Close()
Tests
Injectez les transports : fetch et WebSocket en TypeScript (une fausse classe de socket suffit), un transport
httpx simulé et un connector en Python, un serveur httptest en Go. Les tests du paquet
sdks/typescript/packages/realtime/test/realtime.test.ts montrent un faux WebSocket complet.
FinOps
Type d'usage voice_live : audio_seconds, audio_tokens, jetons texte en entrée et en sortie, sessions, duration_ms.
Voir F-08.
Sécurité
wsUrl ne renvoie que des URL wss pour les hôtes non locaux (ws est permis pour localhost et 127.0.0.1), et
un ws_url fourni par le serveur doit rester sur l'hôte de l'URL de base. La clé API reste sur le serveur : le
navigateur ne détient que le jeton à usage unique d'une session.
Référence
- Référence générée : SDK TypeScript (
client.realtime), Python, Go. - SDK TypeScript, SDK Python, SDK Go, SDK Java.