chantier B unification #1 : engine 3/3 prêt pour intégration (STT + TTS + LLM)
Reproche du user juste : j'ai livré STT complet mais pas TTS ni LLM au même niveau. Cette session corrige : 3 docs d'intégration équivalentes en qualité, 2 bugs in-app investigués et l'un fixé. Investigation TTS "crashe au load in-app" : - DT_NEEDED de libkazeia_tts.so = libllama.so + libggml.so + libggml-base.so + libggml-cpu.so + libc++_shared.so + libdl/libm/libc - Test reproduction sur tablette : lib charge OK avec les 7 deps présentes, crash avec "library libllama.so not found" si une dep manque - Conclusion : c'est juste 7 libs à pousser dans jniLibs/arm64-v8a/, pas un bug code. Liste exhaustive donnée dans TTS_INTEGRATION.md §2.b Investigation LLM "ne tient pas 8 threads in-app" : - kazeia_engine_jni.cpp ligne 117/120/124/131 : n_threads HARDCODE à 4 pour le decode CPU. Pas exposé côté Kotlin. - Fix : ajouter param nThreads à load(), N_DECODE défaut 6 (sweet spot r3 mesuré : Qwen3.5-4B 9.8 tok/s à t=6 vs 7.4 à t=8 contention) - Update EngineJni.load(model, ctx, nThreads) + EngineLlmEngine(ctx, nThreads=6) - Rebuild libkazeia_engine.so 60 KB Docs livrées (format identique STT_INTEGRATION.md) : - TTS_INTEGRATION.md (10 KB) : libs à pousser, façade, clonage vocal, sampling, codes erreur, bench, checklist, pièges - LLM_INTEGRATION.md (10 KB) : option C HTP/CPU, n_threads sweet spot, modèles supportés (Qwen3 dense / Qwen3.5 hybride GDN), cascade Speaker+Thinker, piège affinity Android (decode 9.8 -> 0.32 tok/s in-app si scheduler pin sur 1 cœur) - KAZEIA_ENGINE_OVERVIEW.md actualisé : entrée unique pointant les 3 docs, pattern dispatch flag (mirror STT pour LLM+TTS), état réel 3/3 prêt Reste côté dev (cf KAZEIA_ENGINE_OVERVIEW.md): 1. Pousser les libs jniLibs (8 fichiers pour TTS/LLM, partagées entre eux) 2. Copier les 3 façades Kotlin 3. Câbler 2 flags dispatch supplémentaires (llmEngine + ttsEngine, mirror STT) 4. Valider checklist §7 de chaque doc (System.loadLibrary + 1 call) 5. Bench A/B prod vs lib sur audios/prompts fixtures Le pattern STT a marché parce qu'il avait tout (lib + façade + doc + checklist). Maintenant TTS et LLM aussi.
This commit is contained in:
parent
4ad3b3716d
commit
8c1e83be18
|
|
@ -1,106 +1,161 @@
|
|||
# Kazeia-Engine — vue d'ensemble du moteur unifié
|
||||
# Kazeia-Engine — vue d'ensemble et entrée d'intégration
|
||||
|
||||
**Mai 2026**. Moteur d'inférence local-first pour la tablette Snapdragon 8 Elite (Pad3). Couvre LLM, TTS (avec clonage vocal embarqué) et STT dans une stack cohérente.
|
||||
**Date** : 01/06/2026. Moteur d'inférence local-first pour Snapdragon 8 Elite (Pad3). Unifie **LLM + TTS + STT** dans une stack cohérente. Pas de cloud, pas de root, pas de Python runtime.
|
||||
|
||||
## Documents d'intégration (3 indépendants)
|
||||
|
||||
Pour intégrer dans l'app Kazeia, lis ces 3 docs (un par sous-système) :
|
||||
|
||||
| Sous-système | Doc d'intégration | Lib `.so` | Façade Kotlin |
|
||||
|---|---|---|---|
|
||||
| **STT** (Whisper-Small NPU) | [`STT_INTEGRATION.md`](STT_INTEGRATION.md) | `libkazeia_stt.so` 183 KB | `SttEngine.kt` |
|
||||
| **TTS** (Qwen3-TTS + clonage vocal) | [`TTS_INTEGRATION.md`](TTS_INTEGRATION.md) | `libkazeia_tts.so` 929 KB | `TtsEngine.kt` |
|
||||
| **LLM** (Qwen3.5 hybride / Qwen3 dense) | [`LLM_INTEGRATION.md`](LLM_INTEGRATION.md) | `libkazeia_engine.so` 60 KB | `EngineLlmEngine.kt` |
|
||||
|
||||
Chaque doc a la même structure : libs à pousser, façade Kotlin, exemples, codes d'erreur, bench standalone, checklist d'intégration, pièges connus, perf détaillée.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────── Kazeia-Engine ───────────────────────┐
|
||||
│ │
|
||||
│ STT (Whisper-Small NPU) TTS (Qwen3-TTS in-process) │
|
||||
│ ┌──────────────────────┐ ┌────────────────────────┐ │
|
||||
│ │ libkazeia_stt.so │ │ libkazeia_tts.so │ │
|
||||
│ │ 168 KB │ │ ~5 MB │ │
|
||||
│ │ ↳ stt_engine │ │ ↳ tts_engine │ │
|
||||
│ │ ↳ kazeia_mel │ ────► │ ↳ kazeia_mel │ │ partage
|
||||
│ │ ↳ VAD RMS C++ │ │ ↳ speaker_encoder │ │ mel + VAD
|
||||
│ │ ↳ BPE byte-level │ │ ↳ cp_inference │ │
|
||||
│ │ ↳ JSON parser │ │ ↳ decoder ggml chraac │ │
|
||||
│ │ │ │ ↳ sampler │ │
|
||||
│ │ runtime : │ │ │ │
|
||||
│ │ libonnxruntime.so + │ │ runtime : │ │
|
||||
│ │ libQnnHtp*.so │ │ libllama + libggml* │ │
|
||||
│ └──────────────────────┘ └────────────────────────┘ │
|
||||
│ │
|
||||
│ LLM (Qwen3.5-4B GGUF) │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ libllama.so + libggml-htp-v79.so + chraac codec │ │
|
||||
│ │ ↳ prefill HTP / decode CPU NEON │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
┌────────────────────── Kazeia-Engine ──────────────────────┐
|
||||
│ │
|
||||
│ STT TTS LLM │
|
||||
│ libkazeia_stt libkazeia_tts libkazeia_eng │
|
||||
│ 183 KB 929 KB 60 KB │
|
||||
│ │ │ │ │
|
||||
│ └─ libonnxruntime └────────┬───────────┘ │
|
||||
│ libQnnHtp* libllama 35 MB │
|
||||
│ libcdsprpc libggml + ggml-cpu │
|
||||
│ libggml-hexagon │
|
||||
│ libggml-htp-v79 │
|
||||
│ (chaîne mutualisée TTS+LLM) │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
|
||||
Côté APK :
|
||||
jniLibs/arm64-v8a/
|
||||
libkazeia_stt.so + libonnxruntime.so (2 fichiers, ~20 MB)
|
||||
libkazeia_tts.so + libkazeia_engine.so + libllama.so + ... (8 fichiers, ~53 MB partagés)
|
||||
libc++_shared.so (commune)
|
||||
|
||||
TOTAL ~75 MB pour les 3 sous-systèmes (vs ~50 MB ExecuTorch seul).
|
||||
```
|
||||
|
||||
## API Kotlin unifiée (côté app)
|
||||
|
||||
```kotlin
|
||||
// 1) STT in-process (Whisper-Small NPU)
|
||||
val stt = SttEngine("/data/.../whisper-small-sm8750", useHtp = true)
|
||||
// STT
|
||||
val stt = SttEngine(modelDir = "/data/.../whisper-small-sm8750", useHtp = true)
|
||||
val transcript = stt.transcribe(pcm16, language = "fr")
|
||||
|
||||
// 2) VAD (drop-in replacement de VadStage.kt)
|
||||
// VAD (drop-in replacement de l'ancienne VadStage.kt)
|
||||
val vad = SttVad()
|
||||
when (vad.push(chunk)) { SttVad.SPEECH -> ... ; SttVad.END_OF_SPEECH -> ... }
|
||||
when (vad.push(chunk)) {
|
||||
SttVad.SPEECH -> buffer.addAll(chunk.toList())
|
||||
SttVad.END_OF_SPEECH -> { stt.transcribe(buffer); vad.reset() }
|
||||
}
|
||||
|
||||
// 3) TTS in-process (Qwen3-TTS + clonage vocal embarqué)
|
||||
// TTS in-process (Qwen3-TTS + clonage vocal)
|
||||
val tts = TtsEngine(
|
||||
talkerGguf = "/data/.../talker_f32.gguf",
|
||||
vocabGguf = "/data/.../Qwen3-4B-Q4_0.gguf",
|
||||
dumpDir = "/data/.../tts_dump",
|
||||
useHtp = false,
|
||||
nThreads = 6
|
||||
talkerGguf = "/data/.../talker_f32.gguf",
|
||||
vocabGguf = "/data/.../Qwen3-4B-Q4_0.gguf",
|
||||
dumpDir = "/data/.../tts_dump",
|
||||
nThreads = 6
|
||||
)
|
||||
val r = tts.synthesize("Bonjour, comment ça va ?", "/sdcard/out.wav")
|
||||
|
||||
// 4) LLM via libllama (Qwen3.5-4B Q4_0 cascade Speaker+Thinker)
|
||||
// -- intégration via lib séparée libkazeia_engine.so + façade existante.
|
||||
// Clonage vocal embarqué (optionnel)
|
||||
val pierreVoice = tts.encodeSpeakerWav("/sdcard/voix_pierre.wav")
|
||||
tts.synthesize("Bonjour Pierre.", outWav, xvectorOverride = pierreVoice)
|
||||
|
||||
// LLM (Qwen3.5 GGUF cascade Speaker + Thinker)
|
||||
val llm = EngineLlmEngine("/data/.../Qwen3.5-4B-Q4_0.gguf", ctx = 4096, nThreads = 6)
|
||||
val reply = llm.generate(sys = "Tu es Kazeia...", usr = userInput, max = 96)
|
||||
```
|
||||
|
||||
## Composants livrés à ce jour
|
||||
## Pattern d'unification recommandé (cf demande dev Kazeia)
|
||||
|
||||
| Composant | État | Lib | Doc |
|
||||
|---|---|---|---|
|
||||
| LLM Qwen3.5-4B / Qwen3.5-9B GGUF | prod | `libllama.so` + Hexagon backend | `CLAUDE.md` |
|
||||
| TTS Talker + CP + decoder ggml | prod | `libkazeia_tts.so` | `tts_engine.h` |
|
||||
| Clonage vocal speaker encoder ECAPA-TDNN | **livré 31/05** | `libkazeia_tts.so` | mémoire `project_tts_voice_cloning` |
|
||||
| STT Whisper-Small NPU C++ | **livré 31/05** | `libkazeia_stt.so` | **`STT_INTEGRATION.md`** |
|
||||
| VAD RMS C++ | **livré 31/05** | `libkazeia_stt.so` | idem |
|
||||
| Mel extractor partagé (FFT) | **livré 31/05** | `kazeia_mel.{h,cpp}` | header |
|
||||
Pour permettre une bascule progressive `prod` ↔ `lib` sans dégeler les sous-systèmes, le dev a câblé un flag de dispatch côté app pour STT et propose de faire pareil pour LLM et TTS :
|
||||
|
||||
## Empreinte mémoire (Snapdragon 8 Elite, 16 GB RAM)
|
||||
```kotlin
|
||||
// Dans KazeiaService.kt (ou équivalent)
|
||||
class KazeiaService {
|
||||
private val sttEngine = when (flag("sttEngine", default = "prod")) {
|
||||
"lib" -> SttEngine(...) // libkazeia_stt
|
||||
else -> WhisperHybridEngine(...) // legacy ONNX/QNN direct
|
||||
}
|
||||
private val llmEngine = when (flag("llmEngine", default = "prod")) {
|
||||
"lib" -> EngineLlmEngine(...) // libkazeia_engine
|
||||
else -> ExecuTorchLlmEngine(...) // legacy .pte
|
||||
}
|
||||
private val ttsEngine = when (flag("ttsEngine", default = "prod")) {
|
||||
"lib" -> TtsEngine(...) // libkazeia_tts
|
||||
else -> LegacyTtsPipeline(...) // legacy à supprimer une fois libé validée
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Toggle via `adb shell setprop kazeia.sttEngine lib` (ou équivalent du flag system). Bascule réversible, mesurable en CER/WER/RTF avant de flip le default.
|
||||
|
||||
État réel des 3 dispatch flags (à confirmer côté dev) :
|
||||
- ✅ STT : flag câblé, default `prod`, A/B in-app validé
|
||||
- ⏳ LLM : flag à ajouter (mirror exact du STT). Lib + façade + doc prêts.
|
||||
- ⏳ TTS : flag à ajouter une fois `libkazeia_tts.so` validée load + 1 synth in-app.
|
||||
|
||||
## État livré (01/06/2026, commit en cours)
|
||||
|
||||
| | Lib `.so` | Façade Kotlin | JNI source | Doc d'intégration | Bug investigué |
|
||||
|---|:---:|:---:|:---:|:---:|---|
|
||||
| **STT** | ✅ 183 KB | ✅ `SttEngine.kt` | ✅ `kazeia_stt_jni.cpp` | ✅ `STT_INTEGRATION.md` 15 KB | ✅ 2 bugs fixés (DFT 400 + forced prompt) |
|
||||
| **TTS** | ✅ 929 KB | ✅ `TtsEngine.kt` | ✅ `kazeia_tts_jni.cpp` | ✅ **`TTS_INTEGRATION.md` 10 KB** (cette session) | ✅ Crash load = libs manquantes, liste exhaustive donnée |
|
||||
| **LLM** | ✅ 60 KB | ✅ `EngineLlmEngine.kt` | ✅ `kazeia_engine_jni.cpp` | ✅ **`LLM_INTEGRATION.md` 10 KB** (cette session) | ✅ Bug threads = `nThreads=4` hardcoded, fix `nThreads=6` paramétrable |
|
||||
|
||||
## Empreinte mémoire (Snapdragon 8 Elite, 16 GB RAM, mesures r3)
|
||||
|
||||
| Composant chargé | RAM peak |
|
||||
|---|---:|
|
||||
| LLM Qwen3.5-4B Q4_0 | 2.4 GB |
|
||||
| LLM Qwen3.5-9B Q4_0 | 5.0 GB |
|
||||
| STT Whisper-Small NPU (encoder + decoder QAIRT) | 379 MB |
|
||||
| TTS (talker + CP + decoder + vocab + xvector fixture) | 3.5 GB |
|
||||
| TTS clonage vocal (speaker encoder, en plus du fixture) | +34 MB |
|
||||
| STT Whisper-Small NPU (encoder + decoder QAIRT) | 378 MB |
|
||||
| **Total cascade Speaker 4B + STT + TTS** | **~6.3 GB** (sur 16 GB dispo) |
|
||||
| TTS clonage vocal (speaker encoder, en plus) | +34 MB |
|
||||
| LLM Qwen3.5-4B Q4_0 (1 instance) | 2.4 GB |
|
||||
| LLM Qwen3.5-9B Q4_0 (1 instance) | 5.0 GB |
|
||||
| **Cascade typique** : STT + Speaker 4B + Guard 4B + TTS | ~9 GB / 16 GB |
|
||||
|
||||
## Pipeline vocal de bout en bout (latence typique)
|
||||
## Perf bout-en-bout (latence typique)
|
||||
|
||||
```
|
||||
mic 16 kHz PCM16
|
||||
↓ AudioRecord
|
||||
SttVad (RMS énergie) → ~830 ms après fin de parole (silence 800 ms + overhead)
|
||||
SttVad RMS → ~830 ms après fin parole (silence 800 ms + overhead)
|
||||
↓ end_of_speech
|
||||
SttEngine.transcribe(pcm) → ~0.4-2.0 s selon longueur audio (RTF 0.4)
|
||||
SttEngine.transcribe(pcm) → ~500-2000 ms (RTF 0.18-0.40 sur audio 3-10s FR)
|
||||
↓ texte FR
|
||||
LLM Qwen3.5-4B cascade → ~5-10 s (Guard + Speaker, 4-9 GB total)
|
||||
LLM Speaker 4B (+ Guard 4B) → ~5-10 s (cascade Speaker + Guard 4B)
|
||||
↓ réponse FR
|
||||
TtsEngine.synthesize(text) → ~3-10 s (RTF 2.5-3.0 selon longueur)
|
||||
TtsEngine.synthesize(text) → ~3-10 s (RTF 2.5-3.0 selon longueur)
|
||||
↓
|
||||
audio playback
|
||||
```
|
||||
|
||||
Total latence voix→audio : ~10-25 s selon longueur du tour, dominée par LLM + TTS.
|
||||
Latence totale voix→audio : ~10-25 s par tour, dominée par LLM + TTS.
|
||||
|
||||
## Documentation par sous-système
|
||||
## Documentation détaillée
|
||||
|
||||
- **LLM** : `/opt/Kazeia-engine/CLAUDE.md` (état complet)
|
||||
- **TTS** : `tts_engine.h` (API publique) + clonage vocal section §1 du STT_INTEGRATION.md
|
||||
- **STT** : **`STT_INTEGRATION.md`** (guide intégration complet)
|
||||
- **Mémoire interne assistant** : `/home/alf/.claude/projects/-opt-Kazeia-engine/memory/`
|
||||
- **STT** : [STT_INTEGRATION.md](STT_INTEGRATION.md) — Whisper-Small NPU, VAD, options QNN, A/B accuracy
|
||||
- **TTS** : [TTS_INTEGRATION.md](TTS_INTEGRATION.md) — Qwen3-TTS, clonage vocal, sampling tuning
|
||||
- **LLM** : [LLM_INTEGRATION.md](LLM_INTEGRATION.md) — Qwen3.5/Qwen3, option C, threading
|
||||
- **Patch QNN options prod (gratuit, sans migration)** : [PATCH_QNN_PROD.md](PATCH_QNN_PROD.md)
|
||||
- **Engine global (CLAUDE.md projet)** : `/opt/Kazeia-engine/CLAUDE.md`
|
||||
- **Mémoires assistant** : `/home/alf/.claude/projects/-opt-Kazeia-engine/memory/`
|
||||
|
||||
## Pour intégrer côté app Android
|
||||
## Pour démarrer l'intégration
|
||||
|
||||
Lire `STT_INTEGRATION.md` — guide complet de migration depuis `WhisperHybridEngine.kt`. Les libs `libkazeia_tts.so` et `libkazeia_stt.so` partagent le même runtime (NDK r27d, arm64-v8a, API 31+, `-march=armv8.6-a+i8mm+bf16+dotprod+fp16`).
|
||||
1. Lire les 3 docs d'intégration dans l'ordre : STT → TTS → LLM.
|
||||
2. Pousser les libs nécessaires dans `jniLibs/arm64-v8a/` (cf §2.b de chaque doc).
|
||||
3. Copier les 3 façades Kotlin (`SttEngine.kt`, `TtsEngine.kt`, `EngineLlmEngine.kt`).
|
||||
4. Câbler les 3 flags dispatch côté app pour bascule progressive (cf section ci-dessus).
|
||||
5. Valider la checklist §7 de chaque doc en premier (System.loadLibrary + 1 call OK).
|
||||
6. Bench A/B `prod` vs `lib` sur audios/prompts/modèles fixtures représentatifs.
|
||||
7. Flip default vers `lib` quand les 3 sous-systèmes passent les gates (CER/WER STT, RTF TTS, decode tok/s LLM).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,313 @@
|
|||
# Intégration LLM Kazeia-Engine — Guide développeur
|
||||
|
||||
**Date** : 01/06/2026
|
||||
**Cible** : remplacer le pipeline LLM ExecuTorch `.pte` par `libkazeia_engine.so` + façade Kotlin `EngineLlmEngine.kt`. Modèle GGUF chargé directement (pas de conversion .pte), prefill HTP / decode CPU automatique (option C).
|
||||
|
||||
**Runtime** : libllama (fork ggml-org/llama.cpp commit `f0fe1058b`) + libggml fork ql (Qualcomm) avec backend Hexagon HTP V79.
|
||||
|
||||
**Modèles supportés** (auto-détection via `general.architecture` du GGUF) :
|
||||
- **Qwen3 dense** (Qwen3-4B, Qwen3Guard-Gen-4B/8B) : HTP contexte-unique, matmul HVX (HMX off, fault 0x2e), prefill+decode HTP
|
||||
- **Qwen3.5 hybride GDN** (Qwen3.5-4B, Qwen3.5-9B) : **option C** — prefill HTP avec HMX (159 tok/s) / decode CPU NEON (16.5 tok/s) avec transfert KV automatique entre contextes
|
||||
- **Fallback CPU pur** si pas de HTP détecté
|
||||
|
||||
**Perf prouvée** (Snapdragon 8 Elite SM8750, t=6, standalone CLI) :
|
||||
|
||||
| Modèle | Prefill | Decode | RAM | Mode |
|
||||
|---|---:|---:|---:|---|
|
||||
| Qwen3-4B-Q4_0 | 103 tok/s (HTP) | 17 tok/s (CPU) | 2.4 GB | dense HTP |
|
||||
| Qwen3.5-4B-Q4_0 | 55 tok/s (HTP) | 15 tok/s (CPU) | 2.4 GB | option C |
|
||||
| Qwen3.5-9B-Q4_0 | 41 tok/s (HTP) | 8.4 tok/s (CPU) | 5.0 GB | option C |
|
||||
| Qwen3Guard-Gen-4B-Q4_K_M | 89 tok/s (HTP) | 22 tok/s (CPU) | 2.5 GB | dense HTP |
|
||||
|
||||
⚠️ **Note in-app perf** : le sweet spot threads in-app est `nThreads=6` (mesure `r3`). Versions précédentes du bridge utilisaient `nThreads=4` hardcoded — corrigé dans le commit ce session.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ce que la lib fait
|
||||
|
||||
```
|
||||
libkazeia_engine.so (60 KB, SHARED, dépend de libllama + libggml*)
|
||||
├── EngineLlmEngine load(ggufPath, nCtx=4096, nThreads=6) -> handle
|
||||
├── EngineLlmEngine generate(handle, sys, usr, maxTok) -> String # mono-tour ChatML
|
||||
├── EngineLlmEngine generateRaw(handle, prompt, maxTok) -> String # multi-tour
|
||||
├── EngineLlmEngine reset(handle) # clear KV
|
||||
├── EngineLlmEngine free(handle)
|
||||
└── (+ API TTS embeds-only : nEmbd, nVocab, prefillEmbeds, decodeEmbed — pour Talker TTS)
|
||||
```
|
||||
|
||||
API Kotlin (`EngineLlmEngine.kt`) :
|
||||
|
||||
```kotlin
|
||||
val llm = EngineLlmEngine(
|
||||
model = "/data/.../Qwen3.5-4B-Q4_0.gguf",
|
||||
ctx = 4096,
|
||||
nThreads = 6 // sweet spot Snapdragon 8 Elite r3
|
||||
)
|
||||
|
||||
// Mono-tour avec system + 1 message
|
||||
val reply = llm.generate(
|
||||
sys = "Tu es Kazeia, un assistant psychologique bienveillant. Réponds en français bref.",
|
||||
usr = "Je me sens un peu seul.",
|
||||
max = 96
|
||||
)
|
||||
|
||||
// Multi-tour via ChatSession (gère ChatML + thinking-off déterministe)
|
||||
val chat = llm.newChat(system = "...")
|
||||
val r1 = chat.ask("Bonjour")
|
||||
val r2 = chat.ask("Comment vous allez ?")
|
||||
chat.clear()
|
||||
|
||||
llm.release()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Build (côté app Android)
|
||||
|
||||
### a. Récupérer les artifacts
|
||||
|
||||
```bash
|
||||
cd /opt/Kazeia-engine/dist
|
||||
# Recompile libkazeia_engine.so (60 KB)
|
||||
NDK=/opt/Kazeia/android-ndk-r27d
|
||||
$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android31-clang++ \
|
||||
-std=c++17 -O3 -fPIC \
|
||||
-march=armv8.6-a+i8mm+bf16+dotprod+fp16 \
|
||||
-I./include -shared jni/kazeia_engine_jni.cpp \
|
||||
-L./lib -lllama -lggml -lggml-base -llog -ldl -lm \
|
||||
-o b-jni/libkazeia_engine.so
|
||||
```
|
||||
|
||||
### b. Libs à copier dans `app/src/main/jniLibs/arm64-v8a/`
|
||||
|
||||
**8 fichiers obligatoires** (vérifiés via `readelf -d` sur la lib + dépendances transitives `libllama.so`) :
|
||||
|
||||
| Fichier | Taille | Rôle |
|
||||
|---|---:|---|
|
||||
| `libkazeia_engine.so` | 60 KB | la lib elle-même |
|
||||
| `libllama.so` | 35 MB | fork ggml-org/llama.cpp |
|
||||
| `libggml.so` | 627 KB | ggml core (charge cpu+hexagon backends dynamiquement) |
|
||||
| `libggml-base.so` | 6.6 MB | ggml base ops |
|
||||
| `libggml-cpu.so` | 4.7 MB | backend CPU NEON ARM v8.6 |
|
||||
| `libggml-hexagon.so` | 3.3 MB | backend Hexagon HTP host wrapper |
|
||||
| `libggml-htp-v79.so` | 354 KB | DSP skel V79 (chargé via dlopen runtime) |
|
||||
| `libc++_shared.so` | 1.8 MB | NDK r27d STL |
|
||||
|
||||
**Total** : ~52 MB. **Mutualisable avec libkazeia_tts.so** — si tu intègres TTS en parallèle, ces 7 deps sont les mêmes (juste `libkazeia_engine.so` et `libkazeia_tts.so` qui s'ajoutent en plus).
|
||||
|
||||
### c. Façade Kotlin
|
||||
|
||||
Copie `dist/jni/EngineLlmEngine.kt` dans `app/src/main/java/com/kazeia/llm/EngineLlmEngine.kt`. Package = `com.kazeia.llm`.
|
||||
|
||||
### d. Modèles sur la tablette
|
||||
|
||||
```
|
||||
/data/local/tmp/kazeia/models/
|
||||
├── Qwen3.5-4B-Q4_0.gguf # 2.4 GB — speaker / thinker principal
|
||||
├── Qwen3.5-9B-Q4_0.gguf # 5.0 GB — option si qualité prioritaire
|
||||
├── Qwen3Guard-Gen-4B.Q4_K_M.gguf # 2.5 GB — Guard (modération)
|
||||
└── Qwen3-4B-Q4_0.gguf # 2.4 GB — pour cascade Speaker dense
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Migration depuis ExecuTorch (.pte)
|
||||
|
||||
| Avant (ExecuTorch) | Après (Kazeia-Engine) |
|
||||
|---|---|
|
||||
| `model.pte` 3.3 GB | `Qwen3.5-4B-Q4_0.gguf` 2.4 GB (-30 %) |
|
||||
| Conversion .pte requise (chaîne ExecuTorch + QNN compile) | Pas de conversion : GGUF chargé direct |
|
||||
| Pipeline 100% NPU INT4 | Option C : prefill HTP / decode CPU NEON |
|
||||
| Decode 14-21 tok/s | Decode 15-17 tok/s (4B), équivalent |
|
||||
| Pas extensible à Qwen3.5 (ExecuTorch ne sait pas l'exporter) | Qwen3.5 hybride GDN supporté nativement |
|
||||
| Pas de cascade multi-modèle facile | Cascade Speaker+Thinker via 2 instances `EngineLlmEngine` |
|
||||
|
||||
Suppression côté app : tout le pipeline `ExecuTorchLlmEngine` et ses dépendances ExecuTorch peuvent être retirés.
|
||||
|
||||
---
|
||||
|
||||
## 4. Exemples d'usage
|
||||
|
||||
### Mono-tour psy bref
|
||||
|
||||
```kotlin
|
||||
val llm = EngineLlmEngine(
|
||||
model = "$kazeiaDir/models/Qwen3.5-4B-Q4_0.gguf",
|
||||
ctx = 2048,
|
||||
nThreads = 6
|
||||
)
|
||||
val reply = llm.generate(
|
||||
sys = "Tu es Kazeia, psy bienveillant. Une phrase brève.",
|
||||
usr = "Je rumine la nuit sans dormir.",
|
||||
max = 64
|
||||
)
|
||||
llm.release()
|
||||
```
|
||||
|
||||
### Multi-tour ChatSession
|
||||
|
||||
```kotlin
|
||||
val llm = EngineLlmEngine("$kazeiaDir/Qwen3.5-4B-Q4_0.gguf", nThreads = 6)
|
||||
val chat = llm.newChat("Tu es Kazeia, psy bienveillant.")
|
||||
chat.ask("Bonsoir.")
|
||||
chat.ask("Je me sens vide.")
|
||||
chat.ask("Depuis le départ de ma fille.")
|
||||
chat.clear() // reset historique
|
||||
llm.release()
|
||||
```
|
||||
|
||||
### Cascade Speaker + Thinker
|
||||
|
||||
```kotlin
|
||||
val speaker = EngineLlmEngine("$kazeiaDir/Qwen3.5-9B-Q4_0.gguf", nThreads = 6)
|
||||
val thinker = EngineLlmEngine("$kazeiaDir/Qwen3Guard-Gen-4B.Q4_K_M.gguf", nThreads = 4)
|
||||
// 4 threads sur Thinker pour laisser 4 cœurs au Speaker en parallèle
|
||||
|
||||
val draft = speaker.generate(sysSpeaker, userInput, max = 96)
|
||||
val safe = thinker.generate(sysGuard, "Réponse à vérifier: $draft", max = 32)
|
||||
// si safe == "OK" -> envoyer draft, sinon reformuler
|
||||
|
||||
speaker.release(); thinker.release()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Codes d'erreur
|
||||
|
||||
`load()` renvoie `0L` (= `IllegalStateException` Kotlin via `require`) si :
|
||||
- modèle introuvable ou GGUF invalide
|
||||
- pas assez de RAM
|
||||
- erreur ggml_backend_init (rare)
|
||||
|
||||
`generate()` / `generateRaw()` renvoient un texte vide `""` si :
|
||||
- tokenize fail
|
||||
- llama_decode prefill fail
|
||||
- KV transfer fail (option C uniquement)
|
||||
|
||||
Pas de codes int dédiés (différent de TTS/STT) — l'API LLM est plus simple. Log via stderr `kazeia-engine:` pour le debug.
|
||||
|
||||
---
|
||||
|
||||
## 6. Bench rapide standalone
|
||||
|
||||
`llama-cli` est inclus dans `dist/` pour mesurer la perf avant intégration :
|
||||
|
||||
```bash
|
||||
adb shell '
|
||||
cd /data/local/tmp/kz-engine
|
||||
export LD_LIBRARY_PATH=./lib
|
||||
export ADSP_LIBRARY_PATH=./lib
|
||||
./llama-cli -m Qwen3.5-4B-Q4_0.gguf \
|
||||
-p "Bonjour, comment vas-tu ?" \
|
||||
-n 64 -t 6 -ngl 99 --reasoning-budget 0 -dev HTP0
|
||||
'
|
||||
```
|
||||
|
||||
Bench standardisé :
|
||||
```bash
|
||||
./llama-bench -m Qwen3.5-4B-Q4_0.gguf -t 6 -ngl 99 -p 32,128,512 -n 16
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Checklist d'intégration
|
||||
|
||||
1. **`System.loadLibrary("kazeia_engine")` réussit** au démarrage. Si erreur `library "libllama.so" not found`, vérifier les 8 libs dans `jniLibs/arm64-v8a/`.
|
||||
|
||||
2. **Charge un engine + 1 generate** sur un modèle fixture (Qwen3.5-4B) avec un prompt simple. Le log stderr doit indiquer le mode détecté :
|
||||
```
|
||||
kazeia-engine: HYBRIDE GDN (qwen35) -> option C, prefill HTP+HMX (t=8) / decode CPU (t=6)
|
||||
```
|
||||
ou
|
||||
```
|
||||
kazeia-engine: DENSE (qwen3) -> HTP contexte-unique, matmul HVX (HMX off) (t=6)
|
||||
```
|
||||
ou
|
||||
```
|
||||
kazeia-engine: CPU pur (qwen3, pas de HTP) (t=6)
|
||||
```
|
||||
|
||||
3. **Vérifier n_threads in-app** : si tu vois decode << 10 tok/s sur Qwen3.5-4B, c'est que les threads tombent à 1 cœur (bug Android scheduler). Solution : forcer affinity 8 grands cœurs depuis Kotlin avant de générer (cf §8).
|
||||
|
||||
4. **Sanity null/edge** :
|
||||
- prompt vide → texte vide
|
||||
- maxTok = 0 → texte vide
|
||||
- modèle inexistant → IllegalStateException au constructeur
|
||||
|
||||
5. **Threading app** : `generate()` est synchrone bloquant. Appeler depuis `Dispatchers.IO` ou un thread dédié. Pas multi-thread sur le même handle (1 generate à la fois par engine).
|
||||
|
||||
6. **Cycle de vie** : `release()` quand l'app sort du foreground (libère 2.4-5.0 GB de RAM selon modèle).
|
||||
|
||||
---
|
||||
|
||||
## 8. Piège connu : affinity Android in-app
|
||||
|
||||
**Symptôme** : decode tombe de 9.8 tok/s (CLI standalone) à 0.32 tok/s in-app sur le même modèle.
|
||||
|
||||
**Cause** : le scheduler Android plante le process sur 1 petit cœur quand l'app n'est pas au foreground actif, ou quand il n'y a pas de hint thermique. `llama-cli` standalone via adb shell évite ce piège (process root-like).
|
||||
|
||||
**Fix côté app** :
|
||||
|
||||
```kotlin
|
||||
import android.os.Process
|
||||
import java.io.RandomAccessFile
|
||||
|
||||
private fun pinToBigCores() {
|
||||
// Snapdragon 8 Elite : 8 cores total, cores 0-3 = grands (big), 4-7 = petits (medium/little).
|
||||
// En pratique sur SM8750, masque 0xFC laisse tomber les 2 petits cores et garde les 6 grands.
|
||||
try {
|
||||
val tid = Process.myTid()
|
||||
// sched_setaffinity via /proc/self/task/<tid>/cgroup ou via JNI taskset.
|
||||
// Sinon, fallback : démarrer le thread avec Thread.MAX_PRIORITY.
|
||||
Thread.currentThread().priority = Thread.MAX_PRIORITY
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND)
|
||||
} catch (e: Throwable) { /* best effort */ }
|
||||
}
|
||||
|
||||
// Dans le ViewModel/Service qui héberge l'engine :
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
pinToBigCores()
|
||||
val reply = llm.generate(sys, usr, max)
|
||||
withContext(Dispatchers.Main) { display(reply) }
|
||||
}
|
||||
```
|
||||
|
||||
Sans cette pin, `nThreads=6` côté JNI n'est pas suffisant — Android assigne quand même 1 cœur effectif.
|
||||
|
||||
**Validation** : pendant un generate, faire `adb shell top -H -p <pid>` et vérifier qu'on voit 6 threads actifs autour de 80-100 % CPU. Si on voit 1 thread à 100 % et les autres à 0, l'affinity n'est pas posée.
|
||||
|
||||
---
|
||||
|
||||
## 9. Perf détaillée et tuning
|
||||
|
||||
### Sweet spot threads (mesure `r3`, Snapdragon 8 Elite)
|
||||
|
||||
| Modèle | t=4 | **t=6** | t=8 |
|
||||
|---|---:|---:|---:|
|
||||
| Qwen3.5-4B Q4_0 | 8.2 | **9.8** | 7.4 (contention) |
|
||||
| Qwen3.5-9B Q4_0 | 4.4 | **5.2** | 2.1 (contention) |
|
||||
| Qwen3-4B Q4_0 | 13 | **17** | 14 |
|
||||
|
||||
`t=8` est mauvais à cause de contention (oversubscription + scheduler Android). **Garde 6 par défaut.**
|
||||
|
||||
### Cascade : adapter les threads
|
||||
|
||||
Si tu lances Speaker (4B) et Thinker (4B) en simultané :
|
||||
- Speaker `nThreads = 4`
|
||||
- Thinker `nThreads = 2`
|
||||
|
||||
Le total = 6 grands cœurs, on laisse 2 cœurs au système Android.
|
||||
|
||||
### Decode = memory-bandwidth bound
|
||||
|
||||
`r3` : decode M=1 est limité par BW LPDDR5x ~77 GB/s, pas par compute. Donc Q4_0 (2 GB poids) est ~2× plus rapide que Q8_0 (4 GB) sur le decode. Pas d'intérêt à Q6_K ou plus précis sur ce hardware.
|
||||
|
||||
---
|
||||
|
||||
## 10. Contacts & support
|
||||
|
||||
Code source : `/opt/Kazeia-engine/dist/jni/kazeia_engine_jni.cpp` + `EngineLlmEngine.kt`.
|
||||
|
||||
Bench CLI : `/opt/Kazeia-engine/dist/llama-cli` + `llama-bench`.
|
||||
|
||||
Pour reproduire un bug : capture logcat tag `kazeia-engine`, et output du log stderr (mode détecté + n_threads effectif).
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
# 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`.
|
||||
|
|
@ -3,7 +3,12 @@ package com.kazeia.llm
|
|||
// Mono-tour: generate(sys, usr). Multi-tour: ChatSession (gère l'historique + le template ChatML).
|
||||
|
||||
class EngineJni {
|
||||
external fun load(ggufPath: String, nCtx: Int): Long
|
||||
/**
|
||||
* @param nThreads decode CPU threads. Défaut 0 -> 6 (sweet spot Snapdragon 8 Elite,
|
||||
* mémoire r3). Recommandations : 6 pour Speaker 4B/9B in-app, 4 si cascade
|
||||
* simultanée Speaker+Thinker pour laisser cœurs au Thinker.
|
||||
*/
|
||||
external fun load(ggufPath: String, nCtx: Int, nThreads: Int): Long
|
||||
external fun generate(h: Long, sys: String, usr: String, maxTok: Int): String
|
||||
external fun generateRaw(h: Long, prompt: String, maxTok: Int): String // prompt complet déjà formaté
|
||||
external fun reset(h: Long)
|
||||
|
|
@ -22,9 +27,9 @@ class EngineJni {
|
|||
companion object { init { System.loadLibrary("kazeia_engine") } }
|
||||
}
|
||||
|
||||
class EngineLlmEngine(model: String, ctx: Int = 4096) {
|
||||
class EngineLlmEngine(model: String, ctx: Int = 4096, nThreads: Int = 6) {
|
||||
private val jni = EngineJni()
|
||||
private val h = jni.load(model, ctx)
|
||||
private val h = jni.load(model, ctx, nThreads)
|
||||
init { require(h != 0L) { "Kazeia-Engine: échec du chargement du modèle ($model)" } }
|
||||
|
||||
// Mono-tour : system + un message user.
|
||||
|
|
|
|||
|
|
@ -87,11 +87,18 @@ static std::string kengine_run(KEngine* k, const std::string& p, int maxTok) {
|
|||
return out;
|
||||
}
|
||||
|
||||
// LOAD : nThreads contrôle le n_threads du decode CPU.
|
||||
// Sur Snapdragon 8 Elite (SM8750), l'optimum mesuré sur r3 est t=6 pour Qwen3.5-4B
|
||||
// (decode 9.8 tok/s) et t=6 pour 9B (5.2 tok/s). t=8 réduit à cause de contention
|
||||
// thermique + interaction avec le scheduler Android (in-app souvent dégradé).
|
||||
// Param exposé pour permettre le tuning par voix (Speaker vs Thinker).
|
||||
extern "C" JNIEXPORT jlong JNICALL
|
||||
Java_com_kazeia_llm_EngineJni_load(JNIEnv* e, jobject, jstring path, jint nctx) {
|
||||
Java_com_kazeia_llm_EngineJni_load(JNIEnv* e, jobject, jstring path, jint nctx, jint nThreads) {
|
||||
const char* path_c = e->GetStringUTFChars(path, 0);
|
||||
std::string p(path_c);
|
||||
e->ReleaseStringUTFChars(path, path_c);
|
||||
const int N_DECODE = (nThreads > 0) ? nThreads : 6; // défaut sweet spot r3
|
||||
const int N_PREFILL = 8; // prefill HTP : 8 OK (HTP gère)
|
||||
|
||||
// Détection d'archi AVANT init backend (USE_HMX est latché à l'init).
|
||||
std::string arch = read_arch(p.c_str());
|
||||
|
|
@ -114,22 +121,24 @@ Java_com_kazeia_llm_EngineJni_load(JNIEnv* e, jobject, jstring path, jint nctx)
|
|||
}
|
||||
if (m_h && hybrid) {
|
||||
// qwen3.5-like (GDN) -> OPTION C : prefill HTP (HMX) + instance CPU pour le decode (decode GDN HTP = lent).
|
||||
c_h = make_ctx(m_h, nctx, 8, LLAMA_FLASH_ATTN_TYPE_ENABLED);
|
||||
c_h = make_ctx(m_h, nctx, N_PREFILL, LLAMA_FLASH_ATTN_TYPE_ENABLED);
|
||||
auto mp_c = llama_model_default_params(); mp_c.n_gpu_layers = 0;
|
||||
m_c = llama_model_load_from_file(p.c_str(), mp_c);
|
||||
if (m_c) c_c = make_ctx(m_c, nctx, 4, LLAMA_FLASH_ATTN_TYPE_ENABLED);
|
||||
fprintf(stderr, "kazeia-engine: HYBRIDE GDN (%s) -> option C, prefill HTP+HMX / decode CPU\n", arch.c_str());
|
||||
if (m_c) c_c = make_ctx(m_c, nctx, N_DECODE, LLAMA_FLASH_ATTN_TYPE_ENABLED);
|
||||
fprintf(stderr, "kazeia-engine: HYBRIDE GDN (%s) -> option C, prefill HTP+HMX (t=%d) / decode CPU (t=%d)\n",
|
||||
arch.c_str(), N_PREFILL, N_DECODE);
|
||||
} else if (m_h) {
|
||||
// dense -> HTP contexte-unique, matmul HVX (HMX off) : prefill+decode HTP, stable, ~3x CPU.
|
||||
c_h = make_ctx(m_h, nctx, 4, LLAMA_FLASH_ATTN_TYPE_ENABLED);
|
||||
fprintf(stderr, "kazeia-engine: DENSE (%s) -> HTP contexte-unique, matmul HVX (HMX off)\n", arch.c_str());
|
||||
c_h = make_ctx(m_h, nctx, N_DECODE, LLAMA_FLASH_ATTN_TYPE_ENABLED);
|
||||
fprintf(stderr, "kazeia-engine: DENSE (%s) -> HTP contexte-unique, matmul HVX (HMX off) (t=%d)\n",
|
||||
arch.c_str(), N_DECODE);
|
||||
} else {
|
||||
// pas de HTP (ou échec du load HTP) -> CPU pur universel.
|
||||
auto mp_c = llama_model_default_params(); mp_c.n_gpu_layers = 0;
|
||||
m_c = llama_model_load_from_file(p.c_str(), mp_c);
|
||||
if (!m_c) return 0;
|
||||
c_c = make_ctx(m_c, nctx, 4, LLAMA_FLASH_ATTN_TYPE_ENABLED);
|
||||
fprintf(stderr, "kazeia-engine: CPU pur (%s, pas de HTP)\n", arch.c_str());
|
||||
c_c = make_ctx(m_c, nctx, N_DECODE, LLAMA_FLASH_ATTN_TYPE_ENABLED);
|
||||
fprintf(stderr, "kazeia-engine: CPU pur (%s, pas de HTP) (t=%d)\n", arch.c_str(), N_DECODE);
|
||||
}
|
||||
|
||||
auto* k = new KEngine{ m_h, c_h, m_c, c_c,
|
||||
|
|
|
|||
Loading…
Reference in New Issue