291 lines
12 KiB
Markdown
291 lines
12 KiB
Markdown
# 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`) :
|
||
|
||
```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`.
|