259 lines
9.7 KiB
Markdown
259 lines
9.7 KiB
Markdown
# 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).
|