Aller au contenu principal

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é​

ClientPrérequis
TypeScript @enterprise-ai/realtimeUn 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
Pythonl'extra realtime de la wheel récupérée dans Harbor (websockets, livekit-api), asyncio
Gole paquet realtime du SDK Go (WebSocket de golang.org/x/net)

Installation​

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

    Ré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​

  1. Votre backend crée la session. Il appelle POST /v1/realtime/sessions avec 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.
  2. 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}.
  3. Les événements circulent sur le canal de données ou sur le WebSocket (tableau ci-dessous).
  4. La session se termine. POST /v1/realtime/sessions/{id}/end la 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) :

NomTypeDéfautDescription
baseUrlstringaucun, obligatoireURL du Control Plane.
apiKeystringaucunClé API, pour un client côté serveur uniquement.
tokenstring | (() => string | Promise<string>)aucunJeton OIDC ou fournisseur.
tenant, project, environmentstringaucunEn-têtes de tenancy, comme dans le SDK.
locale"en" | "fr""en"Langue des messages d'erreur.
fetchFetchLikefetch globalAppels HTTP (session, jeton, fin).
WebSocketWebSocketCtorglobalThis.WebSocketImplémentation du WebSocket ; passez ws sur Node.js 18 et 20.
webSocketOptionsRecord<string, unknown>aucunTroisiè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.
maxQueuenumber500Événements sortants mis en file pendant une déconnexion.
now() => numberDate.nowHorloge 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énementSensContenu
session.updateclient vers serveurinstructions, voix, vad, langue
input_audio.appendclient vers serveuraudio (PCM16 20 ms, base64) sur WebSocket
transcript.deltaserveur vers clienttexte, final, locuteur
response.audio.deltaserveur vers clientmorceau d'audio
usage.updateserveur vers clientaudio_seconds, input_tokens, output_tokens, audio_tokens, coût estimé
errorserveur vers clientcode, message, message_i18n
response.createclient vers serveur{text, voice?} : le serveur prononce le texte (rt.createResponse(text, voice))
response.cancelclient vers serveurarrête la réponse en cours (rt.cancelResponse()), erreur no_active_response si aucune ne tourne
input_audio.speech_startedclient vers serveurinterruption depuis un VAD client (rt.speechStarted()) ; tout input_audio.append pendant une réponse l'annule aussi
input_audio.commitclient vers serveurclôt le tour de l'utilisateur (rt.commitAudio()), enregistre l'audio quand config.record est posé
response.createdserveur vers clientturn_id, trace_id
response.doneserveur vers clientturn_id, chunks_sent, latency_ms {tts_ms, first_audio_ms, total_ms}
response.cancelledserveur vers clientturn_id, reason (barge_in ou client_cancel), chunks_sent
recording.savedserveur vers clientrecording_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énements error du serveur portent code, message et message_i18n.
  • quota_exceeded pendant un appel : un usage.update d'avertissement à 90 % de la réservation, puis error et une fin propre.
  • Changement de réseau : reconnexion avec le même identifiant de session et un nouveau ws_token tant que expires_at n'est pas atteint, selon la politique reconnect ; les événements sortants attendent dans une file de maxQueue é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​