7.3 KiB
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 (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'anciencv3_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) viacv_make_prompt.pyà partir de la phrase de consentement (~15 s) + sa transcription. Exemple fourni :damien.cvps.example. Une voix = un.cvpsshippé/téléchargé. Enrôlement cross-lingual : une voix sert pour les 9 langues.
# 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)
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 :
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
- Pousser les 4 libs dans
jniLibs/arm64-v8a/(libkazeia_cosyvoice.so,libcosyvoice.so,libomp.so,libc++_shared.so). Pas delibggml*.so(embarqué statiquement → pas de collision avec le stack LLM/TTS existant). - Copier
CosyVoiceTtsEngine.kt. - Déployer
cv3_distilled_30k_hiftf16.gguf+ les.cvpsenrôlés dans le storage. System.loadLibrary("kazeia_cosyvoice")+ 1 synthèse de test (vérifier PCM non vide).- Câbler le flag
ttsEngine(dispatch par langue cosyvoice/chatterbox). - 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.socharge + ses 5 symboles JNI résolvent contre le nouveaulibcosyvoice.sosur V79 (« CHAINE DE LIENS COMPLETE OK »), 0 exportggml_. - ✅ 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é enlibc++_sharedpartagé ; (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.