Kazeia-engine/dist/TTS_INTEGRATION.md

12 KiB
Raw Blame History

Intégration TTS Kazeia-Engine — Guide développeur

Date : 01/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

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) :

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) :

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

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

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 :

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 :

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.