Kazeia-engine/dist/STT_INTEGRATION.md

9.7 KiB
Raw Blame History

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) :

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 :

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) :

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.0garde 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) :

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 :

private val engine = WhisperHybridEngine(nativeLibDir = ctx.applicationInfo.nativeLibraryDir)
engine.load(modelPath = WHISPER_DIR)
// ...
val result = engine.transcribe(audio, language)

Après :

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 :

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)

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