📍 Guide principal de la série : Agents vocaux IA en 2026 : architecture, modèles, latence.
L’API Realtime d’OpenAI est passée en disponibilité générale en août 2025 avec un nouveau modèle, gpt-realtime, qui consomme et produit directement de l’audio sans repasser par le texte. Trois transports sont supportés : WebSocket pour l’arrière-plan serveur, WebRTC pour les apps web et mobiles, SIP pour la téléphonie traditionnelle. Ce tutoriel construit une session Python en WebSocket d’abord (la voie la plus simple pour expérimenter), puis bascule en WebRTC côté navigateur, et termine sur l’intégration dans LiveKit Agents en mode MultimodalAgent.
Prérequis
- Python 3.10+ avec un environnement virtuel
- Compte OpenAI avec accès à l’API Realtime activé
- Clé API avec scope Realtime, Chat completions
- Sortie/entrée audio fonctionnelles
- Pour la partie navigateur : un serveur HTTPS local (Caddy, mkcert)
- Niveau attendu : Python avancé, asyncio, JS intermédiaire
- Temps estimé : 90 minutes
Étape 1 — Choisir entre WebSocket, WebRTC et SIP
Les trois transports n’adressent pas les mêmes besoins. WebSocket est le plus simple à mettre en œuvre côté serveur : on garde un canal ouvert et on push/pull du JSON contenant les chunks audio en base64. WebRTC est obligatoire dès qu’on veut un client navigateur ou mobile : il gère le codec Opus, le NAT traversal, le jitter buffer, et descend la latence réseau de 100 à 200 ms. SIP est pour la téléphonie : un trunk SIP entrant peut désormais déboucher directement sur une session Realtime sans serveur intermédiaire.
| Transport | Cas d’usage | Endpoint | Latence ajoutée |
|---|---|---|---|
| WebSocket | Backend, prototype, agent server-to-server | wss://api.openai.com/v1/realtime |
~50 ms |
| WebRTC | Apps web/mobile, voix navigateur | https://api.openai.com/v1/realtime/calls |
~30 ms |
| SIP | Téléphonie classique | SIP URI dédié | ~80-150 ms |
Pour démarrer, le WebSocket est le bon point d’entrée : on contrôle tout le code en Python, on voit chaque message du protocole, et on peut basculer ensuite vers WebRTC une fois la logique métier maîtrisée. C’est le chemin que suit le reste du tutoriel.
Étape 2 — Initialiser la connexion WebSocket
L’API Realtime expose un protocole événementiel basé sur JSON, où chaque message a un type. La connexion s’authentifie par header Authorization et requiert le header OpenAI-Beta: realtime=v1. Le modèle est sélectionné via le query parameter ?model=gpt-realtime.
# realtime_ws.py
import asyncio, json, os, base64
import websockets
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.environ["OPENAI_API_KEY"]
URI = "wss://api.openai.com/v1/realtime?model=gpt-realtime"
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"OpenAI-Beta": "realtime=v1",
}
async def main():
async with websockets.connect(URI, additional_headers=HEADERS) as ws # websockets >= 14 ; pour < 14 utiliser extra_headers:
await ws.send(json.dumps({
"type": "session.update",
"session": {
"modalities": ["audio", "text"],
"instructions": (
"Tu es Aïda, assistante vocale francophone. "
"Tu réponds en deux phrases maximum, ton chaleureux."
),
"voice": "alloy",
"input_audio_format": "pcm16",
"output_audio_format": "pcm16",
"turn_detection": {"type": "server_vad", "threshold": 0.5,
"prefix_padding_ms": 300,
"silence_duration_ms": 500},
},
}))
async for raw in ws:
event = json.loads(raw)
print(event["type"])
asyncio.run(main())
Plusieurs points méritent l'attention. modalities active simultanément l'audio et le texte (utile pour récupérer un transcript en parallèle). turn_detection en mode server_vad laisse OpenAI détecter la fin de tour ; on peut basculer sur none pour gérer la détection côté client si on a déjà un VAD performant. À l'exécution, on doit voir au moins les événements session.created puis session.updated arriver dans la console.
Étape 3 — Envoyer de l'audio et recevoir la réponse
L'audio entrant se push avec des messages input_audio_buffer.append contenant des chunks PCM16 16 kHz encodés en base64. La réponse arrive en streaming via une cascade d'événements response.audio.delta.
import sounddevice as sd
import numpy as np
SAMPLE_RATE = 16000
async def push_microphone(ws):
loop = asyncio.get_running_loop()
q = asyncio.Queue()
def cb(indata, frames, t, status):
loop.call_soon_threadsafe(q.put_nowait, indata.copy())
with sd.InputStream(channels=1, samplerate=SAMPLE_RATE,
dtype="int16", callback=cb):
while True:
chunk = await q.get()
await ws.send(json.dumps({
"type": "input_audio_buffer.append",
"audio": base64.b64encode(chunk.tobytes()).decode(),
}))
async def play_responses(ws):
out = sd.OutputStream(channels=1, samplerate=24000, dtype="int16")
out.start()
async for raw in ws:
ev = json.loads(raw)
if ev["type"] == "response.audio.delta":
pcm = base64.b64decode(ev["delta"])
out.write(np.frombuffer(pcm, dtype="int16"))
Trois subtilités. L'audio entrant est attendu en 16 kHz mono PCM16, l'audio sortant arrive en 24 kHz mono PCM16 — il faut donc deux SoundDevice distincts. Côté streaming, le premier response.audio.delta arrive typiquement entre 200 et 350 ms après la fin de la phrase utilisateur, ce qui correspond à la latence speech-to-speech annoncée par OpenAI. La détection de fin de tour est gérée par le serveur (server_vad) ; en cas de VAD trop nerveux, augmenter silence_duration_ms à 700-800.
Étape 4 — Activer le function calling temps réel
L'agent vocal n'est utile que s'il peut interagir avec le système d'information. L'API Realtime supporte le function calling exactement comme l'API Chat Completions : on déclare les outils dans session.update et le modèle émet un événement response.function_call_arguments.done quand il décide d'appeler une fonction.
tools = [{
"type": "function",
"name": "get_order_status",
"description": "Retourne le statut d'une commande client.",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
}]
await ws.send(json.dumps({
"type": "session.update",
"session": {"tools": tools, "tool_choice": "auto"},
}))
# côté boucle de réception :
if ev["type"] == "response.function_call_arguments.done":
args = json.loads(ev["arguments"])
result = my_business_lookup(args["order_id"])
await ws.send(json.dumps({
"type": "conversation.item.create",
"item": {
"type": "function_call_output",
"call_id": ev["call_id"],
"output": json.dumps(result),
},
}))
await ws.send(json.dumps({"type": "response.create"}))
Trois lignes méritent attention. tool_choice: "auto" laisse le modèle décider quand appeler la fonction. Après avoir injecté le résultat, il faut envoyer un response.create pour relancer le modèle, sinon il reste en attente. Le résultat de fonction est traité comme du texte : pour des données sensibles ou volumineuses, le résumé en amont reste de la responsabilité du code côté serveur.
Étape 5 — Gérer les interruptions naturelles
Quand l'utilisateur interrompt l'agent en plein milieu d'une réponse, la session doit immédiatement couper l'audio sortant et redémarrer un nouveau cycle d'écoute. L'API Realtime expose un événement response.cancel et le serveur détecte automatiquement les interruptions si turn_detection est en mode server_vad.
async for raw in ws:
ev = json.loads(raw)
if ev["type"] == "input_audio_buffer.speech_started":
# l'utilisateur reprend la parole, on coupe la sortie
await ws.send(json.dumps({"type": "response.cancel"}))
out.abort()
out = sd.OutputStream(channels=1, samplerate=24000, dtype="int16")
out.start()
L'événement speech_started est émis dès que le VAD serveur détecte la reprise de parole. response.cancel stoppe la génération côté API, ce qui évite de continuer à facturer des tokens audio inutiles. Côté client, on coupe la sortie audio en cours et on redémarre le stream — sinon la queue de samples bufferisée continue d'être lue après l'interruption, ce qui produit l'effet désagréable de l'agent qui termine sa phrase malgré tout.
Étape 6 — Surveiller les coûts et la latence
L'API émet des événements response.done qui contiennent un objet usage avec le détail des tokens consommés. C'est la seule source fiable pour calculer le coût réel d'une session, puisque le tarif au token et la quantité varient avec la longueur des réponses et l'activation du cache.
if ev["type"] == "response.done":
u = ev["response"]["usage"]
audio_in = u["input_token_details"]["audio_tokens"]
audio_in_c = u["input_token_details"].get("cached_tokens", 0)
audio_out = u["output_token_details"]["audio_tokens"]
cost = (
(audio_in - audio_in_c) * 32 / 1_000_000 +
audio_in_c * 0.40 / 1_000_000 +
audio_out * 64 / 1_000_000
)
print(f"Tokens audio in/out: {audio_in}/{audio_out} → {cost*100:.3f} ¢")
Le tarif officiel publié au lancement de gpt-realtime est de 32 USD par million de tokens audio en entrée, 0,40 USD par million pour les tokens audio cachés, 64 USD par million en sortie — soit une réduction de 20 % par rapport au précédent gpt-4o-realtime-preview. En production, activer le cache sur le system prompt (qui ne change pas d'une session à l'autre) divise typiquement la facture en entrée par dix.
Étape 7 — Passer en WebRTC depuis le navigateur
Pour une app web, on n'utilise pas le WebSocket : on ouvre une RTCPeerConnection côté navigateur et on POST le SDP offer à /v1/realtime/calls. Le serveur répond avec un SDP answer et la session est établie en quelques dizaines de millisecondes.
// realtime.js (navigateur)
async function startRealtime(ephemeralKey) {
const pc = new RTCPeerConnection();
pc.ontrack = (e) => { audioEl.srcObject = e.streams[0]; };
const ms = await navigator.mediaDevices.getUserMedia({ audio: true });
pc.addTrack(ms.getTracks()[0]);
const dc = pc.createDataChannel("oai-events");
dc.onmessage = (e) => console.log(JSON.parse(e.data).type);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const answer = await fetch(
"https://api.openai.com/v1/realtime/calls?model=gpt-realtime",
{ method: "POST", headers: {
"Authorization": "Bearer " + ephemeralKey,
"Content-Type": "application/sdp"
}, body: offer.sdp }
).then(r => r.text());
await pc.setRemoteDescription({ type: "answer", sdp: answer });
}
Deux remarques cruciales. Ne jamais exposer la clé API côté navigateur : on génère côté serveur un ephemeral key via l'endpoint POST /v1/realtime/sessions, valide une minute, et on la passe au navigateur. Le data channel oai-events remplace le canal de contrôle JSON du WebSocket — toute la logique métier (function calling, session.update, response.cancel) passe par lui. Le flux audio passe via le track WebRTC standard, ce qui rend la latence réseau quasi optimale.
Étape 8 — Brancher Realtime API dans LiveKit MultimodalAgent
LiveKit Agents 1.5 propose une classe MultimodalAgent qui encapsule la complexité du protocole Realtime : on garde l'API LiveKit (rooms, métriques, observabilité) mais on bascule sur le speech-to-speech direct sans coder le WebSocket.
from livekit.agents import AgentSession
from livekit.plugins import openai
session = AgentSession(
llm=openai.realtime.RealtimeModel(
model="gpt-realtime",
voice="alloy",
instructions="Tu es Aïda, assistante vocale francophone…",
turn_detection={"type": "server_vad", "silence_duration_ms": 500},
),
)
Une seule ligne change par rapport à un pipeline classique : llm=... remplace les briques stt/llm/tts indépendantes par un modèle multimodal unique. Tout le reste — VAD, room, métriques, function calling — reste identique. C'est le chemin le plus court pour passer un projet du pipeline traditionnel au speech-to-speech sans réécrire l'orchestration.
Erreurs fréquentes
| Symptôme | Cause | Solution |
|---|---|---|
| HTTP 401 sur la connexion | Header OpenAI-Beta manquant |
Ajouter OpenAI-Beta: realtime=v1 |
Pas d'événement response.audio.delta |
modalities ne contient pas "audio", ou modèle qui émet la nouvelle convention response.output_audio.delta |
Vérifier session.update ; gérer les deux noms d'événement (renommage en cours côté GA) |
| Audio sortant haché | Décodage trop lent côté client | Augmenter le buffer playback à 200 ms |
| Coût qui explose | Cache désactivé sur le system prompt | Le cache est automatique dès qu'un préfixe se répète |
| VAD ne détecte rien | Format audio incorrect (mp3, 8 kHz) | Forcer PCM16 16 kHz mono côté capture |
| Latence WebRTC > 600 ms | Région cloud lointaine | Tester depuis un VPS proche du datacenter |
Sur un angle proche
- Documentation OpenAI Realtime API
- Modèle gpt-realtime
- Annonce GA Introducing gpt-realtime
- 🔝 Retour au guide principal : Agents vocaux IA en 2026 : architecture, modèles, latence
- Tutoriel précédent : ElevenLabs Flash v2.5
- Tutoriel suivant : Agent vocal sur numéro Twilio