# Intégration CosyVoice3 distillé — Guide développeur Kazeia **Date** : 14/06/2026 **Cible** : moteur TTS clonage vocal zero-shot, **9 langues** (fr, en, de, es, it, ru, zh, ja, ko), tournant **100 % CPU sur la tablette** (SM8750 / V79). **Statut** : pipeline E2E validé sur tablette, JNI livrée. **Pas de bloqueur SELinux** (CPU-only, aucune dépendance FastRPC/hexagon). > Pour le **portugais, l'arabe** et 14 autres langues hors CosyVoice3 → moteur **Chatterbox** (à intégrer, dispatch par langue côté app). --- ## 1. Architecture CosyVoice3-0.5B = LLM (Qwen2.5-0.5B + tête tokens audio) → flow-matching (DiT) → vocoder HiFT. **Le flow a été distillé sur A40 (Reflow 2-NFE)** : 10 étapes+CFG → **2 étapes sans CFG**, ÷10 de calcul, qualité teacher préservée (ECAPA 0,878 vs voix de référence). Base = fork de [Lourdle/cosyvoice.cpp](https://github.com/Lourdle/cosyvoice.cpp) (ggml C++, MIT) avec patch reflow (schedule linéaire + CFG off + N pas, pilotés par env `CV_REFLOW`/`CV_NFE`, lus par le loader). ## 2. Libs à pousser dans `jniLibs/arm64-v8a/` — **4 fichiers seulement** | Fichier | Rôle | |---|---| | `libkazeia_cosyvoice.so` | bridge JNI (5 symboles `com.kazeia.tts.CosyVoiceJni`) | | `libcosyvoice.so` | moteur **auto-contenu** (LLM+flow+HiFT, **ggml statique embarqué**) | | `libomp.so` | OpenMP (NDK r27d) | | `libc++_shared.so` | STL (NDK r27d) | **⚠ PAS de `libggml*.so`** — c'est volontaire et critique. ggml est compilé **en statique dans `libcosyvoice.so` avec visibilité masquée** (`--exclude-libs,ALL`) : `libcosyvoice.so` exporte **0 symbole `ggml_*`** (100 symboles `cosyvoice_*` uniquement). Cela **évite la collision de SONAME** `libggml.so/-base/-cpu` avec le stack LLM/TTS gelé de l'app (fork ql) — les deux moteurs ont des builds ggml différents, et les charger ensemble en `RTLD_GLOBAL` provoquerait une interposition ELF (crash/corruption). Ici, le ggml de CosyVoice est invisible au namespace global → zéro conflit. Vérifié : `readelf -d libkazeia_cosyvoice.so` → NEEDED = **libcosyvoice + libc++_shared + système** ; `nm -D libcosyvoice.so | grep ggml_` → vide. **Aucune dépendance `libggml-hexagon` / FastRPC** non plus → pas de SIGABRT `untrusted_app`. ## 3. Modèle + voix (assets) - **Modèle** : `cv3_distilled_30k_hiftf16.gguf` (~953 Mo, flow q8_0 distillé 2-NFE + **HiFT en F16** = vocodeur −28 % sur ARM) — livré séparément. À placer dans le storage app. (Remplace l'ancien `cv3_distilled_30k.gguf` ; même API, juste plus rapide.) - **Voix** : artefacts `.cvps` (GGUF 4 tenseurs : mel prompt + x-vector + tokens + transcription). **Générés OFFLINE** (enrôlement) via `cv_make_prompt.py` à partir de la phrase de consentement (~15 s) + sa transcription. Exemple fourni : `damien.cvps.example`. Une voix = un `.cvps` shippé/téléchargé. Enrôlement cross-lingual : une voix sert pour les 9 langues. ```bash # enrôlement offline (poste de prépa, pas la tablette) python cv_make_prompt.py voix_consentement.wav transcription.txt sortie.cvps ``` ## 4. Façade Kotlin (`CosyVoiceTtsEngine.kt`) ```kotlin val tts = CosyVoiceTtsEngine("$dir/cv3_distilled_30k_hiftf16.gguf", nfe = 2, nThreads = 8) val damien = tts.loadVoice("$dir/damien.cvps") val pcm: FloatArray = tts.synthesize(damien, "Bonjour, comment allez-vous ?") // 24 kHz mono [-1,1] // ... jouer pcm via AudioTrack (ENCODING_PCM_FLOAT, 24000) ... damien.release(); tts.release() ``` Interface : `nativeLoad(gguf, nfe, nThreads)` · `nativeLoadVoice(handle, cvps)` · `nativeSynthesize(voice, text, speed)→FloatArray` · `nativeFreeVoice` · `nativeFree`. ## 5. Flag de dispatch (menu Kazeia) Même pattern que `sttEngine`. Aiguillage par **langue** : ```kotlin val ttsEngine = when { lang in COSYVOICE_LANGS /* fr,en,de,es,it,ru,zh,ja,ko */ -> cosyVoiceEngine else -> chatterboxEngine // pt,ar,he,hi,tr,pl,nl,... } ``` ## 6. Performance (SM8750 / V79, mesuré) — **streaming chunk-causal + cache K/V prompt** Le moteur tourne en mode **streaming chunk-causal** avec un **cache K/V de prompt persistant** (activé automatiquement par le JNI). Le prompt (la voix, ~70 % du calcul du flow) n'est calculé **qu'une fois par voix**, puis réutilisé sur tous les énoncés suivants. | Tour | LLM | flow | HiFT | tour complet | RTF (énoncé ~3,8 s) | |---|---:|---:|---:|---:|---:| | **1er** (voix non encore vue) | 1,0 s | 5,1 s | 1,2 s | 7,3 s | **~1,9** | | **2e+** (même voix, prompt caché) | 0,9 s | **1,2 s** | 1,2 s | 3,35 s | **0,88** | - **÷4,1 sur le flow** (cache K/V prompt) + **HiFT F16 (−28 %)** → **RTF 0,88 soutenu** sur V79, sous le temps réel (validé on-device 15/06). Étages équilibrés (~0,25-0,32 chacun), plus de goulot dominant. Réponses courtes (cas thérapeutique) : encore mieux. - **Transparent pour l'app** : il suffit de **réutiliser le même moteur + la même voix** entre les tours (ce que fait déjà le pipeline). 1ère synthèse d'une voix = lente (remplit le cache), les suivantes = rapides. Le cache est par-voix (clé = identité du `.cvps`) ; changer de voix re-remplit. - Qualité préservée : sortie cache ≈ sortie non-cachée (mel corr 0,9997 ; ECAPA/campplus ~0,80-0,88, monte avec la longueur du prompt). - RAM ~1 Go (modèle) + ~30 Mo/voix + **~270 Mo de cache K/V** (la 1ère voix). - **1er énoncé reste à RTF ~2** (le prompt s'y calcule) : si la latence du tout premier tour gêne, un chantier *first-audio* (chunks + HiFT streaming) est possible — pas nécessaire pour le temps-réel soutenu. - Génère phrase par phrase (split activé) ; lire en streaming via AudioTrack. ## 7. Checklist intégration 1. Pousser les **4 libs** dans `jniLibs/arm64-v8a/` (`libkazeia_cosyvoice.so`, `libcosyvoice.so`, `libomp.so`, `libc++_shared.so`). **Pas de `libggml*.so`** (embarqué statiquement → pas de collision avec le stack LLM/TTS existant). 2. Copier `CosyVoiceTtsEngine.kt`. 3. Déployer `cv3_distilled_30k_hiftf16.gguf` + les `.cvps` enrôlés dans le storage. 4. `System.loadLibrary("kazeia_cosyvoice")` + 1 synthèse de test (vérifier PCM non vide). 5. Câbler le flag `ttsEngine` (dispatch par langue cosyvoice/chatterbox). 6. AudioTrack en `ENCODING_PCM_FLOAT` @ 24000, lecture par phrase (streaming). ## 8. Validation effectuée (on-device, SM8750) - ✅ Chaîne de liens (libs **streaming-cache** 15/06) : `libkazeia_cosyvoice.so` charge + ses 5 symboles JNI résolvent contre le nouveau `libcosyvoice.so` sur V79 (« CHAINE DE LIENS COMPLETE OK »), 0 export `ggml_`. - ✅ **Streaming-cache validé on-device (V79, cli E2E)** : flow tour 1 (MISS) 5,1 s → tour 2+ (HIT) 1,2 s = ÷4,1, RTF tour complet 2,0 → 1,0. Qualité campplus HIT 0,80 ≈ MISS 0,82. Mel cache vs non-cache corr 0,9997. - ✅ Chemin d'intégration E2E (harness natif répliquant la séquence JNI exacte) : load modèle → load voix → `tts_zero_shot` → PCM produit. Validé. - ⚠ Le marshalling JNI lui-même (JNIEnv string/array) suit le pattern standard mais sera testé pour la 1ʳᵉ fois sous vraie JVM à l'intégration APK. - **3 bugs JNI corrigés lors de cette validation** : (a) `-static-libstdc++` → `bad_cast` (deux runtimes C++) → lié en `libc++_shared` partagé ; (b) `cosyvoice_init_backend_from_path()` manquant avant load → ajouté ; (c) normalisation texte ICU absente en build NO_ICU → désactivée + split activé dans la façade.