# Intégration TTS Kazeia-Engine — Guide développeur > ⚠️ **BLOQUEUR SELinux découvert 02/06 — lire la section 0 avant toute intégration in-app.** > La lib `libkazeia_tts.so` actuelle (linke `libllama.so` + `libggml-hexagon.so`) **NE FONCTIONNE PAS** > dans un APK `untrusted_app` à cause d'une policy SELinux Android/Qualcomm qui interdit > l'accès à `/dev/fastrpc-cdsp` requis par FastRPC. **Rebuild CPU-only requis** pour usage in-app. > Le speaker encoder (clonage vocal) est lui CPU-only et n'est pas affecté. **Date** : 02/06/2026 **Cible** : remplacer le pipeline TTS legacy par `libkazeia_tts.so` + façade Kotlin `TtsEngine.kt`. Inclut le **clonage vocal embarqué** via speaker encoder ECAPA-TDNN. **Runtime** : libllama + libggml (fork ql Qualcomm) avec backend Hexagon HTP V79 — décode ggml in-process. Pas d'ONNX, pas de Python, pas de root. **Composants in-process** : - **Talker** Qwen3-TTS via `libllama.so` (texte → codes audio 16 codebooks) - **CP** (Code Predictor) en ggml C++ avec KV-cache (codes → embeddings audio) - **Decoder** ggml chraac intégré (embeddings → WAV PCM 24 kHz) - **Speaker encoder** ECAPA-TDNN optionnel (WAV ref → x_vector[1024] pour clonage vocal) **Perf prouvée** (Snapdragon 8 Elite SM8750, audio FR 2-5 s) : - RTF total : ~2.5-3.0 selon longueur (talker 32 ms/frame, CP 70 ms/frame, decoder RTF ~2) - Clonage vocal : encoder per-call **190-530 ms** selon ref WAV (5-15 s) - RAM peak : ~3.5 GB (talker f32 1.6 GB + CP 250 MB + decoder 325 MB + vocab 50 MB + fixtures 1.2 GB) - Clonage ajoute ~34 MB RAM permanent au load --- ## 0. Bloqueur SELinux découvert 02/06 — à lire en premier **Symptôme** : `tts_engine_load` crashe in-app (SIGABRT dans le backend hexagon), pas en standalone `adb shell`. **Cause identique au LLM** : `libkazeia_tts.so` linke `libllama.so` qui dépend de `libggml-hexagon.so`, qui dlopen `libcdsprpc.so` (FastRPC). La policy SELinux interdit `untrusted_app` d'accéder à `/dev/fastrpc-cdsp` : ``` avc: denied { open } for path="/dev/fastrpc-cdsp" scontext=u:r:untrusted_app tcontext=u:object_r:vendor_qdsp_device ``` **Spécificité TTS** : dans le pipeline TTS Qwen3, le talker fait son **prefill** sur HTP (option C). Mais en pratique, sur des prompts TTS courts (~20-30 tokens), le prefill HTP n'apporte que **~50 ms** vs CPU pur. Donc le rebuild CPU-only impacte **0 perceptiblement** le TTS : | | Pipeline actuel | TTS CPU-only | |---|---:|---:| | Talker prefill | ~50 ms (HTP) | ~150 ms (CPU) | | Talker loop (decode) | déjà CPU | déjà CPU | | CP loop | déjà CPU | déjà CPU | | Decoder chraac | déjà CPU | déjà CPU | | **Total tour TTS** | **~8 s** | **~8.1 s (+0.1 s)** | → Coût négligeable pour TTS. Le rebuild CPU-only **préserve toutes les fonctionnalités** y compris le **clonage vocal embarqué** (le speaker encoder ECAPA-TDNN est CPU-only de toute façon, jamais touché par cette histoire HTP/SELinux). **Solution** : même rebuild CPU-only (`-DGGML_HEXAGON=OFF`) que pour LLM. Une seule chaîne de libs partagée. Voir `project_engine_selinux_fastrpc_blocker` en mémoire. **État jusqu'à rebuild** : flag `tts_engine=lib` arme la bascule mais la lib ne fonctionnera pas in-app. Garder `tts_engine=prod` jusqu'à rebuild CPU-only des libs. ## 1. Ce que la lib fait ``` libkazeia_tts.so (929 KB, SHARED, dépend de libllama + libggml*) ├── TtsEngine load(talker_gguf, vocab_gguf, dump_dir, useHtp, nThreads, cpUseCache) ├── TtsEngine synthesize(text, outWavPath, maxSteps, seed, sampling, xvectorOverride?) ├── TtsEngine encode_speaker_wav(refWavPath) -> FloatArray[1024] // clonage ├── TtsEngine encode_speaker_waveform(pcm, sampleRate) // clonage in-memory └── TtsEngine free(handle) ``` API Kotlin (`TtsEngine.kt`) : ```kotlin val tts = TtsEngine( talkerGguf = "/data/.../talker_f32.gguf", vocabGguf = "/data/.../Qwen3-4B-Q4_0.gguf", dumpDir = "/data/.../tts_dump", useHtp = false, // true -> HTP0 prefill option C ; false -> CPU pur nThreads = 6, cpUseCache = true, // KV cache CP (gain RTF -39%) ) // Synthèse classique (voix par défaut = damien_xvector.bin fixture) val r = tts.synthesize( text = "Bonjour, comment allez-vous ?", outWavPath = "/sdcard/out.wav", maxSteps = 256, sampling = TtsSampling() // top_p=0.95, rep_penalty=1.10 validés ) println("WAV ${r.frames} frames, ${r.audioMs} ms audio en ${r.totalMs} ms, RTF=${r.totalMs.toFloat()/r.audioMs}") // Synthèse avec voix clonée val xvec = tts.encodeSpeakerWav("/sdcard/voix_pierre.wav") // 1024 floats val r2 = tts.synthesize(text = "...", outWavPath = "...", xvectorOverride = xvec) tts.release() ``` --- ## 2. Build (côté app Android) ### a. Récupérer les artifacts Sur le host (machine de build) : ```bash cd /opt/Kazeia-engine/dist bash build_kazeia_tts.sh # produit b-jni/libkazeia_tts.so ``` ### b. Libs à copier dans `app/src/main/jniLibs/arm64-v8a/` **8 fichiers obligatoires** (vérifiés via `readelf -d` sur la lib produite) : | Fichier | Taille | Origine | Rôle | |---|---:|---|---| | `libkazeia_tts.so` | 929 KB | `dist/b-jni/` | la lib elle-même | | `libllama.so` | 35 MB | `dist/lib/` | fork ggml-org/llama.cpp + Hexagon | | `libggml.so` | 627 KB | `dist/lib/` | ggml core | | `libggml-base.so` | 6.6 MB | `dist/lib/` | ggml base ops | | `libggml-cpu.so` | 4.7 MB | `dist/lib/` | ggml CPU backend (NEON ARM v8.6) | | `libggml-hexagon.so` | 3.3 MB | `dist/lib/` | ggml Hexagon HTP backend (host wrapper) | | `libggml-htp-v79.so` | 354 KB | `dist/lib/` | DSP skel V79 (chargé via dlopen runtime par libggml-hexagon) | | `libc++_shared.so` | 1.8 MB | `dist/lib/` | NDK r27d STL (peut être déjà fournie par votre projet) | **Total** : ~52 MB. Important côté APK mais indispensable. Pas de Maven AAR alternatif — il faut pousser ces .so. **Crash typique** si une lib manque (vu en bench) : ``` dlopen failed: library "libllama.so" not found: needed by /data/app/.../libkazeia_tts.so ``` → Vérifier que les 8 libs sont bien dans `jniLibs/arm64-v8a/`. ### c. Copier la façade Kotlin Copie `dist/jni/TtsEngine.kt` dans `app/src/main/java/com/kazeia/tts/TtsEngine.kt`. Package = `com.kazeia.tts` (cohérent avec l'existant). ### d. Modèles sur la tablette Inchangé par rapport au pipeline TTS actuel. Push dans le dir habituel : ``` /data/local/tmp/kazeia/tts/ ├── talker_f32.gguf # Qwen3-TTS talker 1.6 GB ├── tts_dump/ │ ├── cp_f16.gguf # CP weights │ ├── cp_heads.bin │ ├── cp_codec_embs.bin │ ├── qwen3tts_decoder.gguf # decoder ggml chraac │ ├── text_embed.bin # ~1.2 GB │ ├── tp_fc1_w.bin / tp_fc1_b.bin │ ├── tp_fc2_w.bin / tp_fc2_b.bin │ ├── talker_tok_embd.bin # tied embed reference │ ├── damien_xvector.bin # voix par défaut (1024 floats) │ ├── manifest_text.txt # constantes BOS/EOS/PAD │ ├── speaker_encoder.gguf # OPTIONNEL : 17 MB clonage vocal │ └── mel_basis_qwen3tts.bin # OPTIONNEL : 256 KB librosa mel └── (vocab_gguf = un Qwen3-4B-Q4_0.gguf pour le tokenizer texte) ``` --- ## 3. Migration depuis le pipeline TTS legacy Si tu avais `tts_talker_cpu.cpp` / `tts_cp_cpu.cpp` compilés en source dans le projet app contre un ancien fork ggml : - **Tu peux les supprimer**. Tout ce code est embarqué dans `libkazeia_tts.so` (tts_engine.cpp + cp_inference.cpp + speaker_encoder.cpp + sampler.cpp + kazeia_text_tokenizer.cpp + chraac decoder statique). - Tu n'as plus besoin de recompiler quoi que ce soit contre les headers du fork ql — la lib est self-contained. - Tu n'as plus besoin du tokenizer texte custom — il est dans la lib via `kazeia_text_tokenizer.cpp` qui charge le vocab d'un GGUF Qwen. --- ## 4. Exemples d'usage ### Synthèse simple ```kotlin val tts = TtsEngine( talkerGguf = "$kazeiaDir/talker_f32.gguf", vocabGguf = "$kazeiaDir/Qwen3-4B-Q4_0.gguf", dumpDir = "$kazeiaDir/tts_dump" ) val r = tts.synthesize("Bonjour Pierre, comment vous sentez-vous aujourd'hui ?", outWavPath = "$cacheDir/reply.wav") playWav(r.outWavPath) tts.release() ``` ### Clonage vocal — encode 1 fois, synth N fois ```kotlin val tts = TtsEngine(/* ... */, speakerEncoderGguf = "$kazeiaDir/tts_dump/speaker_encoder.gguf", mel_basis_path = "$kazeiaDir/tts_dump/mel_basis_qwen3tts.bin") // Encode une fois (cache l'utiliser sur les N synthèses qui suivent) val pierreVoice = tts.encodeSpeakerWav("$voicesDir/pierre_5s.wav") // ~190 ms for (text in messages) { val r = tts.synthesize(text, outWavPath = nextWav(), xvectorOverride = pierreVoice) playWav(r.outWavPath) } tts.release() ``` ### Sampling tuning Les défauts (`top_p=0.95`, `rep_penalty=1.10`) sont **validés à l'écoute** sur panel FR (cf mémoire `project_tts_session_28may`). Override possible : ```kotlin val r = tts.synthesize(text, outWavPath, sampling = TtsSampling( cpTemp = 0.9f, cpTopK = 50, cpTopP = 0.95f, cpRepPenalty = 1.10f, talkerTemp = 0.9f, talkerTopK = 50, talkerTopP = 0.95f, talkerRepPenalty = 1.10f )) ``` --- ## 5. Codes d'erreur (`TtsResult.err`) | Code | Signification | |---|---| | 0 | OK | | -1 | engine nul ou texte/outWavPath nul | | -2 | pas de tokenizer chargé (vocabGguf manquant au load) | | -3 | tokens trop courts (texte < 8 tokens BPE) | | -4 | prefill talker FAIL (modèle, vocab, manifest) | | -6 | WAV write FAIL | Au `tts_engine_load` (renvoie `null` → exception Kotlin `IllegalStateException`) : - talker_gguf ou dump_dir nul - fichiers fixtures manquants (`text_embed.bin`, `tp_*`, `tok_embd.bin`, `damien_xvector.bin`) - vocab GGUF invalide - modèle talker invalide --- ## 6. Bench rapide standalone (avant d'intégrer) Pour vérifier que la lib + libs sont OK avant câblage app : ```bash adb push dist/b-jni/tts_pipeline /data/local/tmp/kz-engine/bin/ adb push dist/lib/lib*.so /data/local/tmp/kz-engine/bin/ adb shell ' cd /data/local/tmp/kz-engine export LD_LIBRARY_PATH=./bin export ADSP_LIBRARY_PATH=./bin export KZTTS_TEXT="Bonjour, comment allez-vous ?" export KZTTS_VOCAB_GGUF=Qwen3-4B-Q4_0.gguf ./bin/tts_pipeline talker_f32.gguf tts_dump /tmp/out.wav cpu 64 ' ``` Sortie typique : ``` tts_engine_load: 1.0s (talker_n_embd=1024, npe=4, kz_tok=1, cp_cache=1, spk_enc=0) === TTS : N=42 frames (audio 3.50s) en 8.5s (RTF 2.43) === WAV -> /tmp/out.wav ``` Si la lib charge mais le synth échoue, lire la sortie stderr (codes d'erreur ci-dessus). --- ## 7. Checklist d'intégration (à valider en premier in-app) 1. **`System.loadLibrary("kazeia_tts")` réussit** au démarrage de l'app. Si erreur `library "libllama.so" not found`, vérifier qu'il y a bien les 8 libs dans `jniLibs/arm64-v8a/`. 2. **Charge un engine + 1 synthèse** sur un texte fixture FR (« Bonjour. ») : doit produire un WAV audible. Si silence ou bruit, vérifier que `tts_dump/` est complet (notamment `text_embed.bin` 1.2 GB et `tp_fc1/2_*` qui sont obligatoires). 3. **Sanity null/edge** : - text vide → err -1 - text 3 caractères → err -3 (trop court) - outWavPath dans un dir inexistant → err -6 4. **Threading** : `TtsEngine.synthesize()` est synchrone, ~3-10 s selon longueur. Appeler depuis `Dispatchers.IO`. Le n_threads est paramétrable au load (défaut 6). 5. **Cycle de vie** : `release()` à appeler quand l'app sort du foreground (3.5 GB de RAM libérée). 6. **Clonage vocal (optionnel)** : si tu fournis `speakerEncoderGguf` + `melBasisPath` au load, tu peux appeler `encodeSpeakerWav()` ensuite. Sinon, l'engine utilise le `damien_xvector.bin` fixture (voix damien). --- ## 8. Pièges connus - **Stack TTS legacy** : si tu avais des sources `tts_talker_cpu.cpp` / `tts_cp_cpu.cpp` compilées contre un fork ggml différent, **tu peux et dois les supprimer**. Tout est dans la lib. - **chraac decoder** : le decoder ggml chraac est intégré statiquement dans `libkazeia_tts.so`. Pas besoin de pousser `libqwen3tts-decoder.a` séparément, ni le binaire `decoder_subprocess` (l'ancien pattern subprocess est obsolète). - **HTP TTS** : `useHtp = true` n'apporte rien sur le talker actuel (decode CPU 16.5 tok/s reste meilleur que HTP 3.6 tok/s pour les modèles dense). Garder `useHtp = false` sauf si tu testes le prefill HTP gain. - **Sample rate** : le clonage vocal `encodeSpeakerWav()` attend du **mono 16-bit 24 kHz**. Si tu lis un WAV à un autre SR, resample externe (libAndroid AudioRecord + resampler ou ffmpeg). --- ## 9. Perf détaillée Pour 3 s d'audio synthétisé, 36 frames (12 frames/s = 80 ms/frame) : ``` prefill ~150 ms (talker prompt -> KV initial) talker_loop ~1150 ms (36 frames × 32 ms) cp_loop ~2520 ms (36 frames × 70 ms) decoder ~4400 ms (RTF ~1.3 sur 36 frames) ───────────────────────── total ~8.2 s pour 3 s audio = RTF 2.7 ``` Le decoder (chraac) est le pic. Optimisations futures : décoder par chunks de 8 frames pour overlap, ou portage HTP (R&D ouverte). **Clonage** : +190-530 ms one-time par changement de voix (encode_speaker_wav), 0 ms si on garde la même voix sur plusieurs synthèses. --- ## 10. Contacts & support Code source : `/opt/Kazeia-engine/dist/jni/tts_engine.{h,cpp}` + `cp_inference.cpp` + `speaker_encoder.cpp` + `kazeia_mel.{h,cpp}` + `sampler.{h,cpp}` + `kazeia_tts_jni.cpp`. Test CLI : `/opt/Kazeia-engine/dist/jni/tts_pipeline.cpp` (binaire `tts_pipeline`). Bench artifacts : `/opt/Kazeia-engine/dist/b-jni/libkazeia_tts.so` + `tts_pipeline`. Pour reproduire un bug : capture logcat avec tag `tts_engine`.