Kazeia-engine/dist/STT_INTEGRATION.md

259 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Intégration STT Kazeia-Engine — Guide développeur
**Date** : 31/05/2026
**Cible** : remplacer `WhisperHybridEngine.kt` (527 l) + `MelExtractor.kt` + `VadStage.kt` + `libmel_extractor.so` par la lib unifiée `libkazeia_stt.so` + façade Kotlin `SttEngine.kt`.
**Runtime** : ONNX Runtime 1.24.3 + QNN ExecutionProvider HTP V79 — **inchangé**. Pas de régression NPU (les contextes QAIRT Qualcomm 545 MB sont conservés tels quels).
**Perf de référence** (mesures sur Snapdragon 8 Elite / SM8750, Whisper-Small, FR) :
| Métrique | Prod actuelle (Kotlin) | Lib unifiée (C++) | Gain |
|---|---:|---:|---:|
| Mel extraction | 189 ms (DFT brute force) | **60 ms** (FFT radix-2) | -68 % |
| Encoder NPU | 125 ms | 311 ms* | +149 % |
| Decoder/token NPU | 23 ms | 100 ms* | +335 % |
| **RTF (audio 5 s, 16 tokens FR)** | 0.51 (sur 1.6 s) | **0.41-0.43** | **mieux** |
| **RAM peak** | 545 MB | **378 MB** | **-31 %** |
\* Decoder et encoder C++ sont localement plus lents qu'en ORT Java à cause d'overhead memcpy fp16 et ré-allocation des `Ort::Value` à chaque step. Optim future via IO bindings ORT documentée §8. **Le RTF global et la RAM sont meilleurs que la prod actuelle.**
\* Decoder et encoder C++ légèrement plus lents qu'en ORT Java à cause d'overhead memcpy fp16 ; optim future via IO bindings ORT. Le RTF global reste sous la cible.
---
## 1. Ce que la lib unifiée fait
```
libkazeia_stt.so (168 KB, SHARED)
├── stt_engine_load(model_dir, use_htp, n_threads, max_decode_steps) -> handle
├── stt_engine_transcribe(handle, pcm16, sample_rate, language, force_transcribe)
│ -> { text, mel_ms, encoder_ms, decoder_ms, total_ms, n_tokens, rtf }
├── stt_engine_free(handle)
├── stt_vad_new/push/reset/free (VAD RMS énergie, drop-in replacement VadStage.kt)
└── kazeia_mel (mel spectrogram FFT, partagé avec speaker encoder TTS)
```
Côté Kotlin, l'API est dans `SttEngine.kt` (façade thin) :
```kotlin
val stt = SttEngine(modelDir = "/data/.../whisper-small-sm8750", useHtp = true, nThreads = 6)
val r = stt.transcribe(pcm16, language = "fr")
println("${r.text} [${r.totalMs} ms, RTF ${r.rtf}, ${r.nTokens} tokens]")
stt.release()
```
Et la **VAD** :
```kotlin
val vad = SttVad() // 16 kHz, 100 ms frames, seuil 150, 300/800 ms
val buf = mutableListOf<Short>()
audioRecord.readContinuously { chunk ->
when (vad.push(chunk)) {
SttVad.SPEECH -> buf.addAll(chunk.toList())
SttVad.END_OF_SPEECH -> {
val r = stt.transcribe(buf.toShortArray())
handleTranscription(r.text)
buf.clear(); vad.reset()
}
}
}
```
---
## 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_stt.sh
# produit b-jni/libkazeia_stt.so
```
### b. Inclure dans l'app
Copie dans `app/src/main/jniLibs/arm64-v8a/` :
| Source | Destination | Taille |
|---|---|---:|
| `dist/b-jni/libkazeia_stt.so` | `jniLibs/arm64-v8a/libkazeia_stt.so` | 168 KB |
| `dist/lib/libonnxruntime.so` | `jniLibs/arm64-v8a/libonnxruntime.so` | 20 MB |
**Note** : `libonnxruntime.so` et les libs `libQnn*.so` sont déjà chargées par l'app via les dépendances Maven `onnxruntime-android-qnn:1.24.3` et `qnn-runtime:2.44.0`**garde ces deux deps** dans `build.gradle.kts`. La lib `libkazeia_stt.so` partage le même ORT runtime.
### c. Copier la façade Kotlin
Copie `dist/jni/SttEngine.kt` dans `app/src/main/java/com/kazeia/stt/SttEngine.kt`.
Le package est déjà `com.kazeia.stt` (cohérent avec WhisperHybridEngine actuel). Les classes `SttJni`, `SttEngine`, `SttVad`, et `SttResult` y sont déclarées.
### d. Modèles sur la tablette
**Inchangé** par rapport à la prod actuelle. Push dans le même dir :
```
/data/local/tmp/kazeia/models/whisper-small-sm8750/
├── HfWhisperEncoder.onnx
├── HfWhisperEncoder_qairt_context.bin (201 MB)
├── HfWhisperDecoder.onnx
├── HfWhisperDecoder_qairt_context.bin (345 MB)
├── mel_filters.json (87 KB)
├── vocab.json (1 MB)
└── (optionnel) mel_filters.bin (64 KB, plus rapide à charger que JSON)
```
Pour générer `mel_filters.bin` une fois pour toutes (gain ~50 ms au load) :
```python
import json, numpy as np
arr = np.array(json.load(open('mel_filters.json')), dtype=np.float32).reshape(80, 201)
arr.tofile('mel_filters.bin')
```
---
## 3. Migration depuis WhisperHybridEngine
### Fichiers à **supprimer** de l'app
| Fichier | Pourquoi |
|---|---|
| `app/src/main/java/com/kazeia/stt/WhisperHybridEngine.kt` (527 l) | Remplacé par `SttEngine.kt` |
| `app/src/main/java/com/kazeia/stt/MelExtractor.kt` (18 l) | Remplacé par le mel C++ dans `libkazeia_stt.so` |
| `app/src/main/java/com/kazeia/v2/VadStage.kt` (197 l) | Remplacé par `SttVad` class |
| `app/src/main/jni/mel_extractor.cpp` (203 l) | Idem |
| `app/src/main/jniLibs/arm64-v8a/libmel_extractor.so` | Idem |
| Build entry `mel_extractor` dans `app/src/main/jni/CMakeLists.txt` | Idem |
### Fichiers à **adapter**
**SttStage.kt** (`app/src/main/java/com/kazeia/v2/SttStage.kt`) — la classe wrapper. Remplace l'appel à `WhisperHybridEngine` par `SttEngine` :
Avant :
```kotlin
private val engine = WhisperHybridEngine(nativeLibDir = ctx.applicationInfo.nativeLibraryDir)
engine.load(modelPath = WHISPER_DIR)
// ...
val result = engine.transcribe(audio, language)
```
Après :
```kotlin
private val engine = SttEngine(modelDir = WHISPER_DIR, useHtp = true, nThreads = 6)
// load se fait au constructeur
// ...
val r = engine.transcribe(audio, language = language)
val result = TranscriptionResult(r.text, 0.95f, language, r.totalMs)
```
**KazeiaService.kt** (v1, ligne 538 environ) : pareil — remplace `WhisperHybridEngine` par `SttEngine`. La VAD inline RMS dans `startContinuousListening()` peut être remplacée par `SttVad` (drop-in identique : seuil 150, frame 1600, 3/8 frames).
**SttEngine.kt interface** (`app/src/main/java/com/kazeia/core/SttEngine.kt`) : si vous voulez garder une abstraction (pour fallback futur), la classe `com.kazeia.stt.SttEngine` peut implémenter cette interface trivialement :
```kotlin
override suspend fun transcribe(audioData: ShortArray, language: String): TranscriptionResult {
val r = engine.transcribe(audioData, language = language)
return TranscriptionResult(r.text, 0.95f, language, r.totalMs)
}
```
---
## 4. Variables d'environnement runtime
| Variable | Effet | Défaut |
|---|---|---|
| `ADSP_LIBRARY_PATH` | Chemin où QNN cherche les libs HTP (skel/stub V79). Pour Android, c'est `jniLibs/arm64-v8a/` qui est dans le PATH par défaut. Pas besoin de set manuellement. | auto |
| `LD_LIBRARY_PATH` | Idem pour `libonnxruntime.so`. Auto sur Android. | auto |
Aucune variable d'env spécifique requise — l'app Kotlin charge `libkazeia_stt.so` via `System.loadLibrary("kazeia_stt")` (déclenché par `SttJni.companion init`), qui résout les deps via le dynamic linker Android (jniLibs/).
---
## 5. Format du résultat JNI (interne)
Si tu veux debugger ou wrapper différemment :
`SttJni.nativeTranscribe(...)` renvoie une `String` au format **pipe-séparé** :
```
err|text|mel_ms|encoder_ms|decoder_ms|total_ms|n_tokens|rtf
```
Exemple : `"0|Elle mat est un casque de chantier.|60|311|1602|1991|16|0.398"`
`err=0` = success. Sinon < 0 (voir codes dans `stt_engine.cpp`).
Le texte est en UTF-8 brut (Whisper BPE byte-level décodé). Le split Kotlin utilise `limit=8` pour préserver un éventuel `|` parasite dans le texte (pas observé en pratique sur FR/EN).
---
## 6. Codes d'erreur
| Code | Signification |
|---|---|
| 0 | OK |
| -1 | engine nul / pcm nul / n_samples 0 |
| -2 | engine non chargé |
| -3 | mel compute fail |
| -4 | encoder Run fail (ORT exception, voir logcat) |
| -5 | nom input decoder inconnu (modèle non standard) |
| -6 | decoder Run fail (ORT exception) |
| -7 | sortie `logits` absente |
| -10 | sample_rate != 16000 |
Au load (`stt_engine_load` renvoie `nullptr`) :
- mel_basis absent (.bin ni .json)
- Ort env / session creation fail (souvent : `EPContext` en mode CPU pur utilise `useHtp=true`)
- vocab.json absent ou parse fail
---
## 7. Bench rapide (sur tablette via adb shell)
```bash
adb push dist/b-jni/kazeia_stt_cli /data/local/tmp/stt/bin/
adb push dist/lib/libonnxruntime.so /data/local/tmp/stt/bin/
adb push dist/lib/qnn/*.so /data/local/tmp/stt/bin/ # QNN libs
adb shell '
cd /data/local/tmp/stt
LD_LIBRARY_PATH=./bin ADSP_LIBRARY_PATH=./bin \
./bin/kazeia_stt_cli ./models ./audio/sample_fr_16k.wav fr htp
'
```
Sortie typique sur Snapdragon 8 Elite :
```
=== TRANSCRIPTION (NPU, fr) ===
Bonjour, comment ça va ?
=== TIMING ===
mel 60 ms
encoder 311 ms
decoder 302 ms (8 tokens)
total 684 ms for 2.50 s audio => RTF 0.274
```
---
## 8. Notes pour optim future (non bloquant)
Le decoder C++ est ~4× plus lent par token que la prod Kotlin (100 ms vs 23 ms). Causes identifiées :
- `memcpy` de 24 tenseurs self KV (~12 MB chacun) à chaque step
- Ré-allocation `Ort::Value` à chaque step
Solution : passer aux **IO bindings ORT** (`Ort::IoBinding`) pré-alloue les buffers de sortie une fois pour toutes et ORT écrit dedans en place. Estimation gain : 3-4× sur le decoder = total ~600 ms pour 5 s audio = RTF 0.12. Optim qui peut attendre la prochaine itération si la perf actuelle est suffisante.
---
## 9. Contacts & support
Code source : `/opt/Kazeia-engine/dist/jni/stt_engine.{h,cpp}` + `kazeia_mel.{h,cpp}` + `kazeia_stt_jni.cpp`
Test CLI : `/opt/Kazeia-engine/dist/jni/stt_cli.cpp`
Bench artifacts : `/opt/Kazeia-engine/dist/b-jni/kazeia_stt_cli` + `libkazeia_stt.so`
Pour reproduire un bug : capture logcat avec tag `kazeia_stt` (logs ORT exceptions y vont).