stt: VAD-интерфейс (Sherpa боевая / Silero тестовая), PhraseRecognizer (Whisper), чистый full-text-тест на реальном Silero VAD

This commit is contained in:
2026-08-24 05:43:29 +03:00
parent 7557d7229f
commit c1c169036a
13 changed files with 556 additions and 38 deletions
@@ -23,12 +23,19 @@ import pw.binom.viewmate.core.protocol.Hello
import pw.binom.viewmate.core.protocol.HostToGlasses
import pw.binom.viewmate.core.protocol.SetMode
import pw.binom.viewmate.core.protocol.StopStt
import pw.binom.viewmate.core.protocol.SttAudio
import pw.binom.viewmate.core.protocol.SttCancel
import pw.binom.viewmate.core.protocol.SttDone
import pw.binom.viewmate.core.protocol.Welcome
import pw.binom.viewmate.core.protocol.protocolJson
import pw.binom.viewmate.phone.stt.FakePhraseRecognizer
import pw.binom.viewmate.phone.stt.SileroStreamingVad
import pw.binom.viewmate.phone.stt.SttStreamer
import pw.binom.viewmate.phone.stt.WavPcm
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertTrue
import kotlin.test.fail
/**
@@ -194,10 +201,38 @@ class GlassesServerTest {
}
}
// Полная фраза (StopStt(cancel=false) с текстом → SttDone + onStopFullText) в JVM-юните
// не проверяется: реальный SttStreamer держит нативный VAD (com.k2fsa.sherpa.onnx.Vad),
// а нативная библиотека sherpa-onnx есть ТОЛЬКО под Android-bionic — в JVM-юните её нет,
// и фейк падает UnsatisfiedLinkError (неремонтируемо). Сценарий покрывается на устройстве.
/**
* Полная фраза (StopStt(cancel=false) с текстом → SttDone): реальный Silero VAD
* (SileroStreamingVad, JVM/ONNX) сегментирует фикстуру, фейк-распознаватель
* даёт «привет» на каждый сегмент → очки получают SttDone с непустым текстом.
*/
@Test
fun stopSttFullTextSendsSttDone() = runServer { port, hub ->
hub.sttFactory = {
SttStreamer(
stt = FakePhraseRecognizer(),
vad = SileroStreamingVad(),
onPhrase = { _, _ -> },
onSilence30s = { },
)
}
val pcm = WavPcm.loadPcm(WavPcm.fixture().absolutePath)
val client = wsClient()
client.webSocket("ws://127.0.0.1:$port$GLASSES_WS_PATH") {
// кидать по 3200 байт (100 мс @ 16 кГц s16le mono)
var off = 0
while (off < pcm.size) {
val end = minOf(off + 3200, pcm.size)
send(json.encodeToString(GlassesToHost.serializer(), SttAudio(pcm.copyOfRange(off, end))))
off = end
}
send(json.encodeToString(GlassesToHost.serializer(), StopStt(cancel = false)))
val frame = incoming.receive() as Frame.Text
val msg = json.decodeFromString(HostToGlasses.serializer(), frame.readText())
val done = assertIs<SttDone>(msg)
assertTrue(done.full.isNotBlank())
}
}
// ---------- помощники ----------