// Mini-harness Android pour mesurer le gain QNN sans rien casser. // À placer temporairement dans com.kazeia.stt.BenchQnnHarness ou similaire, // déclenchable via un point d'entrée (settings dev, ou bouton caché). // // Hypothèse : l'app capture déjà des PCM16 16kHz mono lors des sessions // thérapeutiques. Si oui, on charge X audios sauvegardés et on bench // avec et sans patch QNN. Sinon : utiliser des audios fixture FR de 1-3s // (Pierre disponibles ? ou damien_5s_16k.wav du repo Kazeia-Engine/voix). // // Avant le patch : compiler/déployer l'app actuelle, exécuter cette fonction, // noter les temps. // Après le patch : appliquer whisper_hybrid_engine_qnn.diff, recompiler, // re-exécuter cette même fonction, comparer. package com.kazeia.stt import android.content.Context import android.util.Log import kotlinx.coroutines.runBlocking import java.io.File object BenchQnnHarness { private const val TAG = "BenchQnn" /** * @param audioDir directory containing test WAVs mono 16-bit 16kHz (PCM16). * @param modelDir whisper-small-sm8750 dir (= WhisperHybridEngine.WHISPER_DIR). */ fun runBench(ctx: Context, audioDir: File, modelDir: String) { val engine = WhisperHybridEngine(nativeLibDir = ctx.applicationInfo.nativeLibraryDir) { msg -> Log.i(TAG, msg) } runBlocking { engine.load(modelPath = modelDir) } if (!engine.isLoaded()) { Log.e(TAG, "engine load FAILED — cannot bench") return } val wavs = audioDir.listFiles { _, name -> name.endsWith(".wav") }?.sortedBy { it.name } ?: run { Log.e(TAG, "no WAVs in $audioDir"); return } Log.i(TAG, "=== Bench start, ${wavs.size} audios ===") // Warm-up : 1 transcribe à blanc (le 1er est cold) wavs.firstOrNull()?.let { f -> val pcm = readWavPcm16(f) ?: return@let runBlocking { engine.transcribe(pcm, "fr") } } // Bench reproductible : 3 runs par audio, garder la médiane val results = mutableListOf>() for (f in wavs) { val pcm = readWavPcm16(f) ?: continue val durMs = (pcm.size * 1000L) / 16000 val times = mutableListOf() var text = "" repeat(3) { val t0 = System.currentTimeMillis() val r = runBlocking { engine.transcribe(pcm, "fr") } val dt = System.currentTimeMillis() - t0 times.add(dt) text = r.text } times.sort() val median = times[1] results.add(Triple(f.name, median, text)) Log.i(TAG, "${f.name} : ${durMs}ms audio, median=${median}ms (${times.joinToString()}), text='${text}'") } // Récap val totalAudio = results.sumOf { val w = readWavPcm16(audioDir.resolve(it.first)) ?: ShortArray(0) (w.size * 1000L) / 16000 } val totalTime = results.sumOf { it.second } Log.i(TAG, "=== Bench end ===") Log.i(TAG, "total audio = ${totalAudio} ms, total transcribe (median sum) = ${totalTime} ms") Log.i(TAG, "weighted RTF = ${totalTime.toDouble() / totalAudio}") engine.release() } private fun readWavPcm16(f: File): ShortArray? = try { val bytes = f.readBytes() // Skip the 44-byte WAV header (or parse properly if ffmpeg may have inserted extra chunks) var dataOff = 44 // Look for "data" chunk to be safe for (i in 12 until bytes.size - 8) { if (bytes[i] == 'd'.code.toByte() && bytes[i+1] == 'a'.code.toByte() && bytes[i+2] == 't'.code.toByte() && bytes[i+3] == 'a'.code.toByte()) { dataOff = i + 8; break } } val n = (bytes.size - dataOff) / 2 ShortArray(n) { i -> ((bytes[dataOff + 2*i].toInt() and 0xFF) or (bytes[dataOff + 2*i + 1].toInt() shl 8)).toShort() } } catch (e: Exception) { Log.e(TAG, "readWavPcm16 fail ${f.name}: ${e.message}"); null } } /* USAGE : BenchQnnHarness.runBench( ctx = applicationContext, audioDir = File("/sdcard/Android/data/com.kazeia/files/bench_audio"), // ou autre modelDir = "/data/local/tmp/kazeia/models/whisper-small-sm8750" ) Mettre 10-20 audios FR Pierre dans audioDir au préalable. Pas besoin de modifier le runner pour le A/B QNN : on compare juste 2 runs d'app (avant patch / après patch) sur le même corpus. */