Files
view-mate/TASK-mic-gain.md

7.6 KiB
Raw Permalink Blame History

TASK-mic-gain.md — усиление микрофона очков (фикс «пустой фразы»)

Симптом (пользователь, 24.08 вечер)

Очки: TripleClick → «слушаю» → пользователь говорит → Click («отправить фразу») → очки показывают «думаю...» и СРАЗУ возвращаются в «слушаю». Ответа нет. Впечатление: «процесс говорения сломан».

Диагноз (подтверждён дампами и логами 24.08)

Цепь: очки SttMicStream (AudioRecord MIC 16 кГц s16 mono) → BT SPP → телефон SttStreamer (Silero VAD → sherpa whisper-small) → StopStt(cancel=false) → GlassesServer → full.isBlank() → SttCancel(REASON_USER) → очки возвращаются в «слушаю».

Факты:

  1. Жесты/протокол/цикл здоровы — симуляция DEBUG_GESTURE (triple_click → click) воспроизводит симптом, очки логируют Click → отправить фразу, stt отменено — возврат в «слушаю» (SttCancel). Это штатное поведение для ПУСТОЙ фразы.
  2. Микрофон RayNeo X2 пишет сигнал пиком 1–9% шкалы (пики 181–2966 из 32768; RMS 9–184). Дампы stt_dump_1930*.wav (19:30, голос пользователя): после нормализации faster-whisper-base распознаёт «тихо тихо», «спокойная музыка» — РЕЧЬ есть, но амплитуда ничтожна.
  3. Транспорт очки→телефон чист (analyze.py: порядок окон 98%, лаг 0, объём совпадает) — телефон получает тот же тихий PCM.
  4. STT-пайплайн здоров — на эмуляторе (WAV нормального уровня) распознавание работает; на амплитудах ~0.005–0.09 Silero VAD/whisper-small не видят речи → full пуст → пустой клик → возврат в «слушаю».
  5. Вчерашний фикс кадров/байтов (c940a7e+fc84ba1+40edd88) правильный и НЕ трогается — он убрал «через один», но уровень микрофона не поднял.

Корень: на пути аудио очки→телефон нет усиления. Микрофон тихий, VAD/Whisper не видят речь.

Задача

Добавить фиксированное цифровое усиление в app-glasses SttMicStream.kt, в живой ветке микрофона: чанк после micChunkBytes(buf, frames) усилить в MIC_GAIN раз (с clamp), и уже усиленный чанк отправлять onChunk(...) и писать в дамп.

  • Точка правки: app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/stt/SttMicStream.kt
  • Тесты: app-glasses/src/test/kotlin/pw/binom/viewmate/glasses/stt/MicChunkTest.kt (существующий файл, добавить тесты)
  • НЕ трогать: HostToGlasses.kt, GlassesServer.kt, HostConnection.kt, MainActivity.kt (охраняемые 4 файла фикса 7557d72), телефонную сторону (app-phone), протокол (lib-core), WavWriter.kt, существующую функцию micChunkBytes и её тесты.
  • SttMicStream.kt писать можно (не в охране).

Реализация

В SttMicStream.kt, рядом с micChunkBytes:

/** Коэффициент усиления микрофона (микрофон RayNeo X2 пишет пик 1–9% шкалы). */
internal const val MIC_GAIN = 10f

/**
 * Цифровое усиление чанка s16le mono с насыщением.
 * Каждый сэмпл: v = (s * gain).roundToInt().coerceIn(-32768, 32767).
 * Размер чанка не меняется (3200 байт = 1600 сэмплов).
 */
internal fun gainChunk(bytes: ByteArray, gain: Float = MIC_GAIN): ByteArray

Разбор s16le: младший байт + (старший << 8), знак как в s16leToFloat (app-phone) — if (s >= 0x8000) s -= 0x10000.

В цикле записи (живая ветка, после фикса 40edd88):

val frames = recorder.read(buf, 0, CHUNK_BYTES / 2)
if (frames > 0) {
    val raw = micChunkBytes(buf, frames)
    val chunk = gainChunk(raw)          // ← усиление здесь
    if (dumpWriter != null) runCatching { dumpWriter.write(chunk) }.onFailure { ... }
    onChunk(chunk)
}

ВАЖНО: дамп (WavWriter) пишет УСИЛЕННЫЙ чанк (то же, что уходит на телефон) — чтобы дампы очков отражали то, что реально получает STT. (Решение принято: диагностика по дампам очков = тому, что видит телефон; сырой микрофон больше не пишем.)

Обоснование gain = 10: лучший утренний пик речи 2966 (9.05%) → 29660 (90.5%); тихая фраза 483 (1.5%) → 4830 (14.7%) — уже видно VAD. При крике возможен клиппинг — приемлемо (лучше клиппинг, чем пустота). Если на железе VAD захочет ложно срабатывать — константу подстроим.

Тесты (добавить в MicChunkTest.kt)

  1. gainChunk тихого чанка (сэмплы ±1000): пик результата ≥ 9000, размер = 3200 байт.
  2. gainChunk громкого чанка (сэмплы ±30000): ни один сэмпл не выходит за ±32767 (clamp работает).
  3. gainChunk нулевого чанка: байты без изменений.
  4. gainChunk с gain = 1f: байты побайтово идентичны входу (для gain=1 не аллоцировать заново — вернуть bytes).
  5. Сохранение порядка: gainChunk не перемешивает сэмплы (знак/порядок сохраняется — проверка первых 2 сэмплов).

Проверка

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

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

  • Установить оба APK (очки + телефон) с этого коммита, force-stop, am start.
  • Маркеры дампов stt_dump.marker уже лежат на обоих устройствах — дампы пишутся.
  • Пользователь: TripleClick → говорит «поставь спокойную музыку» → Click.
  • Hermes снимает: дампы очков (пик речи должен стать ~50–90%), stt_dump_phone.raw (телефон получил усиленный PCM), logcat: SttDone с непустым текстом → LLM-запрос → ответ на очки.