Kazeia-engine/dist/cosyvoice/COSYVOICE_INTEGRATION.md

95 lines
7.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.