feat: голосовой ввод на очках — TripleClick + PCM → телефон → текст на очки

Протокол: SttAudio (PCM s16 16k mono кусками), SttPhrase/SttDone/SttCancel
(host→очки), StopStt(cancel) (очки→host).

Очки: TripleClick — безусловный вызов (кино: пауза + затемнение + слушаем);
во время распознавания Click — завершить (весь текст в консоль, затемнение
уходит, видео на паузе), DoubleClick — отмена; VAD-тишина 30с — автоотмена.
Микрофон (AudioRecord 16k) → SttAudio; на эмуляторе — PCM из filesDir/samples
(куски 3200 + пауза 200мс). Оверлей текста в Binocular (полупрозрачный фон,
текст по центру, в оба глаза). RECORD_AUDIO в манифесте. DEBUG-ресивер
(am broadcast DEBUG_GESTURE type=triple_click|click|double_click).

Телефон: SttStreamer — стриминговый VAD (Silero fp32, окна 512) → фразы →
Whisper-small int8 (executor, не блокирует приём) → SttPhrase на очки;
StopStt → SttDone (полный текст) / SttCancel; watchdog 30с тишины.

Фиксы (найдены на эмуляторе): IllegalThreadStateException — двойной старт
watchdog-потока (thread{}.apply{start()}); decode входящих — try/catch с
логом (не роняет сессию).

Проверено на эмуляторах (5554 очки + 5556 телефон, forward 8080):
triple_click → фразы «Раз, два» и «3 4 5» на очках (оверлей) → click →
«ВЕСЬ ТЕКСТ: Раз, два 3 4 5»; 30с-тишина → «отменено». Тесты: lib-core,
phone, glasses — зелёные.
This commit is contained in:
Hermes Agent
2026-08-20 04:12:28 +03:00
parent 324c427c74
commit 1304c6d921
16 changed files with 818 additions and 9 deletions
@@ -0,0 +1,31 @@
package pw.binom.viewmate.phone.stt
import kotlin.test.Test
import kotlin.test.assertEquals
class S16ToFloatTest {
@Test
fun convertsSignedSamples() {
val pcm = byteArrayOf(0x00, 0x00, 0xff.toByte(), 0x7f, 0x00, 0x80.toByte())
val out = s16leToFloat(pcm)
assertEquals(3, out.size)
assertEquals(0f, out[0], 1e-6f) // 0
assertEquals(32767f / 32768f, out[1], 1e-6f) // 0x7fff
assertEquals(-32768f / 32768f, out[2], 1e-6f) // 0x8000
}
@Test
fun minMaxSamples() {
val pcm = byteArrayOf(0xff.toByte(), 0x7f, 0x00, 0x80.toByte())
val out = s16leToFloat(pcm)
assertEquals(2, out.size)
assertEquals(32767f / 32768f, out[0], 1e-6f)
assertEquals(-1f, out[1], 1e-6f)
}
@Test
fun emptyInput() {
assertEquals(0, s16leToFloat(ByteArray(0)).size)
}
}