Blog
AI/Voice
10 décembre 20258 min

Pipeline vocal temps reel : de l'audio brut a l'IA

Points clés : Construire un pipeline vocal temps réel, c'est enchaîner Deepgram STT (streaming avec VAD), un LLM via OpenRouter (function calling), et OpenAI TTS — le tout connecté par WebSocket authentifié JWT. Cet article détaille chaque étape, les latences, les fallbacks, et les bugs qui m'ont coûté des nuits blanches.

Le cœur de TAMSIV, c'est la voix. Pas un gadget, pas un bouton micro planqué dans un coin. La voix EST l'interface principale. Tu appuies, tu parles, l'IA comprend et exécute. Mais construire un pipeline vocal temps réel en solo, c'est entrer dans un monde où chaque milliseconde compte et où tout peut casser à tout moment.

Après trois semaines de développement intensif et une quantité déraisonnable de café, j'ai un pipeline qui répond en 1.5 à 3 secondes. Voici comment ça marche, chunk par chunk.

Ondes sonores se propageant dans un environnement sombre avec des particules de lumière bleue et cyan
De l'onde sonore à la réponse structurée : chaque milliseconde compte.

Comment fonctionne l'architecture du pipeline vocal ?

Le pipeline complet en une ligne :

Audio PCM 16kHz mono → WebSocket (JWT) → Deepgram Live STT (VAD) → OpenRouter LLM → Function calling → OpenAI TTS → Reponse vocale

Six étapes, six points de défaillance potentiels. Chacune a ses contraintes, ses latences, et ses pièges. Le tout doit s'enchaîner en moins de 3 secondes pour que l'expérience soit fluide. Au-delà, l'utilisateur pense que l'app a planté.

Décomposons chaque étape.

Pourquoi le WebSocket est-il indispensable pour l'audio temps réel ?

HTTP classique ne fonctionne pas pour le streaming audio. Tu aurais besoin d'envoyer tout l'enregistrement d'un bloc, attendre le traitement, puis recevoir la réponse. La latence serait inacceptable.

Le WebSocket permet un flux bidirectionnel continu : le téléphone envoie des chunks audio pendant que l'utilisateur parle, et le backend peut commencer à traiter avant même que l'utilisateur ait fini.

L'authentification JWT

Chaque connexion WebSocket est authentifiée via un token JWT Supabase :

ws://backend:3001?token=eyJhbGciOiJIUzI1NiIs...

Le token est validé à la connexion. Si le token expire en pleine conversation (les tokens Supabase expirent après 1 heure), le client détecte la déconnexion et se reconnecte automatiquement avec un token frais. J'ai dû gérer ce cas explicitement — au début, les conversations longues plantaient mystérieusement.

La sécurité du WebSocket est détaillée dans l'article sur l'audit de sécurité et le rate limiting.

Comment Deepgram gère-t-il le Speech-to-Text en streaming ?

L'audio doit être en PCM 16 bits, 16kHz, mono. Le téléphone capture l'audio dans ce format et envoie des chunks binaires bruts via le WebSocket. Pas de compression, pas d'encodage — le PCM brut est le format le plus rapide à traiter.

Deepgram reçoit ces chunks et transcrit en streaming. Mais la vraie magie, c'est le VAD (Voice Activity Detection).

Pourquoi le VAD change tout ?

Sans VAD, il faut implémenter un timeout de silence côté client : si l'utilisateur ne parle pas pendant X secondes, on considère qu'il a fini. Le problème :

  • Trop court (1s) : tu coupes l'utilisateur qui réfléchit entre deux phrases.
  • Trop long (3s) : l'app rame, l'utilisateur attend.
  • Variable selon l'utilisateur : certains parlent vite, d'autres prennent leur temps.

Le VAD de Deepgram détecte quand l'utilisateur a fini de parler avec une précision remarquable. Il analyse le signal audio en temps réel et envoie un événement speech_final quand il est certain que l'utilisateur a terminé. Ça prend environ 200ms après la fin de la parole.

Les résultats intermédiaires vs finaux

Deepgram envoie deux types de résultats :

  • is_final: false — Résultats intermédiaires, instables. Le mot détecté peut changer à mesure que le contexte s'enrichit.
  • is_final: true — Résultats confirmés. Le texte ne changera plus.

