Streaming temps réel
Mis à jour le 29 juillet 2026
Pourquoi le streaming change tout
Jusqu’ici, vous avez travaillé en mode synchrone : vous envoyez le texte, vous attendez que toute la synthèse soit terminée, puis vous recevez l’audio complet. Pour une phrase d’accueil pré-générée la veille, cette attente n’a aucune importance. Pour un assistant vocal qui répond à un client au téléphone, elle est rédhibitoire — deux secondes de silence après une question, et l’utilisateur croit que la ligne a coupé.
Le streaming supprime cette attente. L’audio part au fur et à mesure de sa génération, fragment par fragment, et l’utilisateur entend les premiers mots pendant que le modèle produit encore la fin de la phrase. Le temps de génération total ne change pas ; c’est le délai avant le premier son qui s’effondre.
Le protocole d’événements
Le flux repose sur des Server-Sent Events. Chaque message reçu est un événement typé, et deux types vous concernent :
speech.audio.delta: contient un fragment d’audio en base64. C’est l’événement principal que vous traiterezspeech.audio.done: signal de fin — la génération est complète
Côté code, le passage au streaming se fait en ajoutant stream=True et en consommant la réponse dans un bloc with. L’exemple qui suit accumule les fragments en mémoire pour écrire un fichier à la fin : ce n’est pas encore du temps réel, mais c’est la structure de boucle que tout le reste de la leçon reprendra.
import base64
from pathlib import Path
from mistralai.client import Mistral
client = Mistral(api_key="votre-cle-api")
audio_chunks = []
with client.audio.speech.complete(
model="voxtral-mini-tts-2603",
input="Le streaming rend les agents vocaux réactifs et naturels.",
voice_id="votre-voice-id",
response_format="opus",
stream=True,
) as stream:
for event in stream:
if event.event == "speech.audio.delta":
chunk = base64.b64decode(event.data.audio_data)
audio_chunks.append(chunk)
print(f"Chunk reçu : {len(chunk)} octets")
# Assembler tous les chunks
audio_complet = b"".join(audio_chunks)
Path("streaming-output.opus").write_bytes(audio_complet)
print(f"Audio final : {len(audio_complet)} octets")
Jouer le son pendant qu’il arrive
Pour obtenir la réactivité promise, il faut cesser d’accumuler et écrire chaque fragment directement dans la carte son. Avec pyaudio, la modification tient en une ligne : au lieu d’ajouter le fragment décodé à une liste, vous l’envoyez à un flux de sortie ouvert en amont, configuré en Float32 mono à 24 000 Hz.
import base64
import pyaudio
from mistralai.client import Mistral
client = Mistral(api_key="votre-cle-api")
# Configuration audio PCM (le format le plus rapide)
p = pyaudio.PyAudio()
stream_audio = p.open(
format=pyaudio.paFloat32,
channels=1,
rate=24000,
output=True,
)
with client.audio.speech.complete(
model="voxtral-mini-tts-2603",
input="Vous entendez cette phrase pendant que je la génère.",
voice_id="votre-voice-id",
response_format="pcm",
stream=True,
) as stream:
for event in stream:
if event.event == "speech.audio.delta":
audio_data = base64.b64decode(event.data.audio_data)
stream_audio.write(audio_data)
stream_audio.stop_stream()
stream_audio.close()
p.terminate()
Le format demandé est passé de opus à pcm, et ce n’est pas anodin. Le PCM n’a pas besoin d’être décodé : les octets reçus sont directement jouables, ce qui en fait le format recommandé pour le temps réel.
Relayer le flux vers un navigateur
Dans une application web, le serveur ne joue pas le son : il le transmet. Le pattern classique est un WebSocket qui reçoit le texte du navigateur, ouvre le flux Voxtral et repousse chaque fragment décodé vers le client. Côté navigateur, c’est l’API Web Audio qui prend le relais pour jouer les fragments PCM reçus.
# Côté serveur (exemple avec FastAPI)
from fastapi import FastAPI, WebSocket
import base64
from mistralai.client import Mistral
app = FastAPI()
client = Mistral(api_key="votre-cle-api")
@app.websocket("/ws/tts")
async def tts_websocket(websocket: WebSocket):
await websocket.accept()
texte = await websocket.receive_text()
with client.audio.speech.complete(
model="voxtral-mini-tts-2603",
input=texte,
voice_id="votre-voice-id",
response_format="pcm",
stream=True,
) as stream:
for event in stream:
if event.event == "speech.audio.delta":
await websocket.send_bytes(
base64.b64decode(event.data.audio_data)
)
await websocket.close()
Le réglage qui fait la différence : le buffer
La taille des fragments est variable et représente généralement quelques millisecondes d’audio chacun. Si vous jouez le premier fragment dès son arrivée, la moindre irrégularité réseau produit une micro-coupure audible — un hoquet dans la voix qui trahit immédiatement la machine. La parade consiste à accumuler deux ou trois fragments avant de démarrer la lecture, ce qui coûte quelques dizaines de millisecondes et lisse le reste du flux.
Reste le choix du format, qui arbitre entre latence et bande passante. Le PCM donne le premier son le plus rapidement, autour de 0,7 seconde, au prix d’un débit élevé. L’Opus perd un peu sur la latence mais compresse fortement, ce qui en fait le bon compromis quand vos utilisateurs sont en mobilité.
Points clés à retenir
- Le streaming envoie l’audio chunk par chunk pendant la génération
- Activez-le avec
stream=Truedans l’appelaudio.speech.complete() - Les événements
speech.audio.deltacontiennent les fragments audio en base64 - Le format PCM offre la latence la plus basse pour le streaming (~0.7s vs ~2s en MP3)
- Pour une lecture temps réel, utilisez
pyaudioou les WebSockets + Web Audio API