Aller au contenu principal

Messages WebSocket : texte et audio

Protocole d’echange de messages TTS

Une fois la connexion WebSocket etablie, la communication se fait par echange de messages JSON. Vous envoyez du texte, le serveur repond avec de l’audio encode en base64. Comprendre ce protocole est essentiel pour implementer correctement le streaming TTS.

Messages client vers serveur

Votre application envoie deux types de messages au serveur.

text.delta : envoyer un morceau de texte

Le message text.delta envoie un fragment de texte a convertir. Vous pouvez envoyer le texte par morceaux successifs :

{"type": "text.delta", "delta": "Bonjour, bienvenue dans "}
{"type": "text.delta", "delta": "notre presentation sur la synthese vocale."}

Chaque delta peut contenir jusqu’a 15 000 caracteres. Le serveur commence a generer l’audio des qu’il recoit suffisamment de contexte pour produire une prononciation naturelle.

text.done : signaler la fin du texte

Le message text.done indique au serveur que vous avez fini d’envoyer du texte pour cette sequence :

{"type": "text.done"}

Apres avoir recu ce message, le serveur termine la generation audio et envoie un message audio.done. Vous pouvez ensuite recommencer a envoyer de nouveaux deltas pour une nouvelle sequence.

Messages serveur vers client

Le serveur repond avec trois types de messages.

audio.delta : morceau audio

Le message audio.delta contient un fragment audio encode en base64 :

{
  "type": "audio.delta",
  "delta": "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2..."
}

Chaque delta est un morceau audio valide que vous pouvez decoder et jouer immediatement. Les deltas arrivent au fur et a mesure de la generation.

audio.done : fin de la generation

Le message audio.done signale que toute l’audio pour la sequence de texte envoyee a ete generee :

{
  "type": "audio.done",
  "trace_id": "550e8400-e29b-41d4-a716-446655440000"
}

Le trace_id est un identifiant unique de la sequence, utile pour le debugging et le suivi.

error : erreur

En cas de probleme, le serveur envoie un message d’erreur :

{
  "type": "error",
  "message": "Invalid text encoding"
}

Flux de communication complet

Voici le deroulement typique d’une session :

Client                          Serveur
  |                                |
  |-- text.delta ("Bonjour, ") -->|
  |-- text.delta ("comment ") --->|
  |-- text.delta ("allez-vous?")->|
  |<-- audio.delta (chunk 1) -----|
  |<-- audio.delta (chunk 2) -----|
  |-- text.done ----------------->|
  |<-- audio.delta (chunk 3) -----|
  |<-- audio.done ----------------|

Le serveur peut commencer a envoyer des audio.delta avant meme que vous ayez envoye text.done. C’est le principe du streaming : la generation commence des que possible.

Implementation complete en Python

import asyncio
import websockets
import json
import base64

async def stream_tts(api_key, text, voice="eve", language="fr"):
    uri = f"wss://api.x.ai/v1/tts?language={language}&voice={voice}&codec=mp3"
    headers = {"Authorization": f"Bearer {api_key}"}

    audio_chunks = []

    async with websockets.connect(uri, extra_headers=headers) as ws:
        # Envoyer le texte par morceaux de 2000 caracteres
        chunk_size = 2000
        for i in range(0, len(text), chunk_size):
            chunk = text[i:i + chunk_size]
            await ws.send(json.dumps({
                "type": "text.delta",
                "delta": chunk
            }))

        # Signaler la fin du texte
        await ws.send(json.dumps({"type": "text.done"}))

        # Recevoir les morceaux audio
        async for message in ws:
            data = json.loads(message)

            if data["type"] == "audio.delta":
                audio_bytes = base64.b64decode(data["delta"])
                audio_chunks.append(audio_bytes)

            elif data["type"] == "audio.done":
                print(f"Generation terminee (trace: {data['trace_id']})")
                break

            elif data["type"] == "error":
                raise Exception(f"Erreur TTS: {data['message']}")

    # Assembler l'audio complet
    return b"".join(audio_chunks)

Implementation en JavaScript (Node.js)

const WebSocket = require("ws");

function streamTTS(apiKey, text, voice = "eve", language = "fr") {
  return new Promise((resolve, reject) => {
    const ws = new WebSocket(
      `wss://api.x.ai/v1/tts?language=${language}&voice=${voice}&codec=mp3`,
      { headers: { Authorization: `Bearer ${apiKey}` } }
    );

    const audioChunks = [];

    ws.on("open", () => {
      // Envoyer le texte
      ws.send(JSON.stringify({ type: "text.delta", delta: text }));
      ws.send(JSON.stringify({ type: "text.done" }));
    });

    ws.on("message", (raw) => {
      const data = JSON.parse(raw);

      if (data.type === "audio.delta") {
        audioChunks.push(Buffer.from(data.delta, "base64"));
      } else if (data.type === "audio.done") {
        ws.close();
        resolve(Buffer.concat(audioChunks));
      } else if (data.type === "error") {
        ws.close();
        reject(new Error(data.message));
      }
    });

    ws.on("error", reject);
  });
}

Gestion des deltas multiples

Pour les textes generes progressivement (par exemple la sortie d’un LLM), envoyez chaque token directement au WebSocket TTS :

async def llm_to_speech(ws_tts, llm_stream):
    async for token in llm_stream:
        await ws_tts.send(json.dumps({
            "type": "text.delta",
            "delta": token
        }))

    await ws_tts.send(json.dumps({"type": "text.done"}))

Cette architecture “LLM streaming vers TTS streaming” offre la latence la plus basse possible : l’utilisateur entend la reponse pendant que le LLM continue a generer.

Points cles a retenir

  • Deux messages client : text.delta (envoyer du texte) et text.done (fin du texte)
  • Trois messages serveur : audio.delta (morceau audio), audio.done (fin), error
  • L’audio est encode en base64 dans les messages audio.delta
  • Le serveur commence a generer avant de recevoir text.done
  • Chaque delta de texte est limite a 15 000 caracteres
  • Le chaining LLM streaming vers TTS streaming offre la latence minimale