Le piège : accumuler les résultats intermédiaires pour afficher un preview en temps réel (bon pour l'UX) tout en ne transmettant que les résultats finaux au LLM (bon pour la qualité). J'affiche les résultats intermédiaires en gris et les finaux en blanc — l'utilisateur voit sa voix se transformer en texte en direct.

Pour la comparaison entre STT natif (gratuit, sur le device) et Deepgram cloud (plus précis), lis mon article détaillé STT natif vs Deepgram.

Microphone professionnel de studio avec indicateur LED dans un environnement sombre, éclairage bleu et violet
La capture audio est la première étape critique du pipeline.

Comment le LLM orchestre-t-il les actions via function calling ?

La transcription complète part vers OpenRouter avec function calling. OpenRouter est un routeur qui donne accès à 400+ modèles LLM avec fallback automatique — si le modèle principal est down, un fallback prend le relais en quelques secondes.

Le LLM reçoit la transcription et doit comprendre l'intention de l'utilisateur. Il dispose de 7 fonctions :

  • create_task — Créer une tâche
  • update_task — Modifier une tâche existante
  • create_memo — Créer un mémo
  • update_memo — Modifier un mémo existant
  • create_calendar_event — Créer un événement agenda
  • ask_clarification — Demander une précision à l'utilisateur
  • end_conversation — Terminer la conversation

Le LLM analyse la phrase ("rappelle-moi d'acheter du pain demain à 10h"), identifie l'action (create_task), extrait les paramètres (titre, date, heure), et retourne un appel de fonction structuré. Le backend exécute l'action et renvoie le résultat au frontend.

Le pattern PendingCreation est crucial ici : le backend crée une preview de l'élément, et l'utilisateur peut valider, éditer ou annuler avant la sauvegarde définitive en base de données. Zéro mauvaise surprise.

La latence LLM

Le LLM représente le gros de la latence : entre 800ms et 2 secondes selon le modèle et la complexité de la requête. C'est ici que le choix du modèle via OpenRouter fait la différence — un modèle rapide mais moins précis vs un modèle lent mais plus fiable. TAMSIV utilise un modèle configurable avec fallback automatique si le modèle principal est trop lent ou indisponible.

Comment OpenAI TTS génère-t-il la réponse vocale ?

Une fois l'action exécutée, le backend génère une réponse textuelle ("C'est noté ! J'ai créé la tâche 'Acheter du pain' pour demain à 10h"). Ce texte part vers OpenAI TTS avec la voix nova.

L'audio est streamé en retour via le même WebSocket. Le frontend commence à jouer dès les premiers chunks audio, sans attendre la réponse complète. Ça réduit la latence perçue d'environ 500ms — l'utilisateur entend le début de la réponse pendant que la fin est encore en cours de génération.

Pour la personnalisation vocale et le choix de la voix TTS, j'en parle dans l'article sur la personnalisation vocale.

Quels sont les trois modes WebSocket de TAMSIV ?

Au fil du développement, trois modes WebSocket ont émergé :

  1. LiveWebSocketServer (défaut) — STT natif device + Deepgram fallback, orchestration LLM, OpenAI TTS. C'est le mode standard, le plus économique.
  2. RealtimeWebSocketServer — API OpenAI Realtime, bidirectionnel, basse latence. Plus coûteux mais plus fluide pour les conversations longues.
  3. WebSocketServer — Batch STT/TTS, mode legacy. Utilisé pour les cas où le streaming n'est pas nécessaire.

Le mode est sélectionnable côté admin via la table app_config dans Supabase. Ça permet de basculer entre les modes sans déployer une nouvelle version de l'app.

Câbles de fibre optique lumineux bleus et verts dans un centre de données, visualisation de flux de données
Les données traversent le pipeline à la vitesse de la lumière — en théorie.

Comment gérer les erreurs dans un pipeline temps réel ?

La règle d'or : tout peut échouer à tout moment. Deepgram peut être down. OpenRouter peut timeout. OpenAI TTS peut retourner une erreur 429. La connexion WebSocket peut couper en pleine conversation.

Voici les mécanismes de résilience :

  • Retry intelligents : Chaque étape a un nombre de retries configuré avec backoff exponentiel. Pas de retry en boucle infinie.
  • Circuit breakers : Si un service échoue trop souvent, on arrête de l'appeler pendant X secondes pour éviter de surcharger un service déjà en difficulté.
  • Fallbacks à chaque étape : STT natif si Deepgram est down, modèle LLM alternatif via OpenRouter, réponse texte si TTS échoue.
  • AlertService : Chaque fallback déclenche une alerte email (via Resend) + log Supabase. Je sais en temps réel quand quelque chose dégrade.

Chaque ligne de gestion d'erreur représente un bug vécu en production. Le pipeline est robuste aujourd'hui, mais il a fallu des dizaines de sessions de debug pour en arriver là.

Quel est le budget latence de chaque étape ?

Décomposition d'une interaction vocale complète :

  • Capture audio + envoi WebSocket : ~50ms (négligeable)
  • Deepgram STT + VAD : ~200-400ms après fin de parole
  • OpenRouter LLM (function calling) : ~800ms-2000ms (variable)
  • OpenAI TTS (premier chunk) : ~300-500ms

Total : 1.3 à 3 secondes. L'objectif est de rester sous 2 secondes pour 90% des interactions. Au-delà, l'expérience devient frustrante.

Le streaming TTS est le meilleur levier : l'utilisateur entend le début de la réponse après ~1.5s en moyenne, même si la génération complète prend 3 secondes. La latence perçue est bien inférieure à la latence réelle.

Quelles leçons tirer pour construire ton propre pipeline vocal ?

Après trois semaines de dev intensif, voici ce que je conseillerais :

  1. Commence par le WebSocket : C'est la colonne vertébrale. Si le WebSocket est solide, le reste s'emboîte.
  2. Utilise le VAD du provider STT : N'implémente pas ta propre détection de silence — c'est un gouffre de complexité pour un résultat inférieur.
  3. Streame tout : STT en streaming, TTS en streaming. Chaque milliseconde économisée améliore l'UX.
  4. Prévois les fallbacks dès le jour 1 : Pas en mode "je verrai plus tard". Chaque étape doit avoir un plan B.
  5. Mesure la latence en production : Les benchmarks locaux sont trompeurs. La latence réelle dépend du réseau, de la charge serveur, et de la localisation géographique. Le dashboard admin m'a été indispensable pour ça.

FAQ

Pourquoi Deepgram plutôt que Google Speech-to-Text ou AWS Transcribe ?

Deepgram offre le meilleur rapport qualité/latence pour le streaming. Google STT est excellent mais plus cher et plus lent en mode streaming. AWS Transcribe est robuste mais l'intégration WebSocket est plus complexe. Deepgram a aussi un VAD intégré, ce qui simplifie énormément le code.

Le pipeline fonctionne-t-il en mode hors-ligne ?

Le STT natif du device fonctionne hors-ligne. Mais le LLM et le TTS nécessitent une connexion internet. En mode hors-ligne, TAMSIV permet la saisie textuelle classique et met en queue les requêtes vocales pour quand la connexion revient.

Combien coûte une interaction vocale complète ?

Avec le STT natif (gratuit), un LLM économique via OpenRouter (~$0.001-0.01), et OpenAI TTS (~$0.015/1000 chars), une interaction coûte entre $0.01 et $0.03. Le détail des coûts est dans l'article rétrospective sur les 650 commits.

Peut-on remplacer OpenAI TTS par une solution open-source ?

Techniquement oui. Des projets comme Coqui TTS ou Bark produisent des résultats corrects. Mais la qualité de la voix "nova" d'OpenAI reste supérieure, et le streaming est mieux supporté. Pour un projet en production, le coût additionnel d'OpenAI TTS ($15/million de caractères) se justifie par la qualité.

Comment gérer les langues multiples dans le pipeline vocal ?

Deepgram supporte la détection automatique de langue. Le LLM via OpenRouter est naturellement multilingue. Le TTS OpenAI génère de l'audio dans la langue du texte fourni. TAMSIV supporte 6 langues (FR, EN, DE, ES, IT, PT) sans configuration spécifique par langue dans le pipeline. Le détail de l'internationalisation est dans l'article sur l'i18n en 6 langues.