# 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() 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).