Files
view-mate/TASK-full-buffer-stt.md

5.4 KiB
Raw Permalink Blame History

TASK-full-buffer-stt.md — полный текст по клику: распознавание всего буфера (не VAD-сегментов)

Симптом (24.08, подтверждено дампами)

Пользователь: TripleClick → говорит → Click → «думаю...» → сразу возврат в «слушаю». Полный текст пуст (stt ВЕСЬ ТЕКСТ: пусто в логах телефона).

Диагноз (по дампам голоса пользователя)

  1. Микрофон RayNeo X2 пишет РВАНО: пачки 50–100мс с провалами до нуля (в дампе 213110, 16.7с — 66% тишины). Это железо/драйвер очков, не транспорт (очки→телефон совпадают 98%).
  2. Silero VAD (тот же silero_vad.onnx с телефона, прогнан на хосте) на усиленных дампах нарезает речь на 10+ сегментов по 0.3–0.4с (213110: 12 сегментов).
  3. Whisper (faster-whisper-base на хосте) на ПОЛНОМ дампе распознаёт «Ого! | А что ты делаешь?» (212441, 5.9с) и «...грека...» (213110) — НЕ пусто. На отдельных VAD-сегментах 0.3–0.4с whisper возвращает пусто/мусор.
  4. Вывод: кликовый режим не должен зависеть от VAD-сегментации. Начало/конец фразы задаёт пользователь кликами; полный текст = распознавание ВСЕГО накопленного PCM одним вызовом whisper. VAD остаётся только для промежуточных фраз на очки (не трогать).

Задача

app-phone SttStreamer.kt:

  1. Буфер всей сессии: в accept(pcm) накапливать принятые сэмплы (FloatArray) в поле sessionPcm (сбрасывается в resetLocked()). Не терять при VAD-обработке (VAD-путь остаётся как есть, параллельно).
  2. finish(): распознать полный буфер одним вызовом whisper:
    • sessionPcm целиком, если ≤ 30с (16000*30 сэмплов);
    • если длиннее — последние 30с (хвост: sessionPcm.takeLast(16000*30)), чтобы команда пользователя (сказанная перед кликом) точно попала;
    • результат: stt.recognize(chunk) — один вызов, текст в full (перезаписать, не конкатенировать с VAD-фразами).
    • finish() по-прежнему ждёт lastFuture (VAD-распознавания в executor), затем выполняет полный прогон.
  3. resetLocked(): очищать sessionPcm.
  4. Логи: при распознавании полного буфера log("stt", "полный буфер: ${N} сэмплов (${dur}с) → '${text}'").

НЕ трогать: охраняемые 4 файла (HostToGlasses.kt, GlassesServer.kt, HostConnection.kt, MainActivity.kt), протокол lib-core, app-glasses, WhisperStt, StreamingVad/SherpaStreamingVad, VAD-путь и onPhrase (промежуточные фразы).

Точки правки

  • app-phone/src/main/kotlin/pw/binom/viewmate/phone/stt/SttStreamer.kt — буфер + finish() + resetLocked().
  • Тесты: новый app-phone/src/test/kotlin/pw/binom/viewmate/phone/stt/FullBufferSttTest.kt (JVM, фейковый PhraseRecognizer, FakeVad как в существующих тестах — см. stopSttFullTextSendsSttDone фикстуру).

Тесты (JVM)

  1. accept 3 чанка по 3200Б (9600Б = 0.3с) → finish() → фейковый recognizer получил ОДИН вызов с полным буфером (9600 байт → 4800 сэмплов), full = текст фейка.
  2. Длинный буфер > 30с: accept 40с → finish() → recognizer получил ровно 16000*30 сэмплов (хвост), текст не пуст.
  3. reset() → finish() → recognizer НЕ вызывался, full пуст.
  4. VAD-путь не сломан: FakeVad с одним сегментом → onPhrase по-прежнему вызывается (промежуточная фраза бродкастится), но full после finish() = результат полного буфера.

Проверка

  1. ./gradlew :app-phone:testDebugUnitTest — зелёные (было 158 + новые).
  2. Полная: ./gradlew :app-phone:assembleDebug :app-glasses:assembleDebug :lib-core:jvmTest.
  3. Коммит: stt(phone): полный текст по клику — whisper на всём буфере, VAD только для промежуточных фраз. Push НЕ делать.

Приёмка на железе (Hermes + пользователь)

  • Установить оба APK, force-stop, am start.
  • Пользователь: TripleClick → фраза («поставь спокойную музыку») → Click.
  • Ожидание: stt ВЕСЬ ТЕКСТ: поставь спокойную музыку (не пусто) → LLM → ответ на очки.