feat(app-phone): офлайн-распознавание речи — sherpa-onnx + Whisper-small
STT-модуль для отладки (без UI): WhisperStt (offline whisper int8, CPU), VadSegmenter (Silero VAD, фразы), WavReader (16k mono s16), SttDebug (распознавание wav-файла по фразам с печатью в лог). Вызов: am start --ez stt_debug true (первый wav из filesDir/samples). Модели — в filesDir/models (вне git): small-encoder/decoder.int8.onnx, small-tokens.txt, silero_vad.onnx (fp32!). Грабли, найденные на эмуляторе (Android 15): - Silero VAD int8 (643КБ) молчит в Android-рантайме — нужна fp32 (2.2МБ) - WavReader: littleShort возвращал беззнаковое 0..65535 — сэмплы с DC-offset, whisper давал мусор; теперь знаковый -32768..32767 - VAD требует подачу окнами ровно window_size (целиком — переполнение) - Модели кладутся через adb push /data/local/tmp + run-as cp (внешний каталог приложения на Android 15 не видит adb-файлы) Проверено на эмуляторе (CPU): «раз два три четыре пять» → «Раз, два 3 4 5», грека → почти идеально, masha/anton → 21 фраза, диалог распознан. README: таблица жестов обновлена (TripleClick — безусловный вызов).
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
package pw.binom.viewmate.phone
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import pw.binom.viewmate.phone.stt.SttDebug
|
||||
import pw.binom.viewmate.phone.ui.ViewMateApp
|
||||
import java.io.File
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -11,5 +17,46 @@ class MainActivity : ComponentActivity() {
|
||||
setContent {
|
||||
ViewMateApp()
|
||||
}
|
||||
if (intent?.getBooleanExtra("stt_debug", false) == true) {
|
||||
runSttDebug()
|
||||
}
|
||||
}
|
||||
|
||||
/** Отладочный прогон распознавания: берёт первый wav из filesDir/samples. */
|
||||
private fun runSttDebug() {
|
||||
val samplesDir = File(filesDir, "samples")
|
||||
val wav = samplesDir.listFiles()
|
||||
?.filter { it.isFile && it.extension.equals("wav", true) }
|
||||
?.firstOrNull()
|
||||
if (wav == null) {
|
||||
val msg = "[stt] нет wav-файлов в ${samplesDir.absolutePath}"
|
||||
log("stt", msg)
|
||||
showOverlay(msg)
|
||||
return
|
||||
}
|
||||
showOverlay("[stt] распознаю ${wav.name}...")
|
||||
Thread {
|
||||
val text = SttDebug.run(this, wav.absolutePath)
|
||||
runOnUiThread { showOverlay(text ?: "[stt] не удалось распознать") }
|
||||
}.start()
|
||||
}
|
||||
|
||||
/** TextView поверх экрана — чтобы видеть результат на эмуляторе. */
|
||||
private fun showOverlay(text: String) {
|
||||
val tv = TextView(this).apply {
|
||||
this.text = text
|
||||
setTextColor(Color.WHITE)
|
||||
setBackgroundColor(Color.argb(200, 0, 0, 0))
|
||||
textSize = 16f
|
||||
setPadding(24, 24, 24, 24)
|
||||
}
|
||||
addContentView(
|
||||
tv,
|
||||
FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
Gravity.TOP,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user