Multi-Model TTS Gateway - Synthese Vocale Multi-Modèles

# Parameters
BATCH_MODE = "true"

Module : 02-Audio-Advanced
Niveau : Avance
Technologies : Kokoro TTS, TADA 3B ML, Qwen3 TTS
Duree estimee : 45 minutes
VRAM : ~12 GB total (2+6+4)

Objectifs d’Apprentissage

Prerequis

  • Gateway TTS multi-modèle deploye (port 8196)
  • Notebooks 01-5 (Kokoro) et 02-1 (Chatterbox) recommandes
  • Comprehension de l’API OpenAI TTS

Navigation : Index | << Précédent

import os

# Parametres Papermill - JAMAIS modifier ce commentaire

# Configuration notebook
notebook_mode = "interactive"        # "interactive" ou "batch"
skip_widgets = False               # True pour mode batch MCP
debug_level = "INFO"

# Parametres TTS
# DEUX services existent (diagnostic #13110) :
#   - TTS_GATEWAY_URL (port 8196) : gateway MULTI-modeles kokoro/tada/qwen -- c'est
#     lui que ce notebook compare ;
#   - TTS_API_URL (port 8191) : service Kokoro SEUL -- les routes /tada/* et /qwen/*
#     n'y existent pas (404) et /health n'y a pas de cle "models".
gateway_url = os.getenv("TTS_GATEWAY_URL", "http://localhost:8196")
default_voice = "alloy"              # Voix par defaut (OpenAI-compatible)
compare_all_models = True           # Comparer les 3 modeles
benchmark_latency = True            # Benchmarks de latence
save_results = True                 # Sauvegarder les resultats
# Setup environnement et imports
import os
import sys
import json
import time
import requests
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Any, Optional
import logging

import numpy as np
import soundfile as sf
from IPython.display import Audio, display, HTML

# Import helpers GenAI
GENAI_ROOT = Path.cwd()
while GENAI_ROOT.name != 'GenAI' and len(GENAI_ROOT.parts) > 1:
    GENAI_ROOT = GENAI_ROOT.parent

HELPERS_PATH = GENAI_ROOT / 'shared' / 'helpers'
if HELPERS_PATH.exists():
    sys.path.insert(0, str(HELPERS_PATH.parent))
    try:
        from helpers.audio_helpers import (
            play_audio, save_audio, display_audio_array, display_audio_bundle
        )
        print("Helpers audio importes")
    except ImportError:
        print("Helpers audio non disponibles - mode autonome")

# Repertoires
OUTPUT_DIR = GENAI_ROOT / 'outputs' / 'audio' / 'multi-tts'
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

# Configuration logging
logging.basicConfig(level=getattr(logging, debug_level))
logger = logging.getLogger('multi_tts')

print(f"Multi-Model TTS Gateway - Synthese Vocale")
print(f"Date : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Gateway : {gateway_url}")
print(f"Sortie : {OUTPUT_DIR.relative_to(GENAI_ROOT)}")
Helpers audio importes
Multi-Model TTS Gateway - Synthese Vocale
Date : 2026-08-28 18:05:24
Gateway : http://localhost:8196
Sortie : outputs\audio\multi-tts

