diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5ea4785..e0a7b3e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -68,46 +68,148 @@ Chaque backend implemente ce trait. Le dispatch se fait via `get_backend(name)` - **macOS**: `say` (zero latence, voix systeme) - **Linux / Windows**: `kokoro` (Rust pur, pas de dependance Python) +## SpeakOptions + +Structure centrale passee a chaque backend via `TtsBackend::speak()`. Tous les champs sont optionnels — les backends ignorent ceux qu'ils ne supportent pas. + +```rust +pub struct SpeakOptions { + pub voice: Option, // Nom de voix (ex: "Chelsie", "af_heart") + pub lang: Option, // Code langue ISO (ex: "fr", "en") + pub rate: Option, // Debit en mots/min (say uniquement) + pub gender: Option, // "feminine" | "masculine" + pub style: Option, // "calm" | "energetic" | "warm" | ... + pub ref_audio: Option, // Chemin audio pour voice cloning + pub ref_text: Option, // Transcription de l'audio de reference + pub model: Option, // Model ID (ex: Qwen/Qwen3-TTS-12Hz-0.6B-Base) +} +``` + +**Resolution de priorite** : flags CLI / params MCP > preferences DB > valeurs par defaut du backend. + +## Resolution du voice cloning + +Quand un utilisateur demande `-v patrick` (ou `voice: "patrick"` via MCP), le systeme : + +1. Cherche un clone nomme `patrick` dans la table `voice_clones` via `clone::resolve_voice()` +2. Si trouve : extrait `ref_audio` et `ref_text` du clone +3. Verifie le backend courant — si c'est `say` ou `kokoro` (qui ne supportent pas le cloning), **bascule automatiquement** : + - **macOS** : vers `qwen` (MLX-Audio Python) + - **Linux / Windows** : vers `qwen-native` (Rust pur) +4. Met `voice = None` (ne pas passer le nom du clone comme voix au backend) +5. Passe `ref_audio` + `ref_text` dans `SpeakOptions` + +Ce mecanisme est identique dans le CLI (`main.rs::handle_speak`) et le serveur MCP (`mcp.rs::tool_speak`). + +## Playback audio asynchrone (PlayHandle) + +Le module `audio.rs` fournit deux modes de lecture via `rodio` : + +- **`play_audio_blocking(path)`** — bloque le thread jusqu'a la fin de la lecture. Supporte WAV, MP3, OGG, FLAC. +- **`play_wav_async(path)`** — lance la lecture dans un thread et retourne un `PlayHandle`. + +```rust +pub struct PlayHandle { + join: Option>>, +} + +impl PlayHandle { + pub fn wait(self) -> Result<()>; // Bloque jusqu'a la fin +} + +impl Drop for PlayHandle { + fn drop(&mut self); // Attend la fin du thread au drop +} +``` + +Le pattern `PlayHandle` est utilise par le backend `qwen` pour le pipeline de chunking : pendant que le chunk N est joue, le chunk N+1 est genere en parallele. + +## Pipeline de decoupage par phrases (qwen backend) + +Le backend `qwen` decoupe le texte long en phrases pour reduire la latence percue : + +1. **Split** : decoupe sur `.` `!` `?` `;` +2. **Merge** : fusionne les petites phrases consecutives tant que `len < MIN_CHUNK_CHARS` (120 caracteres) — pour reduire le nombre d'appels subprocess Python +3. **Pipeline** : + - Si 1 seul chunk : appel direct avec `--play --stream` (latence optimale) + - Si N chunks : pipeline chevauche (overlap generation + playback) : + - Genere chunk 0 → joue chunk 0 (async) + genere chunk 1 en parallele + - Quand chunk 1 genere → attend fin chunk 0 → joue chunk 1 + genere chunk 2... + - Resultat : la latence inter-chunks est masquee + ## Base de donnees SQLite via `rusqlite` avec WAL mode. Fichier: `~/.config/vox/vox.db`. -### Schema +### Schema DDL complet ```sql --- Preferences utilisateur (une seule ligne, UPSERT) -CREATE TABLE preferences ( - id INTEGER PRIMARY KEY CHECK (id = 1), - backend TEXT, voice TEXT, lang TEXT, rate INTEGER, - gender TEXT, style TEXT, model TEXT, pack TEXT +CREATE TABLE IF NOT EXISTS preferences ( + id INTEGER PRIMARY KEY CHECK (id = 1), + backend TEXT, + voice TEXT, + lang TEXT, + rate INTEGER, + gender TEXT, + style TEXT, + model TEXT ); - --- Voice clones -CREATE TABLE voice_clones ( - name TEXT PRIMARY KEY, - ref_audio TEXT NOT NULL, - ref_text TEXT, - created_at TEXT DEFAULT (datetime('now')) +-- Migration ajoutee dynamiquement pour les bases existantes : +-- ALTER TABLE preferences ADD COLUMN pack TEXT; + +CREATE TABLE IF NOT EXISTS usage_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%S','now')), + backend TEXT NOT NULL, + voice TEXT, + lang TEXT, + text_len INTEGER NOT NULL, + duration_ms INTEGER ); --- Journal d'utilisation -CREATE TABLE usage_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - timestamp TEXT DEFAULT (datetime('now')), - backend TEXT NOT NULL, - voice TEXT, lang TEXT, - text_len INTEGER NOT NULL, - duration_ms INTEGER +CREATE TABLE IF NOT EXISTS voice_clones ( + name TEXT PRIMARY KEY, + ref_audio TEXT NOT NULL, + ref_text TEXT, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%S','now')) ); ``` +**Migration** : la colonne `pack` sur `preferences` est ajoutee via `ALTER TABLE` si absente (detection par `SELECT pack FROM preferences LIMIT 0`). Cela permet la compatibilite avec les bases creees avant cette fonctionnalite. + +**UPSERT** : `set_preference()` insere d'abord une ligne vide avec `ON CONFLICT(id) DO NOTHING`, puis fait `UPDATE`. La contrainte `CHECK (id = 1)` garantit une seule ligne. + ### Requetes d'agregation -- `get_usage_summary()` — total calls + total chars -- `get_backend_stats()` — calls, chars, duration par backend -- `get_lang_stats()` — calls par langue -- `get_total_duration_ms()` — temps de parole cumule -- `get_usage_stats()` — 50 dernieres entrees +| Fonction | Requete | Retour | +|----------|---------|--------| +| `get_usage_summary()` | `SELECT COUNT(*), SUM(text_len)` | `(u64, u64)` — total calls + total chars | +| `get_backend_stats()` | `GROUP BY backend` + COUNT/SUM | `Vec` — calls, chars, duration par backend | +| `get_lang_stats()` | `GROUP BY lang` + COUNT | `Vec` — calls par langue | +| `get_total_duration_ms()` | `SUM(duration_ms)` | `u64` — temps de parole cumule en ms | +| `get_usage_stats()` | `ORDER BY id DESC LIMIT 50` | `Vec` — 50 dernieres entrees | + +## Strategie de securite + +| Vecteur | Protection | Implementation | +|---------|-----------|----------------| +| SQL injection | Parametres lies (`?1`, `?2`, ...) | `rusqlite::params![]` partout, jamais d'interpolation de valeurs utilisateur | +| Cles de preferences invalides | Whitelist validee | `set_preference()` valide `key` contre `["backend", "voice", "lang", "rate", "gender", "style", "model", "pack"]` | +| Valeurs de preferences invalides | Validation par type/enum | `gender` → `Gender::parse()`, `style` → `IntonationStyle::parse()`, `rate` → `parse::()`, `lang` → `SUPPORTED_LANGS.contains()`, `backend` → whitelist plateforme | +| Path traversal (audio) | Extensions validees | `validate_audio()` verifie existence + extension dans `[wav, mp3, flac, ogg, m4a]` | +| Injection shell | Pas de `sh -c` | Toutes les commandes externes via `std::process::Command` avec args separes | +| Backends invalides | Enum par plateforme | macOS: `[kokoro, say, qwen, qwen-native]`, autres: `[kokoro, qwen-native]` | + +## Latence par backend : cold start vs warm start + +| Backend | Cold start | Warm start | Notes | +|---------|-----------|------------|-------| +| `say` | ~100ms | ~100ms | Pas de modele a charger, appel systeme direct | +| `kokoro` | ~5-8s | ~2-5s | Chargement ONNX + voices.bin via Python. Pas de cache persistent | +| `qwen` | ~5-15s | ~1-2s | Cold = telechargement modele (~1.2 GB) + chargement Python. Warm = Python startup seul | +| `qwen-native` | ~10-30s | ~2-5s | Cold = telechargement HuggingFace + chargement candle. Warm = modele en memoire (`static Mutex>`) | + +**Note** : `qwen-native` garde le modele en memoire via un `Mutex` global — les appels suivants au meme processus (ex: serveur MCP) sont donc en warm start. Les appels CLI individuels sont toujours en cold start. ## Protocole MCP diff --git a/docs/FEATURES.md b/docs/FEATURES.md index a83c978..c11d765 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -67,10 +67,39 @@ vox config set gender feminine # Genre vocal vox config set style warm # Style d'intonation vox config set rate 180 # Debit (say uniquement) vox config set model # Modele TTS specifique +vox config set pack peon # Sound pack actif vox config reset # Reinitialiser tout ``` -Priorite de resolution : **flags CLI > preferences DB > valeurs par defaut**. +Priorite de resolution : **flags CLI / params MCP > preferences DB > valeurs par defaut**. + +### Reference des cles de preferences + +| Cle | Valeurs acceptees | Validation | +|-----|-------------------|-----------| +| `backend` | macOS: `kokoro`, `say`, `qwen`, `qwen-native` / Autres: `kokoro`, `qwen-native` | Whitelist par plateforme | +| `voice` | Nom de voix ou de clone (texte libre) | Aucune (le backend valide au moment du speak) | +| `lang` | `en`, `fr`, `es`, `de`, `it`, `pt`, `zh`, `ja`, `ko`, `ru`, `ar`, `nl` | Validation contre `SUPPORTED_LANGS` | +| `rate` | Entier positif (mots/min, ex: `150`, `200`) | Parse en `u32`, erreur si non-numerique | +| `gender` | `feminine`, `masculine` | Parse via `Gender::parse()`, erreur sinon | +| `style` | `calm`, `energetic`, `warm`, `authoritative`, `cheerful`, `serious` | Parse via `IntonationStyle::parse()`, erreur sinon | +| `model` | ID de modele HuggingFace (texte libre, ex: `mlx-community/Qwen3-TTS-12Hz-0.6B-Base-4bit`) | Aucune (le backend valide au chargement) | +| `pack` | Nom de pack installe (texte libre) | Aucune (verifie a l'utilisation) | + +### Matrice des capacites par backend + +| Capacite | `say` | `kokoro` | `qwen` | `qwen-native` | +|----------|-------|----------|--------|----------------| +| Voice cloning | Non | Non | Oui | Oui | +| Rate (debit) | Oui (`-r`) | Non | Non | Non | +| Gender hint | Non | Non | Oui | Oui | +| Style hint | Non | Non | Oui | Oui | +| Choix de voix | Oui (voix Apple) | Oui (prefixe `xx_nom`) | Oui (Chelsie, Aidan, Luna, Ryan) | Non (clones uniquement) | +| Choix de modele | Non | Non | Non | Oui | +| Langues | Toutes (via voix) | en, fr, ja, zh, ko, hi, it, pt, de, es | en, fr, es, de, it, pt, zh, ja, ko, ru, ar, nl | en, fr, es, de, it, pt, zh, ja, ko, ru | +| Plateforme | macOS | Toutes | macOS (Apple Silicon) | Toutes | +| GPU | Non | Non | Non (CPU MLX) | Metal (macOS) / CUDA (Linux) | +| Dependance externe | Aucune | `kokoro-onnx`, `soundfile` (Python) | `mlx-audio` (Python) | Aucune (Rust pur) | ## Sound packs @@ -85,7 +114,7 @@ vox pack play error -p peon_fr # Jouer depuis un pack specifique vox pack remove peon # Desinstaller un pack ``` -Categories de sons : greeting, acknowledge, complete, error, permission, annoyed. +Categories de sons : greeting, acknowledge, complete, error, permission, resource_limit, annoyed. ## Statistiques d'utilisation @@ -135,6 +164,15 @@ vox init -m all # Les trois modes L'init est idempotent : relancer `vox init` ne duplique pas les configurations. +### Comparaison des modes d'init + +| Mode | Ce qu'il fait | Quand l'utiliser | +|------|--------------|-----------------| +| `mcp` (defaut) | Configure le serveur MCP dans les fichiers de config de 14 outils IA | L'assistant appelle `vox_speak`, `vox_hear`, etc. nativement via le protocole MCP. Meilleure integration. | +| `cli` | Cree `CLAUDE.md` + hook `Stop` dans `.claude/settings.json` | L'assistant appelle `vox` via bash. Plus simple mais moins de fonctionnalites (pas de STT, stats, etc.). | +| `skill` | Cree `/speak` dans `~/.claude/commands/speak.md` | L'utilisateur invoque manuellement `/speak ` dans Claude Code. | +| `all` | Les trois modes combines | Maximum de compatibilite. | + ### Mode CLI Cree un `CLAUDE.md` dans le projet courant avec des instructions pour que l'assistant appelle `vox` apres les taches significatives. Ajoute un hook `Stop` dans `.claude/settings.json` qui dit "Termine" a la fin de chaque reponse. @@ -143,6 +181,37 @@ Cree un `CLAUDE.md` dans le projet courant avec des instructions pour que l'assi Cree une commande `/speak` dans `~/.claude/commands/speak.md` pour invoquer vox via slash command. +## Serveur MCP (`vox serve`) + +Lance le serveur MCP sur stdio (JSON-RPC 2.0, protocole `2024-11-05`). C'est cette commande que les outils IA appellent apres `vox init`. + +```bash +vox serve # Lance le serveur (bloque, lit stdin, ecrit stdout) +``` + +Le serveur est normalement lance automatiquement par l'outil IA. Il n'est pas necessaire de le lancer manuellement, sauf pour du debug. + +### Reference complete des 14 outils MCP + +| Outil | Description | Parametres | +|-------|-------------|------------| +| `vox_speak` | Synthetise et joue du texte | **`text`** (requis, string) : texte a prononcer. `voice` (string) : nom de voix ou clone. `lang` (string) : code langue. `backend` (string) : kokoro/say/qwen/qwen-native. `style` (string) : calm/energetic/warm/authoritative/cheerful/serious. `gender` (string) : feminine/masculine. `rate` (integer) : debit mots/min (say uniquement). | +| `vox_list_voices` | Liste les voix d'un backend | `backend` (string) : kokoro/say/qwen/qwen-native. Defaut : backend par defaut de la plateforme. | +| `vox_clone_list` | Liste les voice clones | Aucun parametre. | +| `vox_clone_add` | Ajoute un voice clone | **`name`** (requis, string) : nom du clone. **`audio`** (requis, string) : chemin du fichier audio de reference. `text` (string) : transcription de l'audio (ameliore la qualite). | +| `vox_clone_remove` | Supprime un voice clone | **`name`** (requis, string) : nom du clone a supprimer. | +| `vox_config_show` | Affiche les preferences | Aucun parametre. Retourne : backend, voice, lang, rate, gender, style, model, pack. | +| `vox_config_set` | Modifie une preference | **`key`** (requis, string) : cle (backend/voice/lang/rate/gender/style/model). **`value`** (requis, string) : valeur. | +| `vox_stats` | Statistiques d'utilisation | Aucun parametre. Retourne : total requests, total chars, 10 dernieres entrees. | +| `vox_pack_list` | Liste les sound packs | Aucun parametre. Retourne : packs installes (avec actif marque) + disponibles. | +| `vox_pack_install` | Installe un sound pack | **`name`** (requis, string) : nom du pack (peon, peon_fr, peon_pl, peasant, peasant_fr, sc_kerrigan, sc_battlecruiser, ra2_soviet_engineer). | +| `vox_pack_set` | Active un sound pack | **`name`** (requis, string) : nom du pack installe. | +| `vox_pack_play` | Joue un son d'un pack | `category` (string, defaut: "greeting") : greeting/acknowledge/complete/error/permission/resource_limit/annoyed. `pack` (string) : nom du pack (utilise le pack actif si omis). | +| `vox_pack_remove` | Supprime un sound pack | **`name`** (requis, string) : nom du pack. Si le pack supprime etait actif, le pack actif est remis a vide. | +| `vox_hear` | Enregistre et transcrit (STT) | `lang` (string, defaut: "fr") : code langue. `timeout` (integer, defaut: 30) : duree max en secondes. `silence` (number, defaut: 2.0) : secondes de silence avant arret. macOS uniquement. | + +Les parametres en **gras** sont requis. Le serveur renvoie `isError: true` si un parametre requis est manquant ou invalide. + ## Speech-to-Text (macOS) Transcription locale via mlx-whisper. diff --git a/docs/GUIDE.md b/docs/GUIDE.md index 1aa0474..788dab7 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -153,24 +153,89 @@ vox hear -l fr - Voix Apple integrees (Samantha, Thomas, etc.) - Support du debit (`-r 200`) - Pas de voice cloning +- Zero dependance, zero configuration ### kokoro (Rust pur) -- Cross-platform, zero dependance externe -- Bonne qualite vocale -- ~2-5s de latence +- Cross-platform, necessite `kokoro-onnx` et `soundfile` (Python) +- Bonne qualite vocale, voix pre-definies avec prefixe langue (`af_`, `ff_`, `jf_`, etc.) +- ~2-5s warm / ~5-8s cold start - Pas de voice cloning +- Modele ONNX ~80 MB dans `~/.config/vox/kokoro/` ### qwen (MLX Python, macOS) - Qualite neurale superieure -- Voice cloning supporte +- Voice cloning supporte (via `ref_audio` + `ref_text`) - Necessite `mlx-audio` + Apple Silicon -- ~1s warm / ~5-15s cold start +- ~1-2s warm / ~5-15s cold start +- Pipeline de chunking pour les textes longs (overlap generation/playback) ### qwen-native (Rust pur) - Meme modele Qwen3-TTS mais en Rust (candle) - Voice cloning supporte -- GPU via Metal (macOS) ou CUDA (Linux) -- Cross-platform +- GPU via Metal (macOS) ou CUDA (Linux), feature flags `metal`/`cuda` +- Cross-platform, zero dependance Python +- ~2-5s warm / ~10-30s cold start +- Le modele est garde en memoire via `Mutex` global — ideal pour le serveur MCP + +## Configuration avancee + +### Variables d'environnement + +| Variable | Description | Defaut | +|----------|-------------|--------| +| `VOX_CONFIG_DIR` | Repertoire de configuration alternatif | `~/.config/vox/` | +| `VOX_DB_PATH` | Chemin de base de donnees alternatif | `~/.config/vox/vox.db` | +| `ANTHROPIC_API_KEY` | Cle API Claude (requis pour `vox chat`) | Aucun | + +### Modeles personnalises + +Pour les backends `qwen` et `qwen-native`, il est possible d'utiliser un modele different : + +```bash +# Via CLI +vox -b qwen-native -m "mlx-community/Qwen3-TTS-12Hz-0.6B-Base-4bit" "Texte" + +# Via preference persistante +vox config set model "mlx-community/Qwen3-TTS-12Hz-0.6B-Base-4bit" +``` + +Les modeles sont telecharges automatiquement depuis HuggingFace Hub au premier appel. Le modele par defaut de `qwen-native` est `Qwen/Qwen3-TTS-12Hz-0.6B-Base`. Le modele par defaut de `qwen` (MLX) est `mlx-community/Qwen3-TTS-12Hz-0.6B-Base-bf16`. + +### Arborescence des donnees locales + +``` +~/.config/vox/ + vox.db # SQLite : preferences, clones, usage logs + clones/ # Fichiers audio des voice clones (.wav) + packs/ # Sound packs installes + peon/ + manifest.json + sounds/ + kokoro/ # Modele Kokoro (si utilise) + kokoro-v1.0.onnx + voices-v1.0.bin +``` + +## Verification de l'integration MCP + +Apres `vox init`, pour verifier que tout fonctionne : + +```bash +# 1. Verifier que le binaire est dans le PATH +which vox + +# 2. Tester le serveur MCP manuellement (Ctrl+C pour arreter) +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | vox serve + +# 3. Verifier la configuration de Claude Code +cat ~/.claude.json | grep vox + +# 4. Tester un appel complet +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}} +{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | vox serve +``` + +Le serveur doit repondre avec la liste des 14 outils. Si l'outil IA ne detecte pas vox apres l'init, redemarrez-le. ## Depannage @@ -180,11 +245,62 @@ Le binaire n'est pas dans votre PATH. Verifiez avec `which vox` ou reinstallez. ### "Backend 'say' is only available on macOS" Utilisez `kokoro` ou `qwen-native` sur Linux/Windows : `vox config set backend kokoro` -### Backend qwen lent au premier lancement -Normal : le modele (~1.2 GB) est telecharge automatiquement. Les lancements suivants sont plus rapides. +### Backend lent (cold start vs warm start) + +Tous les backends neuraux (kokoro, qwen, qwen-native) ont un temps de demarrage a froid (cold start) significant : + +| Backend | Cold start | Warm start | Comment accelerer | +|---------|-----------|------------|-------------------| +| `say` | ~100ms | ~100ms | Rien a faire, toujours rapide | +| `kokoro` | ~5-8s | ~2-5s | Pas de cache persistent — chaque appel CLI recharge le modele | +| `qwen` | ~5-15s | ~1-2s | Le premier appel telecharge le modele (~1.2 GB). Ensuite, seul le startup Python est lent | +| `qwen-native` | ~10-30s | ~2-5s | Le modele est garde en memoire dans le processus MCP. Utiliser via MCP (pas CLI) pour beneficier du warm start | + +**Conseil** : pour la latence la plus basse avec un backend neural, utilisez `vox serve` (via MCP) plutot que des appels CLI individuels. Le backend `qwen-native` garde son modele en memoire dans un `Mutex` global, donc les appels suivants au meme processus sont instantanes. + +### Qualite du voice cloning + +Pour de meilleurs resultats avec le voice cloning : + +- **Format** : WAV 16-bit, mono, 16-48 kHz (le WAV est recommande pour eviter les artefacts de compression) +- **Duree** : 5-15 secondes de parole continue +- **Contenu** : parler naturellement, pas trop vite, avec des phrases completes +- **Environnement** : calme, sans bruit de fond, sans echo +- **Transcription** : toujours fournir `--text` avec la transcription exacte — cela ameliore significativement la qualite +- Formats acceptes : wav, mp3, flac, ogg, m4a (mais WAV recommande) + +### Corruption de la base de donnees + +Si la base SQLite est corrompue (`database disk image is malformed`) : + +```bash +# Methode 1 : reinitialiser (perd les preferences et logs, garde les clones audio) +rm ~/.config/vox/vox.db +# La base sera recree automatiquement au prochain appel + +# Methode 2 : utiliser une base temporaire pour depannage +VOX_DB_PATH=/tmp/vox_test.db vox config show + +# Methode 3 : tenter une reparation SQLite +sqlite3 ~/.config/vox/vox.db ".recover" | sqlite3 ~/.config/vox/vox_recovered.db +mv ~/.config/vox/vox_recovered.db ~/.config/vox/vox.db +``` ### Enregistrement micro ne fonctionne pas Installez sox : `brew install sox` (macOS) ou `apt install sox` (Linux). ### "ANTHROPIC_API_KEY is required" (chat mode) Exportez votre cle API : `export ANTHROPIC_API_KEY=sk-ant-...` + +## Choix du backend : guide de performance + +| Usage | Backend recommande | Pourquoi | +|-------|-------------------|----------| +| Feedback rapide (1-2 phrases) | `say` (macOS) | Latence ~100ms, zero configuration | +| Cross-platform, zero config | `kokoro` | Marche partout, bonne qualite, pas de GPU | +| Qualite maximale | `qwen` ou `qwen-native` | Voix neurale, prosodie naturelle | +| Voice cloning | `qwen` (macOS) ou `qwen-native` | Seuls backends supportant le cloning | +| Serveur MCP longue duree | `qwen-native` | Modele reste en memoire, warm start ~2-5s | +| Texte long (>500 chars) | `qwen` | Pipeline de chunking avec overlap generation/playback | +| Machine sans GPU | `kokoro` ou `say` | Pas de GPU requis, latence acceptable | +| Machine avec GPU (Metal/CUDA) | `qwen-native` | Acceleration materielle via feature flags `metal`/`cuda` |