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) ettext.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