Kazeia-engine/dist/cosyvoice/COSYVOICE_INTEGRATION.md

7.3 KiB
Raw Permalink Blame History

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

  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.