Aller au contenu principal

F-10 Streaming audio

Brique F de la plateforme Enterprise AI. Technologie : pistes LiveKit et services audio à faible latence (Opus, trames PCM16 de 20 ms) entre clients, STT, agents et TTS. Version anglaise : ../../en/bricks/F-10-audio-streaming.md.

Rôle et responsabilités​

  • Transporter les trames audio avec tampons de gigue, rééchantillonnage et négociation de codec.
  • Exposer un protocole WebSocket pour les clients sans WebRTC (serveurs, passerelles téléphoniques).

Position et interactions​

  • Chemin d'exécution : transport temps réel.
  • Événements : session.update, input_audio.append, transcript.delta, response.audio.delta, usage.update, error.

Part open source et part développée​

  • Open source réutilisé : LiveKit, Opus, passerelle SIP LiveKit pour la téléphonie.
  • Développé dans la plateforme : Versionnement du protocole d'événements, contre-pression, service de rééchantillonnage.

Modèle de données​

Tables du Control Plane (schéma eai) : eai.realtime_sessions, eai.telemetry_spans.

Chaque table de tenant a tenant_id uuid NOT NULL et une politique de sécurité au niveau des lignes sur current_setting('app.tenant_id', true)::uuid. ClickHouse usage_events est la réplique analytique de eai.usage_events (ReplacingMergeTree par event_id).

Contrat : contracts/v1/multimodal-contract.schema.json (schema_version 1.0). L'usage est toujours remonté avec contracts/v1/usage-event.schema.json, l'audit avec contracts/v1/audit-event.schema.json.

Endpoints d'API​

MéthodeChemin
WebSocket/v1/realtime/ws/{session_id}
POST/v1/realtime/sessions

WebSocket /v1/realtime/ws/{session_id}​

Requête :

{
"type": "input_audio.append",
"audio": "<base64 pcm16 20ms>"
}

Réponse :

{
"type": "transcript.delta",
"text": "Bonjour",
"final": false
}

POST /v1/realtime/sessions​

Requête :

{
"room": "support-voice-42",
"pipeline": "stt",
"ttl_seconds": 900,
"config": {
"language": "fr",
"sample_rate": 16000,
"dlp_mode": "mask"
}
}

Réponse :

{
"session_id": "rts_3K",
"livekit_url": "wss://<livekit-host>",
"token": "eyJhbGciOi...",
"ws_url": "wss://<control-plane-host>/v1/realtime/ws/rts_3K",
"expires_at": "2026-10-29T10:15:00Z"
}

Exemples SDK​

Python​

from enterprise_ai import EnterpriseAI

client = EnterpriseAI(base_url="<control-plane-url>", api_key="eai_live_xxx", tenant="acme", locale="fr")
session = client.realtime.create_session("stt", config={"language": "fr"})

TypeScript​

import { EnterpriseAI } from "@enterprise-ai/sdk";

const client = new EnterpriseAI({ baseUrl: "<control-plane-url>", apiKey: process.env.EAI_API_KEY, tenant: "acme", locale: "fr" });
const session = await client.realtime.createSession({ pipeline: "stt", config: { language: "fr" } });

Java​

EnterpriseAIClient client = EnterpriseAIClient.builder()
.baseUrl("<control-plane-url>")
.apiKey(System.getenv("EAI_API_KEY"))
.tenant("acme")
.locale(Locale.FRENCH)
.build();
JsonNode session = client.realtime().createSession(Map.of("pipeline", "stt", "config", Map.of("language", "fr")));

Go​

client, err := eai.NewClient(eai.WithBaseURL("<control-plane-url>"), eai.WithAPIKey(os.Getenv("EAI_API_KEY")), eai.WithTenant("acme"), eai.WithLocale("fr"))
if err != nil { log.Fatal(err) }
ctx := context.Background()
_, err = client.Realtime.CreateSession(ctx, map[string]any{"pipeline": "stt", "config": map[string]any{"language": "fr"}})

Client temps réel TypeScript (@enterprise-ai/realtime)​

import { EnterpriseAI } from "@enterprise-ai/sdk";
import { RealtimeClient } from "@enterprise-ai/realtime";

const client = new EnterpriseAI({ baseUrl: "<control-plane-url>", apiKey: process.env.EAI_API_KEY, tenant: "acme", locale: "fr" });
const rtClient = new RealtimeClient({ baseUrl: "<control-plane-url>", apiKey: process.env.EAI_API_KEY, tenant: "acme", locale: "fr" });
const s = await rtClient.createSession({ pipeline: "stt" });
const rt = await rtClient.connect(s, { language: "fr", turn_detection: { type: "server_vad" } });
rt.on("transcript.delta", (e) => render(e.delta));
rt.on("usage.update", (e) => showBudget(e.usage));
rt.on("error", (e) => console.error(e.error.code, e.error.message));
rt.appendAudio(pcm20ms, 16000);
rt.close();
await rtClient.endSession(s.session_id);

Package Go realtime​

raw, err := client.Realtime.CreateSession(ctx, map[string]any{"pipeline": "stt"})
if err != nil { return err }
var s struct{ SessionID string `json:"session_id"` }
if err := json.Unmarshal(raw, &s); err != nil { return err }
conn, err := realtime.Dial(ctx, client, s.SessionID)
if err != nil { return err }
defer conn.Close()
_ = conn.UpdateSession(map[string]any{"language": "fr"})
_ = conn.AppendAudio(pcm20ms)
for {
ev, err := conn.Recv()
if err != nil { return err }
if ev.Type == realtime.TranscriptDelta {
var d struct{ Delta string `json:"delta"` }
_ = ev.Decode(&d)
log.Println(d.Delta)
}
}

Configuration​

Variables d'environnement : EAI_LIVEKIT_URL, EAI_LIVEKIT_API_KEY, EAI_LIVEKIT_API_SECRET.

Clés de valeurs Helm (deploy/helm/enterprise-ai/values.yaml) : global.networkPolicy.

Sécurité​

  • Le WebSocket exige le jeton de session ; contrôle d'origine pour les navigateurs.
  • Tenant déduit de l'identifiant d'accès, jamais du corps ; sécurité au niveau des lignes PostgreSQL sur chaque table de tenant.
  • Chaque mutation et décision de politique écrit un événement d'audit chaîné par hachage (G-11).
  • Les erreurs utilisent l'enveloppe standard avec message_i18n en et fr.

Observabilité​

  • Trames perdues, sous-alimentation des tampons, latence de bout en bout par saut.
  • traceparent W3C propagé, X-Request-Id renvoyé, les spans portent eai.tenant_id, eai.project, eai.agent_id, eai.run_id.

Unités FinOps​

  • Compté dans voice_live (duration_ms, bytes_transferred).

Modes de défaillance et résilience​

  • Client trop lent -> le serveur supprime les trames les plus anciennes et envoie un événement error avec le code backpressure.

Adoption autonome​

Les intégrateurs téléphonie n'utilisent que le protocole WebSocket avec leur pile SIP.

Tests d'acceptation​

  1. Aller-retour de 1000 trames sans perte sur un réseau propre.
  2. Une version de protocole incompatible renvoie un événement error, pas une fermeture silencieuse.

Feuille de route d'implémentation

Preuves vocales du kit L4​

Audio de réponse diffusé en morceaux response.audio.delta de 100 ms au rythme du temps réel, annulé proprement par input_audio.speech_started ou response.cancel (MULT-010). Voir le guide du runtime vocal.