Synthétiser en temps réel
La synthèse vocale en temps réel produit la voix pendant que le texte est encore en cours d'écriture. Vous ouvrez une session WebSocket, envoyez le texte par fragments au fur et à mesure de son arrivée — par exemple, depuis un modèle en streaming de tokens — et l'API renvoie la voix dès qu'elle est prête. L'auditeur entend les premiers mots avant que le texte soit complet.
Une session prend en charge plusieurs générations successives : terminez l'une, commencez la suivante, sans vous reconnecter.
Si le texte est déjà complet, utilisez Génération de voix ou Streaming de la voix. Pour choisir entre les modes, consultez Audio hors ligne et temps réel.
Comprendre le cycle de vie
Une session est une connexion WebSocket qui traverse quatre étapes, toujours dans le même ordre. Votre application envoie le texte, et l'API renvoie l'audio pendant que le texte continue de circuler.
À chaque étape, votre application effectue les actions suivantes :
| Étape | Vous envoyez | Vous recevez | Action de votre application |
|---|---|---|---|
| Ouvrir | Une connexion avec le modèle et votre clé API | session.created, avec les paramètres par défaut | Attendez-le avant d'envoyer quoi que ce soit d'autre. |
| Configurer | session.update, avec vos paramètres | session.updated, avec les paramètres appliqués | Vérifiez les valeurs. Elles sont fixes jusqu'à la prochaine mise à jour. |
| Envoyer le texte | input_text.append, un fragment de texte à la fois | speech.audio.delta, avec chaque fragment audio | Décodez chaque fragment et transmettez-le à votre lecteur, ou ajoutez-le au fichier que vous écrivez. |
| Terminer | input_text.flush pour finir une génération, ou input_text.end pour terminer la session | speech.audio.done, avec l'usage de la génération | Commencez la génération suivante après flush, ou fermez la connexion après end. |
Si l'API rejette un message, elle envoie un événement error à cette étape. Consultez Problèmes courants.
Se connecter à une session
Ouvrez une connexion WebSocket vers le point de terminaison TTS temps réel, avec le modèle dans la chaîne de requête et votre clé API dans l'en-tête Authorization :
wss://api.mistral.ai/v1/audio/speech/realtime?model=voxtral-mini-tts-3-0
Authorization: Bearer $MISTRAL_API_KEYLe serveur répond avec session.created, contenant les paramètres par défaut :
{
"type": "session.created",
"session": {
"model": "voxtral-mini-tts-3-0-13",
"voice": "voice_447",
"streaming_mode": "responsive",
"response_format": "wav"
}
}Configurer la session
Envoyez un message session.update pour modifier les paramètres des générations suivantes. Le serveur accuse réception avec session.updated :
{
"type": "session.update",
"session": {
"voice": "voice_0",
"streaming_mode": "long_form",
"response_format": "mp3"
}
}Paramètres de la session
| Champ | Type | Valeur par défaut | Sert à |
|---|---|---|---|
voice | Chaîne, un identifiant de voix prédéfinie | voice_447 | Choisir une voix prête à l'emploi. Consultez Voix. |
ref_audio | Chaîne, audio encodé en base64 | Non défini | Reproduire une voix à partir d'un court enregistrement. Fournissez ce champ ou voice, pas les deux. Consultez Clonage de voix. |
streaming_mode | Chaîne | responsive | responsive (latence réduite) ou long_form (qualité supérieure pour les textes longs). |
response_format | Chaîne | wav | pcm, wav, mp3, flac, opus. Consultez Formats audio. |
Mettez à jour les paramètres de la session avant le début de la saisie, ou après que toutes les générations en attente sont terminées sans texte non vidé. Une mise à jour pendant une génération active est une erreur récupérable : la mise à jour est rejetée, et la connexion reste ouverte.
Envoyer le texte et recevoir l'audio
Envoyez le texte par fragments avec input_text.append. Chaque message ne contient que le nouveau delta — les espaces ne sont pas insérés automatiquement, incluez-les dans le texte que vous envoyez.
| Vous envoyez | Action |
|---|---|
input_text.append | Ajoute du nouveau texte à la génération en cours. Envoyez uniquement le nouveau delta. |
input_text.flush | Termine la saisie de la génération en cours. Peut inclure un delta text final. La connexion reste ouverte pour la génération suivante. |
input_text.end | Termine la saisie, vide les générations en file d'attente, et ferme la connexion (code 1000). N'accepte pas de champ text. |
Le serveur renvoie l'audio dès qu'il est prêt :
| Vous recevez | Champs | Envoyé quand |
|---|---|---|
speech.audio.delta | audio_data (base64) | Un fragment audio est prêt. Décodez chaque delta séparément et concaténez les octets dans l'ordre. |
speech.audio.done | usage | Tout l'audio de la génération en cours a été envoyé. |
error | error.message, error.code | L'API rejette un message ou une génération échoue. Un échec de génération ferme la connexion. |
L'audio peut arriver avant flush ou end une fois qu'il y a suffisamment de texte. Le serveur ne vide pas automatiquement sur les pauses, les espaces ou la ponctuation — c'est vous qui décidez quand une génération est complète.
Voici une session complète avec deux générations :
Client: WebSocket connect
Server: session.created
Client: {"type":"session.update","session":{"voice":"voice_0"}}
Server: session.updated
Client: {"type":"input_text.append","text":"Hello "}
Client: {"type":"input_text.append","text":"world."}
Client: {"type":"input_text.flush"}
Server: speech.audio.delta ...
Server: speech.audio.done
Client: {"type":"input_text.append","text":"Goodbye."}
Client: {"type":"input_text.end"}
Server: speech.audio.delta ...
Server: speech.audio.done
Server: WebSocket close 1000Le script suivant exécute la même session, enregistre l'audio de chaque génération dans un fichier distinct, et mesure le délai avant le premier son :
import asyncio
import base64
import json
import os
import time
from pathlib import Path
import websockets
MODEL = "voxtral-mini-tts-3-0"
URL = f"wss://api.mistral.ai/v1/audio/speech/realtime?model={MODEL}"
HEADERS = {"Authorization": f"Bearer {os.environ['MISTRAL_API_KEY']}"}
async def receive_audio(ws, label, start):
"""Receive audio for one generation. Returns the raw bytes."""
chunks = []
first_audio = None
while True:
msg = json.loads(await ws.recv())
if msg["type"] == "speech.audio.delta":
if first_audio is None:
first_audio = time.perf_counter() - start
print(f" [{label}] First audio after {first_audio:.2f}s")
chunks.append(base64.b64decode(msg["audio_data"]))
elif msg["type"] == "speech.audio.done":
print(f" [{label}] Done ({sum(len(c) for c in chunks)} bytes)")
return b"".join(chunks)
elif msg["type"] == "error":
raise RuntimeError(msg["error"]["message"])
async def main():
start = time.perf_counter()
async with websockets.connect(URL, additional_headers=HEADERS) as ws:
# Wait for session.created
msg = json.loads(await ws.recv())
assert msg["type"] == "session.created"
print(f"Connected ({time.perf_counter() - start:.2f}s)")
# Configure the session
await ws.send(json.dumps({
"type": "session.update",
"session": {"voice": "voice_447", "response_format": "wav"},
}))
msg = json.loads(await ws.recv())
assert msg["type"] == "session.updated"
# Generation 1: send text in pieces, then flush
await ws.send(json.dumps({"type": "input_text.append", "text": "Hello "}))
await ws.send(json.dumps({"type": "input_text.append", "text": "world. "}))
await ws.send(json.dumps({
"type": "input_text.append",
"text": "This is a test of the realtime TTS API.",
}))
await ws.send(json.dumps({"type": "input_text.flush"}))
audio1 = await receive_audio(ws, "gen1", start)
Path("realtime_gen1.wav").write_bytes(audio1)
# Generation 2: send text, then end (closes the connection)
await ws.send(json.dumps({"type": "input_text.append", "text": "Goodbye."}))
await ws.send(json.dumps({"type": "input_text.end"}))
audio2 = await receive_audio(ws, "gen2", start)
Path("realtime_gen2.wav").write_bytes(audio2)
print(f"Total: {time.perf_counter() - start:.2f}s")
print("Saved realtime_gen1.wav and realtime_gen2.wav")
asyncio.run(main())Terminer une génération
Après un flush, le prochain input_text.append démarre une nouvelle génération. Les paramètres de la session persistent, mais le contexte du modèle de la génération précédente n'est pas conservé. speech.audio.done signifie que tout l'audio de cette génération a été envoyé — pas que le client a fini de le lire.
Après end, continuez à recevoir jusqu'à ce que le serveur ferme la connexion. Le serveur vide les générations en file d'attente, envoie leur audio, puis ferme avec le code 1000.
Chaque speech.audio.delta contient de l'audio en base64 dans audio_data. Décodez chaque delta séparément et concaténez les octets dans l'ordre. La sortie WAV a un en-tête par génération ; les deltas individuels ne sont pas des fichiers autonomes.
Problèmes courants
Ces problèmes sont propres aux sessions temps réel. Pour les erreurs liées à la requête elle-même, comme une voix inconnue ou un format invalide, consultez Dépannage.
| Symptôme | Cause | Solution |
|---|---|---|
| Aucun audio n'arrive | Vous avez envoyé input_text.append mais jamais flush ou end | Le serveur ne vide pas automatiquement. Envoyez flush quand le texte d'une génération est complet, ou end pour terminer et fermer. |
| L'audio arrive dans le désordre | Vous avez décodé ou écrit les fragments dans le désordre | Décodez chaque speech.audio.delta et écrivez les octets dans leur ordre d'arrivée. |
| La connexion se ferme en cours de génération | Une génération a échoué | Le serveur émet error et ferme la connexion en cas d'échec. Vérifiez le message d'erreur, puis ouvrez une nouvelle session. |
session.update est rejeté | Vous l'avez envoyé pendant une génération active | Mettez à jour les paramètres avant le début de la saisie, ou après speech.audio.done sans texte non vidé. |
| Le fichier WAV sauvegardé ne se lit pas | Vous avez concaténé plusieurs générations dans un seul fichier | Chaque génération a son propre en-tête WAV. Enregistrez chaque génération dans un fichier distinct, ou utilisez un format sans en-tête comme pcm. |