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.

i
Information
Comprendre le cycle de vie

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 :

ÉtapeVous envoyezVous recevezAction de votre application
OuvrirUne connexion avec le modèle et votre clé APIsession.created, avec les paramètres par défautAttendez-le avant d'envoyer quoi que ce soit d'autre.
Configurersession.update, avec vos paramètressession.updated, avec les paramètres appliquésVérifiez les valeurs. Elles sont fixes jusqu'à la prochaine mise à jour.
Envoyer le texteinput_text.append, un fragment de texte à la foisspeech.audio.delta, avec chaque fragment audioDécodez chaque fragment et transmettez-le à votre lecteur, ou ajoutez-le au fichier que vous écrivez.
Terminerinput_text.flush pour finir une génération, ou input_text.end pour terminer la sessionspeech.audio.done, avec l'usage de la générationCommencez 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

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_KEY

Le 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

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

Paramètres de la session

ChampTypeValeur par défautSert à
voiceChaîne, un identifiant de voix prédéfinievoice_447Choisir une voix prête à l'emploi. Consultez Voix.
ref_audioChaîne, audio encodé en base64Non définiReproduire une voix à partir d'un court enregistrement. Fournissez ce champ ou voice, pas les deux. Consultez Clonage de voix.
streaming_modeChaîneresponsiveresponsive (latence réduite) ou long_form (qualité supérieure pour les textes longs).
response_formatChaînewavpcm, wav, mp3, flac, opus. Consultez Formats audio.
Note

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

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 envoyezAction
input_text.appendAjoute du nouveau texte à la génération en cours. Envoyez uniquement le nouveau delta.
input_text.flushTermine 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.endTermine 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 recevezChampsEnvoyé quand
speech.audio.deltaaudio_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.doneusageTout l'audio de la génération en cours a été envoyé.
errorerror.message, error.codeL'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 1000

Le 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

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.

Attention

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

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ômeCauseSolution
Aucun audio n'arriveVous avez envoyé input_text.append mais jamais flush ou endLe 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ésordreVous avez décodé ou écrit les fragments dans le désordreDécodez chaque speech.audio.delta et écrivez les octets dans leur ordre d'arrivée.
La connexion se ferme en cours de générationUne 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 activeMettez à 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 pasVous avez concaténé plusieurs générations dans un seul fichierChaque 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.
Aller plus loin

Aller plus loin