Pipeline LLM vers TTS streaming
Mis à jour le 30 juillet 2026
Connecter un LLM à la synthèse vocale en temps réel
Le cas d’usage le plus puissant du WebSocket TTS est le chaining avec un LLM en streaming. Le LLM génère du texte token par token, chaque token est envoyé au TTS via WebSocket, et l’audio est joue au fur et à mesure. L’utilisateur entend la réponse pendant que le LLM continue à réfléchir.
Architecture du pipeline
Le flux de données traverse trois étapes :
Utilisateur -> LLM (streaming) -> WebSocket TTS -> Audio (lecture)
question tokens texte audio.delta haut-parleur
Chaque étape fonctionne en streaming, ce qui minimise la latence perçue. L’utilisateur n’attend pas que le LLM ait fini de générer toute sa réponse pour commencer à l’entendre.
Implémentation en Python
Voici un pipeline complet qui connecte l’API Chat Completions de Grok au TTS WebSocket :
import asyncio
import websockets
import json
import base64
import aiohttp
async def llm_to_speech(api_key, prompt, voice="rex", language="fr"):
# Etape 1 : Connexion WebSocket TTS
tts_uri = f"wss://api.x.ai/v1/tts?language={language}&voice={voice}"
tts_headers = {"Authorization": f"Bearer {api_key}"}
audio_chunks = []
async with websockets.connect(tts_uri, additional_headers=tts_headers) as tts_ws:
# Étape 2 : Appel LLM en streaming
async with aiohttp.ClientSession() as session:
async with session.post(
"https://api.x.ai/v1/chat/completions",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
},
json={
"model": "grok-4.5",
"messages": [{"role": "user", "content": prompt}],
"stream": True
}
) as llm_response:
# Etape 3 : Chaque token LLM -> delta TTS
buffer = ""
async for line in llm_response.content:
line = line.decode().strip()
if not line.startswith("data: "):
continue
if line == "data: [DONE]":
break
chunk = json.loads(line[6:])
token = chunk["choices"][0].get("delta", {}).get("content", "")
if token:
buffer += token
# Envoyer par phrases complètes
if any(c in token for c in ".!?\n"):
await tts_ws.send(json.dumps({
"type": "text.delta",
"delta": buffer
}))
buffer = ""
# Envoyer le reste du buffer
if buffer:
await tts_ws.send(json.dumps({
"type": "text.delta",
"delta": buffer
}))
await tts_ws.send(json.dumps({"type": "text.done"}))
# Etape 4 : Collecter l'audio
async for msg in tts_ws:
data = json.loads(msg)
if data["type"] == "audio.delta":
audio_chunks.append(base64.b64decode(data["delta"]))
elif data["type"] == "audio.done":
break
return b"".join(audio_chunks)
Stratégie de buffering
Envoyer chaque token individuellement au TTS n’est pas optimal. Le moteur de synthèse produit un meilleur résultat quand il dispose de contexte. Trois stratégies de buffering :
Par phrase
Attendez un délimiteur de phrase (., !, ?, retour à la ligne) avant d’envoyer :
if any(c in token for c in ".!?\n"):
await tts_ws.send(json.dumps({
"type": "text.delta", "delta": buffer
}))
buffer = ""
C’est le meilleur compromis entre qualité et latence.
Par taille
Envoyez quand le buffer atteint une certaine taille (par exemple 200 caractères) :
if len(buffer) >= 200:
await tts_ws.send(json.dumps({
"type": "text.delta", "delta": buffer
}))
buffer = ""
Plus simple mais peut couper au milieu d’un mot.
Par temporisation
Envoyez après un délai sans nouveau token (utile quand le LLM réfléchit) :
last_token_time = time.time()
# Si plus de 500ms sans token, envoyer le buffer
if time.time() - last_token_time > 0.5 and buffer:
await tts_ws.send(...)
buffer = ""
Lecture audio en temps réel
Pour lire l’audio au fur et à mesure (pas seulement le collecter), utilisez une queue asynchrone :
import asyncio
audio_queue = asyncio.Queue()
async def receive_audio(tts_ws):
"""Reçoit les chunks audio et les met en queue."""
async for msg in tts_ws:
data = json.loads(msg)
if data["type"] == "audio.delta":
audio_bytes = base64.b64decode(data["delta"])
await audio_queue.put(audio_bytes)
elif data["type"] == "audio.done":
await audio_queue.put(None) # Sentinelle de fin
break
async def play_audio():
"""Lit les chunks audio depuis la queue."""
while True:
chunk = await audio_queue.get()
if chunk is None:
break
# Jouer le chunk (pyaudio, sounddevice, etc.)
play_chunk(chunk)
En exécutant receive_audio et play_audio en parallèle, la lecture commence dès le premier chunk reçu.
Gestion des erreurs dans le pipeline
Le pipeline peut échouer à trois endroits : le LLM, le TTS, ou le réseau. Chaque point doit être géré :
async def robust_pipeline(api_key, prompt):
try:
audio = await llm_to_speech(api_key, prompt)
return audio
except aiohttp.ClientError:
print("Erreur LLM - vérifiez votre connexion")
except websockets.ConnectionClosed:
print("Connexion TTS perdue - reconnexion necessaire")
except json.JSONDecodeError:
print("Réponse invalide - format inattendu")
except Exception as e:
print(f"Erreur inattendue: {e}")
return None
Points clés à retenir
- Le pipeline LLM streaming vers TTS WebSocket offre la latence minimale
- Bufferisez par phrase pour le meilleur compromis qualité/latence
- Utilisez une queue asynchrone pour la lecture audio en temps réel
- Gérez les erreurs à chaque étape du pipeline
- Le texte total est illimité via WebSocket (deltas de 15 000 caractères max)
- La limite de 50 sessions concurrentes s’applique à l’ensemble de l’équipe