133 lines
4.3 KiB
Kotlin
133 lines
4.3 KiB
Kotlin
package com.kazeia.stt
|
|
|
|
// Engine STT Whisper-Small (Qualcomm AI Hub HfWhisper KV-cache) in-process.
|
|
// Backend : ONNX Runtime + QNN ExecutionProvider (HTP V79 sur SM8750).
|
|
// Empreinte RAM : ~545 MB (encoder ctx 201 MB + decoder ctx 345 MB).
|
|
//
|
|
// Bench prod (Whisper-Small, FR, audio 1.6s) : ~825 ms, RTF 0.51.
|
|
// Bench port C++ (audio 5s, FR) : ~2000 ms, RTF 0.40 — voir CLAUDE.md/STT.
|
|
//
|
|
// Le bridge natif est dans dist/jni/kazeia_stt_jni.cpp ; build via
|
|
// dist/build_kazeia_stt.sh.
|
|
|
|
class SttJni {
|
|
external fun nativeLoad(
|
|
modelDir: String,
|
|
useHtp: Boolean,
|
|
nThreads: Int,
|
|
maxDecodeSteps: Int
|
|
): Long
|
|
|
|
/** Renvoie une chaîne : "err|text|mel_ms|enc_ms|dec_ms|total_ms|n_tokens|rtf" */
|
|
external fun nativeTranscribe(
|
|
handle: Long,
|
|
pcm: ShortArray,
|
|
sampleRate: Int,
|
|
language: String,
|
|
forceTranscribe: Boolean
|
|
): String
|
|
|
|
external fun nativeFree(handle: Long)
|
|
|
|
external fun nativeVadNew(
|
|
sampleRate: Int, frameSize: Int, rmsThreshold: Int,
|
|
minSpeechFrames: Int, silenceEndFrames: Int
|
|
): Long
|
|
|
|
/** 0 = silence, 1 = speech in progress, 2 = end_of_speech declenched */
|
|
external fun nativeVadPush(handle: Long, pcm: ShortArray): Int
|
|
external fun nativeVadReset(handle: Long)
|
|
external fun nativeVadFree(handle: Long)
|
|
|
|
companion object { init { System.loadLibrary("kazeia_stt") } }
|
|
}
|
|
|
|
data class SttResult(
|
|
val text: String,
|
|
val melMs: Int,
|
|
val encoderMs: Int,
|
|
val decoderMs: Int,
|
|
val totalMs: Int,
|
|
val nTokens: Int,
|
|
val rtf: Float
|
|
)
|
|
|
|
class SttEngine(
|
|
modelDir: String,
|
|
useHtp: Boolean = true,
|
|
nThreads: Int = 6,
|
|
maxDecodeSteps: Int = 200
|
|
) {
|
|
private val jni = SttJni()
|
|
private val h = jni.nativeLoad(modelDir, useHtp, nThreads, maxDecodeSteps)
|
|
init { require(h != 0L) { "SttEngine: load FAILED (modelDir=$modelDir, useHtp=$useHtp)" } }
|
|
|
|
/**
|
|
* Transcrit un buffer PCM16 mono. Le sample rate doit être 16000 (resample externe sinon).
|
|
* @param language "fr", "en", "auto", ... (cf Whisper langues, 99 codes ISO).
|
|
* @param forceTranscribe true -> override <|translate|> en <|transcribe|>.
|
|
*/
|
|
@Throws(IllegalStateException::class)
|
|
fun transcribe(
|
|
pcm: ShortArray,
|
|
language: String = "fr",
|
|
sampleRate: Int = 16000,
|
|
forceTranscribe: Boolean = true
|
|
): SttResult {
|
|
val raw = jni.nativeTranscribe(h, pcm, sampleRate, language, forceTranscribe)
|
|
// Format : "err|text|mel|enc|dec|total|n_tokens|rtf"
|
|
// limit=8 conserve un éventuel '|' dans le texte (au cas où) dans le 2e champ.
|
|
val parts = raw.split('|', limit = 8)
|
|
if (parts.size < 8) error("SttEngine.transcribe: format inattendu '$raw'")
|
|
val err = parts[0].toInt()
|
|
if (err != 0) error("SttEngine.transcribe err=$err raw='$raw'")
|
|
return SttResult(
|
|
text = parts[1],
|
|
melMs = parts[2].toInt(),
|
|
encoderMs = parts[3].toInt(),
|
|
decoderMs = parts[4].toInt(),
|
|
totalMs = parts[5].toInt(),
|
|
nTokens = parts[6].toInt(),
|
|
rtf = parts[7].toFloat()
|
|
)
|
|
}
|
|
|
|
fun release() { jni.nativeFree(h) }
|
|
}
|
|
|
|
/**
|
|
* VAD RMS énergie compatible drop-in avec VadStage.kt prod.
|
|
* Paramètres par défaut : 16 kHz, frame 100 ms (1600 samples), seuil PCM16 brut 150,
|
|
* 3 frames pour déclencher parole (300 ms), 8 frames pour fin (800 ms).
|
|
*
|
|
* Usage typique :
|
|
* val vad = SttVad()
|
|
* audioRecord.read(buf) { pcm ->
|
|
* when (vad.push(pcm)) {
|
|
* SttVad.SILENCE -> { ... }
|
|
* SttVad.SPEECH -> { ... // bufferise }
|
|
* SttVad.END_OF_SPEECH -> { stt.transcribe(buffer) ; vad.reset() ; buffer.clear() }
|
|
* }
|
|
* }
|
|
*/
|
|
class SttVad(
|
|
sampleRate: Int = 16000,
|
|
frameSize: Int = 1600,
|
|
rmsThreshold: Int = 150,
|
|
minSpeechFrames: Int = 3,
|
|
silenceEndFrames: Int = 8
|
|
) {
|
|
private val jni = SttJni()
|
|
private val h = jni.nativeVadNew(sampleRate, frameSize, rmsThreshold,
|
|
minSpeechFrames, silenceEndFrames)
|
|
fun push(pcm: ShortArray): Int = jni.nativeVadPush(h, pcm)
|
|
fun reset() { jni.nativeVadReset(h) }
|
|
fun release() { jni.nativeVadFree(h) }
|
|
|
|
companion object {
|
|
const val SILENCE = 0
|
|
const val SPEECH = 1
|
|
const val END_OF_SPEECH = 2
|
|
}
|
|
}
|