L’environnement Python est configuré. La cellule suivante charge le fichier .env et résout l’URL du gateway : TTS_GATEWAY_URL (gateway multi-modèles, port 8196) est la cible de ce notebook ; TTS_API_URL (service Kokoro seul, port 8191) n’est qu’un fallback explicite — interroger ce dernier sur les routes /tada/* ou /qwen/* répond 404, car ces routes n’y existent pas (diagnostic #13110).

# Chargement de la configuration .env
from dotenv import load_dotenv

# Remonter jusqu'au dossier GenAI
current_path = Path.cwd()
genai_path = None

while len(current_path.parts) > 1:
    if current_path.name == "GenAI":
        genai_path = current_path
        break
    current_path = current_path.parent

if genai_path:
    env_path = genai_path / ".env"
    if env_path.exists():
        load_dotenv(env_path)
        print(f".env charge depuis: {env_path.name}")
    else:
        print("WARNING: .env non trouve dans GenAI")
else:
    print("WARNING: Dossier GenAI non trouve")

# Routage explicite (#13110) : le gateway multi-modeles est designe par
# TTS_GATEWAY_URL + TTS_GATEWAY_API_KEY. TTS_API_URL designe le service Kokoro
# seul : sur ce service, /tada/* et /qwen/* repondent 404 (routes inexistantes)
# et /health n'expose pas la cle "models".
_gw_url = os.getenv('TTS_GATEWAY_URL')
if _gw_url:
    tts_url = _gw_url
    tts_api_key = os.getenv('TTS_GATEWAY_API_KEY', '')
    tts_url_source = 'TTS_GATEWAY_URL (gateway multi-modeles)'
else:
    tts_url = os.getenv('TTS_API_URL', gateway_url)
    tts_api_key = os.getenv('TTS_API_KEY', '')
    tts_url_source = 'TTS_API_URL (fallback -- service possiblement Kokoro seul)'
    print("WARNING: TTS_GATEWAY_URL absente du .env -- fallback sur TTS_API_URL.")
    print("         Si cette URL designe le service Kokoro seul (port 8191), les")
    print("         routes /tada et /qwen n'existent pas : la comparaison restera")
    print("         limitee a Kokoro avec des verdicts RECOVERABLE pour les autres.")

tts_headers = {"Authorization": f"Bearer {tts_api_key}"} if tts_api_key else {}
print(f"TTS Gateway URL: {tts_url} (source: {tts_url_source})")
print(f"Auth: {'cle configuree (TTS_GATEWAY_API_KEY)' if tts_api_key else 'ABSENTE -- le gateway requiert une cle'}")
WARNING: .env non trouve dans GenAI
TTS Gateway URL: http://localhost:8196 (source: TTS_GATEWAY_URL (gateway multi-modeles))
Auth: cle configuree (TTS_GATEWAY_API_KEY)

Section 1 : Architecture Multi-Model TTS

Le gateway TTS multi-modèle expose 3 modèles via des paths différents :

Gateway (Port 8196)
├── /v1/audio/speech         → Kokoro TTS (default)
├── /tada/v1/audio/speech    → TADA 3B ML
└── /qwen/v1/audio/speech    → Qwen3 TTS

Modèles disponibles

Modèle VRAM Vitesse Qualite Voix Cas d’usage
Kokoro ~2GB Très rapide Bonne 6 Prototypage, gros volume
TADA 3B ML ~6GB Rapide Très bonne 1 Qualite professionnelle
Qwen3 TTS ~4GB Moyen Excellente 6+ Voice cloning, custom

Avantages de l’architecture

  1. Backward compatible : /v1/audio/speech utilise Kokoro par defaut
  2. Path-based routing : Pas de paramètre model necessaire
  3. Load balancing : Chaque modèle peut etre deploye sur des GPUs différents
  4. Lazy loading : Les modèles se chargent a la demande
  5. OpenAI-compatible : Même API que OpenAI TTS

Le diagramme ci-dessous rend le routage de la gateway TTS sous forme de graphe : un point d’entree unique dispatche vers trois moteurs selon le prefixe d’URL.

flowchart LR
    GW(["Gateway<br/>(Port 8196)"]) --> K["/v1/audio/speech<br/>Kokoro TTS (default)"]
    GW --> T["/tada/v1/audio/speech<br/>TADA 3B ML"]
    GW --> Q["/qwen/v1/audio/speech<br/>Qwen3 TTS"]
    classDef gw fill:#d1e7dd,stroke:#0f5132,color:#0a3622
    classDef eng fill:#fff3cd,stroke:#b8860b,color:#5c4400
    class GW gw
    class K,T,Q eng

Lecture. La gateway expose une API unique (port 8196) et route chaque requête vers le moteur TTS approprie selon le prefixe de chemin : /v1/... vers Kokoro (defaut), /tada/... vers TADA 3B ML, /qwen/... vers Qwen3 TTS. Ce patron de reverse proxy applicatif permet d’offrir une interface OpenAI-compatible homogene tout en multiplexant plusieurs backends specialises derriere une seule adresse.

Verification de l’etat du gateway

Avant de tester la generation TTS, il est essentiel de verifier que le gateway multi-modèle est operationnel et que tous les modèles sont correctement enregistres.

Procedure de verification : 1. Health check : Interroger l’endpoint /health pour confirmer que le gateway tourne 2. Liste des modèles : Recuperer la liste des modèles enregistres via /v1/models 3. Liste des voix : Verifier les voix disponibles pour chaque modèle via /v1/voices

Endpoints testes : - GET {gateway_url}/health : Status du gateway - GET {gateway_url}/v1/models : Modèles disponibles avec leurs paths - GET {gateway_url}/v1/voices : Voix enregistrees et compatibilites

Cette verification permet de s’assurer que l’infrastructure est prete avant de lancer les tests de generation, ce qui evite de perdre du temps a debugger des erreurs reseau ou de configuration.

# Verification du gateway et des modeles
print("VERIFICATION DU GATEWAY")
print("=" * 50)

try:
    # Health check -- deux formats possibles (#13110) :
    #   gateway multi-modeles : {"status": ..., "models": ["kokoro","tada","qwen"]}
    #   service Kokoro seul   : {"status": ..., "model": "kokoro", ...}
    response = requests.get(f"{tts_url}/health", timeout=10)
    health = response.json()
    print(f"Status: {health.get('status', 'inconnu')}")
    if 'models' in health:
        print(f"Modeles declares par le gateway: {', '.join(health['models'])}")
    elif 'model' in health:
        print(f"Service MONO-modele detecte (cle 'model'): {health['model']}")
        print("  -> un /health sans cle 'models' designe un service seul, pas le")
        print("     gateway multi-modeles : verifier TTS_GATEWAY_URL (port 8196).")
    else:
        print("Format de /health inconnu (ni 'models' ni 'model').")

    # Lister les modeles (routes reelles)
    try:
        response = requests.get(f"{tts_url}/v1/models", headers=tts_headers, timeout=10)
        models_info = response.json()
        print(f"\nModeles enregistres (routes reelles):")
        for model in models_info.get('data', []):
            print(f"  - {model['id']}: {model['name']}")
            print(f"    Path: {model['path']}")
            print(f"    Description: {model['description']}")
    except Exception as e:
        print(f"\n(listing /v1/models indisponible: {str(e)[:80]})")

    # Lister les voix
    try:
        response = requests.get(f"{tts_url}/v1/voices", headers=tts_headers, timeout=10)
        voices_info = response.json()
        print(f"\nVoix disponibles:")
        for voice in voices_info.get('voices', []):
            models = ', '.join(voice.get('models', []))
            print(f"  - {voice['id']}: {voice['name']} (modeles: {models})")
    except Exception as e:
        print(f"\n(listing /v1/voices indisponible: {str(e)[:80]})")

    print("\nGateway operationnel!")

except requests.exceptions.ConnectionError:
    print("ERREUR: Gateway non accessible")
    print(f"Verifiez que le service tourne sur {tts_url}")
except Exception as e:
    print(f"ERREUR: {str(e)[:100]}")
VERIFICATION DU GATEWAY
==================================================
Status: healthy
Modeles declares par le gateway: kokoro, tada, qwen

Modeles enregistres (routes reelles):
  - kokoro: Kokoro TTS
    Path: /v1/audio/speech
    Description: Fast, lightweight TTS model (default)
  - tada: TADA 3B ML
    Path: /tada/v1/audio/speech
    Description: HumeAI's advanced TTS model
  - qwen3: Qwen3 TTS
    Path: /qwen/v1/audio/speech
    Description: Qwen's high-quality TTS with custom voice

Voix disponibles:
  - alloy: Alloy (modeles: kokoro, tada, qwen)
  - echo: Echo (modeles: kokoro, tada, qwen)
  - fable: Fable (modeles: kokoro, tada, qwen)
  - onyx: Onyx (modeles: kokoro, tada, qwen)
  - nova: Nova (modeles: kokoro, tada, qwen)
  - shimmer: Shimmer (modeles: kokoro, tada, qwen)

Gateway operationnel!

Interpretation du health check

La cellule ci-dessus interroge le service et imprime ce qu’elle trouve – cette interpretation decrit comment lire sa sortie, elle ne recopie aucun chiffre.

Deux formats de /health existent (source du diagnostic #13110) :

Format detecte Signification Consequence pour la comparaison
cle 'models' (liste) Gateway multi-modeles (kokoro/tada/qwen, port 8196) Comparaison possible sur les 3 routes
cle 'model' (scalaire) Service mono-modele (Kokoro seul, port 8191) Routes /tada/* et /qwen/* inexistantes -> 404, comparaison limitee a Kokoro

Ce que révèle chaque cas :

  • Si la cellule affiche la liste des modeles et leurs paths (/v1/audio/speech, /tada/v1/audio/speech, /qwen/v1/audio/speech), le gateway multi-modeles est bien atteint : la comparaison de la Section 2 pourra mesurer les trois moteurs.
  • Si elle affiche Service MONO-modele detecte, la variable d’environnement TTS_GATEWAY_URL est absente et le fallback a pris le service Kokoro seul : la comparaison rendra des verdicts RECOVERABLE pour TADA et Qwen – c’est le comportement honnete, pas un echec du notebook.

Note technique : un 404 sur /tada/* ou /qwen/* signifie que la route n’existe pas sur le service interroge (routage vers le mauvais service) ; un 401 signifie que la route existe mais que la cle d’API manque. Ces deux codes racontent des problemes differents – le tableau de la Section 2 les distingue.

Exercice 1 : Monitoring du Gateway avec Health Check Automatise

Duree estimee : 10 minutes

Dans un environnement de production, il est crucial de surveiller la disponibilite des modèles TTS. Dans cet exercice, vous allez implementer une fonction qui interroge le health check du gateway a intervalles reguliers et genere un rapport de disponibilite.

Objectif : Créer check_gateway_availability() qui teste l’endpoint /health du gateway, enregistre le statut de chaque modèle (disponible/indisponible), et retourne un resume.

Indices : - # Étape 1 : Envoyer une requête GET sur {gateway_url}/health avec un timeout de 10 secondes - # Étape 2 : Parser la reponse JSON pour extraire le statut global et la liste des modèles - # Étape 3 : Construire un dictionnaire {"status": "healthy/unhealthy", "models": {"kokoro": True, "tada": False, ...}} - # Indice : Utiliser requests.get(url, timeout=10) et gerer les exceptions ConnectionError et Timeout

def check_gateway_availability(gateway_url):
    """
    Verifie la disponibilite du gateway TTS et de ses modeles.
    
    Args:
        gateway_url (str): URL de base du gateway
    
    Returns:
        dict: {"status": "healthy"/"unreachable"/"error",
               "models": {"kokoro": bool, "tada": bool, "qwen": bool},
               "response_time_s": float}
    """
    # TODO etudiant : implementer le health check
    # Etape 1 : Envoyer une requete GET sur /health avec timeout
    # Etape 2 : Parser la reponse JSON pour le statut et les modeles
    # Etape 3 : Gerer les exceptions (ConnectionError, Timeout)
    result = {"status": "unchecked", "models": {}, "response_time_s": 0.0}
    return result  # TODO etudiant : retourner le rapport

# TODO etudiant : tester le health check
# health_report = check_gateway_availability(tts_url)
# print(f"Status: {health_report['status']}")
# print(f"Response time: {health_report['response_time_s']:.2f}s")
# for model, available in health_report['models'].items():
#     print(f"  {model}: {'OK' if available else 'INDISPONIBLE'}")
print("Exercice a completer")
Exercice a completer

Section 2 : Génération et comparaison des moteurs déployés

Comparaison directe des modèles réellement déployés sur le gateway, sur un texte commun, avec essais répétés — le bilan ne retient que ce qui a tourné.

Approche de comparaison

Le code suivant mesure les moteurs réellement déployés sur le gateway, sur un texte commun et des paramètres comparables, avec essais répétés.

Methodologie de test : 1. Texte commun : le même texte est envoyé aux trois modèles (mêmes mots, même voix) — pas de variante courte par défaut 2. Essais répétés : N_TRIALS = 3 appels par modèle ; le tableau final rapporte la médiane et la dispersion — robustes à l’essai de chauffe (premier appel = chargement du modèle en VRAM) 3. Routage par path : chaque modèle est contacté sur son endpoint spécifique (pas de paramètre model) 4. Gestion d’erreur : timeout spécifique par modèle (runtime machine-dep, incluant le chargement à froid : 120s Kokoro, 240s TADA, 300s Qwen3) ; un essai échoué n’interrompt ni les essais suivants ni les modèles suivants 5. Bilan honnête : le tableau est généré depuis results — statut HTTP, latence, durée audio, RTF, taille — et le compte final ne retient que les moteurs réellement testés avec succès ; un moteur absent reçoit un verdict RECOVERABLE-*

Paramètres de configuration : - sample_text : texte commun (~220 caractères) pour les trois modèles - default_voice : voix “alloy” (compatible OpenAI) - timeout : adapté à la vitesse de chargement de chaque modèle

Résultats attendus : - Tableau comparatif auto-généré avec médianes et dispersions - Fichiers audio sauvegardés dans outputs/audio/multi-tts/ - Lecteurs audio embarqués (aperçus ~3s) pour écoute directe

# Comparaison des modeles TTS reellement deployes -- texte commun, essais repetes
import io

print("COMPARAISON DES MODELES TTS")
print("=" * 50)

# Texte COMMUN aux trois modeles : memes mots, memes parametres (comparabilite).
sample_text = (
    "L'intelligence artificielle transforme la facon dont nous "
    "interagissons avec la technologie. La synthese vocale est "
    "devenue si naturelle qu'il est parfois difficile de distinguer "
    "une voix humaine d'une voix artificielle."
)

# Essais repetes : la mediane est robuste a l'essai de chauffe (le premier appel
# d'un moteur peut payer son chargement en VRAM), la dispersion le montre.
# timeout genereux : chargement a froid possible selon l'activite GPU de la machine.
N_TRIALS = 3
models_config = {
    "kokoro": {
        "url": f"{tts_url}/v1/audio/speech",
        "name": "Kokoro",
        "description": "Modele leger et rapide (propriete documentee)",
        "timeout": 120,
    },
    "tada": {
        "url": f"{tts_url}/tada/v1/audio/speech",
        "name": "TADA 3B ML",
        "description": "Modele avance de HumeAI (propriete documentee)",
        "timeout": 240,
    },
    "qwen": {
        "url": f"{tts_url}/qwen/v1/audio/speech",
        "name": "Qwen3 TTS",
        "description": "Modele haute qualite de Qwen (propriete documentee)",
        "timeout": 300,
    },
}

results = {}

for model_key, config in models_config.items():
    print(f"\n--- {config['name']} ---")
    print(f"Description (documentee) : {config['description']}")
    trials = []
    kept = None
    for trial in range(1, N_TRIALS + 1):
        try:
            start_time = time.time()
            # Pas de parametre "model" : le routage se fait par le path
            response = requests.post(
                config['url'],
                json={"input": sample_text, "voice": default_voice},
                headers=tts_headers,
                timeout=config['timeout'],
            )
            elapsed = time.time() - start_time
            content_type = response.headers.get('content-type', '')

            if response.status_code == 200 and 'json' not in content_type:
                audio_bytes = response.content
                audio_data, sample_rate = sf.read(io.BytesIO(audio_bytes))
                duration = len(audio_data) / sample_rate
                trials.append({"status": 200, "latency_s": elapsed,
                               "duration_s": duration,
                               "size_kb": len(audio_bytes) / 1024})
                if kept is None:
                    kept = (audio_bytes, audio_data, sample_rate)
                print(f"  essai {trial}/{N_TRIALS} : OK  latence={elapsed:.2f}s "
                      f"audio={duration:.1f}s ({len(audio_bytes)/1024:.1f} KB)")
            elif response.status_code == 200:
                trials.append({"status": -1, "latency_s": elapsed})
                print(f"  essai {trial}/{N_TRIALS} : reponse JSON inattendue -- "
                      f"{str(response.json())[:120]}")
            else:
                trials.append({"status": response.status_code, "latency_s": elapsed})
                print(f"  essai {trial}/{N_TRIALS} : ECHEC HTTP {response.status_code} -- "
                      f"{response.text[:120]}")
        except requests.exceptions.Timeout:
            trials.append({"status": "timeout", "latency_s": config['timeout']})
            print(f"  essai {trial}/{N_TRIALS} : TIMEOUT (> {config['timeout']}s)")
        except Exception as e:
            print(f"  essai {trial}/{N_TRIALS} : ERREUR {str(e)[:100]}")

    ok_trials = [t for t in trials if t.get("status") == 200]
    results[model_key] = {
        "trials": trials,
        "n_ok": len(ok_trials),
        "n_trials": N_TRIALS,
        "text_len": len(sample_text),
    }

    if kept is not None:
        audio_bytes, audio_data, sample_rate = kept
        results[model_key].update({
            "audio": audio_bytes,
            "duration": ok_trials[0]["duration_s"],
            "sample_rate": sample_rate,
        })
        # ~3s apercu (convention audio-roll) ; audio complet sauvegarde sur disque
        try:
            _preview = audio_data[:int(sample_rate * 3)]
            _buf = io.BytesIO()
            sf.write(_buf, _preview, sample_rate, format="WAV")
            display_audio_bundle(_buf.getvalue())
        except NameError:
            print("  (helpers audio indisponibles : pas d'apercu inline)")
        if save_results:
            filepath = OUTPUT_DIR / f"{model_key}_comparison.wav"
            with open(filepath, 'wb') as f:
                f.write(audio_bytes)
            print(f"  Sauvegarde : {filepath.name}")
COMPARAISON DES MODELES TTS
==================================================

--- Kokoro ---
Description (documentee) : Modele leger et rapide (propriete documentee)
  essai 1/3 : OK  latence=13.83s audio=13.9s (652.8 KB)
  essai 2/3 : OK  latence=2.89s audio=13.9s (652.8 KB)
  essai 3/3 : OK  latence=2.99s audio=13.9s (652.8 KB)
  Sauvegarde : kokoro_comparison.wav

--- TADA 3B ML ---
Description (documentee) : Modele avance de HumeAI (propriete documentee)
  essai 1/3 : OK  latence=9.51s audio=12.3s (574.7 KB)
  essai 2/3 : OK  latence=8.90s audio=16.0s (749.1 KB)
  essai 3/3 : OK  latence=9.74s audio=13.9s (649.7 KB)
  Sauvegarde : tada_comparison.wav

--- Qwen3 TTS ---
Description (documentee) : Modele haute qualite de Qwen (propriete documentee)
  essai 1/3 : OK  latence=40.18s audio=14.1s (660.0 KB)
  essai 2/3 : OK  latence=42.86s audio=15.9s (746.3 KB)
  essai 3/3 : OK  latence=43.02s audio=16.0s (750.0 KB)
  Sauvegarde : qwen_comparison.wav

Lecture des resultats de generation

Les chiffres ci-dessus sont les mesures du run courant. Cette section explique comment les lire – elle ne recopie aucun statut ni aucun chiffre : le tableau comparatif suivant est genere automatiquement depuis results.

Documente vs mesure :

  • Les descriptions des modeles (« leger et rapide », « avance », « haute qualite ») sont des proprietes documentees – des fiches constructeur, pas des observations de ce run.
  • Les colonnes du tableau (statut HTTP, latence, duree audio, ratio temps reel, taille) sont des observations mesurees dans ce notebook, sur le texte commun, avec essais repetes.

Si un moteur echoue (404, 401, timeout), le tableau affiche le code d’echec et un verdict RECOVERABLE-MACHINE, et le bilan final compte uniquement les moteurs reellement testes avec succes. Un 404 sur une route prefixee signifie que la route n’existe pas sur le service interroge (cf. routage TTS_GATEWAY_URL vs TTS_API_URL en tete de notebook) ; un 401 signale une cle d’API manquante.

Robustesse du code de comparaison :

  • requests.exceptions.Timeout capture les depassements de delai (timeout par modele)
  • status_code != 200 capture les erreurs HTTP sans interrompre les modeles suivants
  • la verification content-type evite de lire du JSON comme de l’audio
  • chaque moteur garde ses essais separes : un essai echoue ne fausse pas la mediane

Note pedagogique : un modele en panne ne doit pas faire echouer tout le systeme – le pattern « collecter l’echec, continuer, reverdiger le bilan sur ce qui a tourne » est exactement ce qu’on attend d’une application de production.

La cellule suivante construit le tableau comparatif entièrement depuis results : statut HTTP par essai, latence médiane et dispersion (essais répétés), durée audio médiane, ratio temps réel, taille — et le nombre de moteurs réellement testés avec succès. Aucune ligne de ce tableau n’est écrite à la main.

# Tableau comparatif -- GENERE AUTOMATIQUEMENT depuis results (aucun chiffre recopie)
import statistics

print("\n" + "=" * 95)
print("TABLEAU COMPARATIF (mesures du run courant, genere depuis results)")
print("=" * 95)

print(f"{'Modele':<12} {'Essais OK':<10} {'Latence med':<12} {'Dispersion':<16} "
      f"{'Audio med':<11} {'RTF':<7} {'KB med':<9}")
print("-" * 95)

n_ok_models = 0
for model_key, data in results.items():
    ok = [t for t in data['trials'] if t.get('status') == 200]
    name = models_config[model_key]['name']
    if ok:
        n_ok_models += 1
        lats = [t['latency_s'] for t in ok]
        durs = [t['duration_s'] for t in ok]
        sizes = [t['size_kb'] for t in ok]
        med_l, med_d = statistics.median(lats), statistics.median(durs)
        if len(lats) > 1:
            disp = f"+{(max(lats) - med_l):.2f}/-{(med_l - min(lats)):.2f}s"
        else:
            disp = "n/a (1 essai)"
        rtf = med_d / med_l if med_l > 0 else 0.0
        print(f"{name:<12} {len(ok)}/{data['n_trials']:<7} {med_l:<12.2f} {disp:<16} "
              f"{med_d:<11.1f} {rtf:<7.1f} {statistics.median(sizes):<9.1f}")
    else:
        codes = sorted({str(t.get('status')) for t in data['trials']})
        verdict = ("RECOVERABLE-MACHINE (route inexistante : mauvais service interroge)"
                   if any(c == '404' for c in codes)
                   else "RECOVERABLE-MACHINE (route joignable mais echec/timeout)")
        print(f"{name:<12} 0/{data['n_trials']:<7} -- code(s) {','.join(codes)} : {verdict}")

print("-" * 95)
print(f"Modeles reellement testes avec succes : {n_ok_models}/{len(results)}")

if n_ok_models > 1:
    lats = {models_config[k]['name']: statistics.median(
        [t['latency_s'] for t in results[k]['trials'] if t.get('status') == 200])
        for k in results if results[k]['n_ok'] > 0}
    fastest = min(lats, key=lats.get)
    print(f"Latence mediane la plus basse : {fastest} ({lats[fastest]:.2f}s)")

print("\nNote : aucun classement de qualite/naturalite ici -- cela exigerait un")
print("protocole d'ecoute (MOS) ou une metrique definie, hors scope de ce notebook.")

===============================================================================================
TABLEAU COMPARATIF (mesures du run courant, genere depuis results)
===============================================================================================
Modele       Essais OK  Latence med  Dispersion       Audio med   RTF     KB med   
-----------------------------------------------------------------------------------------------
Kokoro       3/3       2.99         +10.84/-0.11s    13.9        4.7     652.8    
TADA 3B ML   3/3       9.51         +0.23/-0.61s     13.9        1.5     649.7    
Qwen3 TTS    3/3       42.86        +0.16/-2.68s     15.9        0.4     746.3    
-----------------------------------------------------------------------------------------------
Modeles reellement testes avec succes : 3/3
Latence mediane la plus basse : Kokoro (2.99s)

Note : aucun classement de qualite/naturalite ici -- cela exigerait un
protocole d'ecoute (MOS) ou une metrique definie, hors scope de ce notebook.

Interpretation de la comparaison

La lecture se fait sur les colonnes mesurées du tableau ci-dessus — cette interprétation décrit les colonnes, elle ne récite pas leurs valeurs (elles sont celles du run courant, imprimées par la cellule précédente).

  • Essais OK : nombre d’essais réussis sur le total tenté. Un moteur à 0 essai reçoit un verdict RECOVERABLE-MACHINE et reste hors du bilan comparatif — on ne compare pas un moteur qu’on n’a pas mesuré.
  • Latence (médiane ± dispersion) : temps de génération sur les essais réussis. La médiane est robuste à l’essai de chauffe — le premier appel d’un moteur peut payer son chargement en VRAM, la dispersion le révèle (une forte dispersion asymétrique signale un essai froid).
  • Ratio temps réel (RTF) : durée audio produite ÷ latence. RTF > 1 : le moteur produit l’audio plus vite que sa durée d’écoute.
  • KB méd : taille de l’audio produit — utile pour estimer un débit de stockage.

Propriétés documentées ≠ observations : « leger/rapide » (Kokoro), « avancé » (TADA), « haute qualité » (Qwen3) sont des fiches de modèle. Seules les colonnes du tableau sont mesurées ici. Aucun classement de qualité/naturalité n’est émis : cela exigerait un protocole d’écoute (type MOS) ou une métrique objective définie, hors scope de ce notebook.

Choix de modèle en production : le critère mesuré pertinent dépend du cas d’usage — latence (interaction vocale) vs RTF (génération en masse). La Section 3 donne l’arbre de décision documenté, à croiser avec ces mesures sur votre matériel.

Exemple guide 1 : Comparaison personnalisee avec paramètres

L’exemple ci-dessus compare les modèles déployés avec le texte commun et des paramètres par défaut. Votre mission : testez les modèles avec des textes plus longs et des paramètres différents pour comprendre l’impact sur la qualite et le temps de generation.

Indices : - Utilisez un texte de 100+ mots (extrait de Wikipedia ou autre) - Testez avec speed différents (0.5, 1.0, 1.5, 2.0) pour Kokoro - Mesurez le runtime machine-dep : temps de generation pour chaque configuration - Observez comment la longueur du texte affecte le ratio real-time

# --- Exercice 1 : Comparaison personnalisee ---

def compare_tts_long_text(text, models=None, speed_values=None):
    """
    Compare les modeles TTS avec un texte long et des vitesses differentes.
    Retourne un DataFrame avec les resultats.
    """
    # Exercice: Pour chaque modele, generer l'audio avec le texte long
    # Exercice: Pour Kokoro, tester differentes valeurs de speed
    # Exercice: Mesurer le temps de generation et la duree audio
    # Exercice: Calculer le ratio real-time pour chaque configuration
    pass

# Test
# long_text = "Votre texte de 100+ mots ici..."
# df = compare_tts_long_text(long_text)
# print(df.to_string())

Section 3 : Guide de Sélection de Modèle

Cette section est un guide documenté (fiches de modèles, arbres de décision) — à croiser avec les observations mesurées de la Section 2 sur votre matériel.

Arbre de decision

Besoin TTS
    │
    ├─ Prototypage / Tests → Kokoro (rapide, gratuit)
    ├─ Production standard → TADA 3B ML (qualite, vitesse)
    ├─ Haute qualite → Qwen3 TTS (meilleure naturalite)
    ├─ Gros volume → Kokoro (pas de cout per-token)
    └─ Voice cloning → Qwen3 TTS (custom voices)

Cas d’usage par modèle

Cas d’usage Modèle recommande Pourquoi
Prototypage rapide Kokoro Instantane, pas d’attente
Audiobooks TADA 3B ML Voix claire, professionnelle
Chatbots vocaux Kokoro Reponse rapide, faible latence
Voice cloning Qwen3 TTS Support voix customisees
Applications mobiles Kokoro Modèle leger, fonctionne sur CPU
Contenu marketing Qwen3 TTS Meilleure qualite perceptuelle

Transition vers l’integration

Maintenant que nous avons compare les 3 modèles et compris leurs forces/faiblesses, passons a l’integration pratique. La section suivante montre comment encapsuler la logique de routing dans un client Python reutilisable, puis comment automatiser la sélection du modèle avec un routeur intelligent.

Points abordes : - Client Python multi-modèle avec gestion d’erreurs - Architecture orientee objet pour faciliter l’integration - Routeur intelligent qui selectionne le meilleur modèle automatiquement

Section 4 : Integration dans une Application

Exemple de code Python

import os
import requests

class MultiModelTTSClient:
    """Client TTS multi-modèle."""
    
    def __init__(self, gateway_url="http://localhost:8196"):
        self.gateway_url = gateway_url
        self.api_key = os.getenv("TTS_GATEWAY_API_KEY", "")
        self.models = {
            "kokoro": "/v1/audio/speech",
            "tada": "/tada/v1/audio/speech",
            "qwen": "/qwen/v1/audio/speech"
        }
    
    def generate(self, text: str, model: str = "kokoro", voice: str = "alloy"):
        """Generer de la parole avec le modèle specifie."""
        url = self.gateway_url + self.models[model]
        
        response = requests.post(
            url,
            json={"input": text, "voice": voice},
            headers={"Authorization": f"Bearer {self.api_key}"},
            timeout=60
        )
        
        if response.status_code == 200:
            return response.content
        else:
            raise Exception(f"TTS error: {response.status_code}")

# Utilisation
client = MultiModelTTSClient()
audio = client.generate("Bonjour!", model="tada", voice="alloy")

Note : l’URL par défaut est le gateway multi-modèles local (port 8196, variable TTS_GATEWAY_URL) ; la clé vient de TTS_GATEWAY_API_KEY.

# Statistiques de session
print("STATISTIQUES DE SESSION")
print("=" * 50)

print(f"Date : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Gateway : {tts_url}")
print(f"Modeles testes avec succes : "
      f"{sum(1 for d in results.values() if d['n_ok'] > 0)}/{len(results)} "
      f"(essais OK : {sum(d['n_ok'] for d in results.values())}/"
      f"{sum(d['n_trials'] for d in results.values())})")

if save_results:
    saved = list(OUTPUT_DIR.glob('*'))
    total_size = sum(f.stat().st_size for f in saved) / (1024**2)
    print(f"Fichiers sauvegardes : {len(saved)} ({total_size:.1f} MB)")

print(f"\nPROCHAINES ETAPES")
print(f"1. Integrer le TTS dans une application FastAPI (03-1)")
print(f"2. Construire un pipeline vocal complet (03-2)")
print(f"3. Explorer le voice cloning avec XTTS (02-2)")
print(f"4. Comparer tous les modeles TTS et STT (03-1)")

print(f"\nNotebook Multi-Model TTS termine - {datetime.now().strftime('%H:%M:%S')}")
STATISTIQUES DE SESSION
==================================================
Date : 2026-08-28 18:08:25
Gateway : http://localhost:8196
Modeles testes avec succes : 3/3 (essais OK : 9/9)
Fichiers sauvegardes : 3 (1.8 MB)

PROCHAINES ETAPES
1. Integrer le TTS dans une application FastAPI (03-1)
2. Construire un pipeline vocal complet (03-2)
3. Explorer le voice cloning avec XTTS (02-2)
4. Comparer tous les modeles TTS et STT (03-1)

Notebook Multi-Model TTS termine - 18:08:25

Conclusion du Notebook

Ce notebook a presente l’architecture multi-modèle TTS avec un gateway centralise qui permet de router les requêtes vers le modèle le plus adapte au cas d’usage.

Points cles retenus

Concept Description
Path-based routing Chaque modèle a son propre endpoint (/v1/audio/speech, /tada/v1/audio/speech, /qwen/v1/audio/speech)
Routage d’environnement TTS_GATEWAY_URL (8196) = gateway multi-modèles ; TTS_API_URL (8191) = service Kokoro seul — les routes /tada/*, /qwen/* y répondent 404
Bilan généré, pas recopié Le tableau comparatif est construit depuis results : statut HTTP, latence médiane ± dispersion, RTF, nombre de moteurs réellement testés
Documenté ≠ mesuré Les fiches des modèles sont des propriétés documentées ; seules les colonnes du tableau sont des observations de ce run
Integration simple Client Python avec méthode generate(text, model, voice)

Tableau decisionnel (documenté)

Scénario Modèle Rationale
Prototypage rapide Kokoro Generation instantanee, faible latence
Production standard TADA 3B ML Equilibre qualite/vitesse
Haute qualite Qwen3 TTS Meilleure naturalite, support voix custom
Gros volume Kokoro Pas de cout per-token, leger
Applications mobiles Kokoro Compatible CPU, faible footprint

À croiser avec les mesures de ce run (Section 2) sur votre matériel.

Prochaines étapes

  1. Integration API : Construire un endpoint FastAPI qui expose le gateway TTS (notebook 03-1)
  2. Pipeline vocal complet : Combiner STT (Whisper/Qwen ASR) + TTS pour une application de dialogue (notebook 03-2)
  3. Voice cloning : Explorer XTTS pour créer des voix personnalisees (notebook 02-2)
  4. Optimisation : Implementer le cache audio pour reduire la latence sur les textes repetitifs

Note technique : L’authentification par API key (header Authorization) est necessaire pour les appels au gateway. Configurez la variable d’environnement TTS_GATEWAY_API_KEY (et TTS_GATEWAY_URL) dans le fichier .env du dossier GenAI.

Exercice 2 : Routage intelligent du gateway TTS

Un gateway TTS doit choisir le meilleur modèle selon les caractéristiques de la demande (langue, qualite, latence). Implementez une fonction de routage.

Indice : Définir des critères de sélection (langue supportee, qualite requise) et retourner le nom du modèle.

def route_tts_request(text, target_lang, quality="standard", max_latency_ms=500):
    # Etape 1 : Lister les modeles disponibles et leurs capacites
    # Etape 2 : Filtrer par langue supportee
    # Etape 3 : Trier par qualite et latence
    pass

result = None  # TODO etudiant
print("Exercice a completer")
Exercice a completer

Exercice 3 : Benchmark de latence avec retry

Mesurer la latence reelle de chaque endpoint TTS avec un mécanisme de retry en cas d’echec.

Indice : Utiliser time.time() pour mesurer chaque appel et implementer un compteur de retry.

def benchmark_tts_latency(endpoints, text, n_trials=3, max_retries=2):
    # Etape 1 : Pour chaque endpoint, faire n_trials appels
    # Etape 2 : Implementer le retry sur echec
    # Etape 3 : Calculer les metriques de latence
    pass

result = None  # TODO etudiant
print("Exercice a completer")
Exercice a completer

Exemple guide 2 : Routeur TTS intelligent

La Section 3 presente un arbre de decision pour choisir le modèle adapte. Votre mission : implementez une fonction smart_tts_router() qui selectionne automatiquement le meilleur modèle et genere l’audio.

Indices : - Critères de sélection : longueur du texte, langue, besoin de clonage vocal, contrainte de temps - Texte court (< 50 mots) + rapidite -> Kokoro - Texte long + qualite -> Qwen3 TTS - Clonage vocal -> Qwen3 TTS (paramètre voice) - Ajoutez un fallback si un modèle est indisponible

# --- Exercice 2 : Routeur TTS intelligent ---

def smart_tts_router(text, voice_clone=None, max_wait_seconds=30):
    """
    Selectionne automatiquement le meilleur modele TTS et genere l'audio.
    Criteres : longueur, clonage vocal, contrainte de temps.
    Retourne (audio_bytes, model_used, generation_time).
    """
    # Exercice: Verifier la disponibilite de chaque modele
    # Exercice: Estimer le temps de generation selon la longueur du texte
    # Exercice: Si voice_clone demande -> Qwen3 TTS
    # Exercice: Si texte court + rapide -> Kokoro
    # Exercice: Si texte long + qualite -> Qwen3 ou TADA
    # Exercice: Implementer un fallback si le modele choisi echoue
    pass

# Test
# audio, model, t = smart_tts_router("Bonjour le monde")
# print(f"Modele: {model}, Temps: {t:.1f}s")

Pistes de solution pour le routeur intelligent :

Pour implementer le routeur TTS intelligent, voici les étapes cles :

1. Verification de disponibilite

def check_model_health(model_url):
    try:
        response = requests.get(f"{model_url}/health", timeout=5)
        return response.status_code == 200
    except:
        return False

2. Estimation du temps de generation - Kokoro : runtime machine-dep : ~0.1s par mot (ratio real-time ~10x) - TADA 3B ML : runtime machine-dep : ~0.2s par mot (ratio real-time ~5x) - Qwen3 TTS : runtime machine-dep : ~1s par mot (ratio real-time ~1x)

3. Logique de sélection

word_count = len(text.split())
has_voice_clone = voice_clone is not None
urgent = max_wait_seconds < (word_count * 0.5)  # Seuil arbitraire

if has_voice_clone:
    return "qwen"  # Seul Qwen supporte le voice cloning
elif urgent or word_count < 50:
    return "kokoro"  # Plus rapide
elif word_count > 200:
    return "qwen"  # Meilleure qualite pour texte long
else:
    return "tada"  # Compromis

4. Fallback - Si le modèle choisi echoue (timeout, erreur), retenter avec Kokoro - Kokoro est le plus stable et le plus rapide, donc bon fallback

Note technique : Ajoutez un système de cache pour les textes repetitifs. Utilisez un hash du texte comme cle de cache pour eviter de regenerer le même audio.

Retour au sommet