5.7 KiB
TASK-stt-cancel-reason.md — очки не должны слушать в фоне (цикл SttCancel)
Контекст (отчёт пользователя 23.08 ~21:10)
«Смотрю видео на фоне, очки рядом лежат — походу он всё это время слушает». По логам очков:
бесконечный цикл каждые ~31с: стt отменено — возврат в «слушаю» → микрофон запущен →
30с тишины → телефон шлёт SttCancel → очки снова открывают микрофон. При этом очки в лаунчере
(Activity не в фокусе), процесс жив. Очки даже распознают фоновый звук ТВ. Система начала
блокировать запись (AppOps RECORD_AUDIO Operation not started).
Корень: SttCancel приходит от ДВУХ разных источников, а очки обрабатывают их одинаково:
- Пустой клик пользователя (
GlassesServer, StopStt(cancel=false) с пустым full) — по касяку №2 очки должны вернуться в «слушаю» (resumeSttListening). - Watchdog тишины 30с (
PhoneApp.ensureSttonSilence30s) — пользователь молчит/ушёл, нужно ВЫЙТИ из режима общения (микрофон закрыть, оверлей скрыть), как было в старой логике SttCancel (listening=false, thinking=false). Сейчас оба шлют голыйSttCancel(), и очки на любой из них вызывают resumeSttListening → цикл.
Правки
1. lib-core — протокол: SttCancel получает reason
lib-core/src/commonMain/kotlin/pw/binom/viewmate/core/protocol/HostToGlasses.kt:
@SerialName("stt_cancel")
class SttCancel(val reason: String = "user") : HostToGlasses
reason="user"— отмена/пустой клик пользователя: очки возвращаются в «слушаю»;reason="timeout"— авто-отмена по тишине 30с: очки ВЫХОДЯТ из режима (микрофон закрыть, оверлей скрыть, как exitAssistantMode). Backward-compatible: старое сообщение без поля десериализуется как reason="user". Поправить тест round-trip SttCancel вlib-core/src/commonTest/.../HostToGlassesTest.kt.
2. Телефон — источники
app-phone/.../PhoneApp.ktonSilence30s (~строка 376):broadcast(SttCancel())→broadcast(SttCancel(reason = "timeout")).app-phone/.../GlassesServer.ktStopStt(cancel=false) с пустым full:broadcast(SttCancel())→broadcast(SttCancel(reason = "user"))(явно; можно и дефолт, но явно читаемее).- StopStt(cancel=true) (двойной клик, отмена распознавания): тоже
SttCancel(reason = "user")— пользователь остаётся в разговоре, очки возвращаются в «слушаю».
3. Очки — обработка по reason
app-glasses/.../HostConnection.kt ветка is SttCancel:
reason == "timeout"→ выход из режима:GlassesApp.instance.listening.value = false,thinking.value = false, и остановить микрофон. Для этого вMainActivityсделать публичный метод (напримерfun onSttTimeout()), который вызывает логику exitAssistantMode() (стоп микрофона + сброс состояний) — или сделать exitAssistantMode() публичной и вызыватьGlassesApp.instance.mainActivity?.exitAssistantMode(). Выбрать чистое решение по коду.- иначе (user) → как сейчас:
GlassesApp.instance.thinking.value = false+mainActivity?.resumeSttListening().
4. Очки — защита «лежат и слушают» (onPause)
app-glasses/.../MainActivity.kt onPause(): сейчас там только пауза кино + sendPlaybackPositionNow.
Добавить: если режим общения активен (listening || thinking || assistantText.isNotBlank()) —
выход из режима общения (та же логика exitAssistantMode: sttMic.stop(), сброс состояний).
Т.е. при потере фокуса Activity (пользователь ушёл в лаунчер) микрофон гарантированно
закрывается, цикл невозможен.
5. Сборка, тесты, коммит
./gradlew :lib-core:build :app-glasses:assembleDebug :app-phone:assembleDebug— BUILD SUCCESSFUL.git add -A && git commit -m 'Очки: разграничение SttCancel (user/timeout) — выход из режима при тишине 30с, микрофон закрывается при потере фокуса'(БЕЗ push).
КРИТИЧЕСКИЕ ПРАВИЛА
- workspace = /root/WORK/view-mate. Пути ТОЛЬКО относительные от корня (app-glasses/..., app-phone/..., lib-core/...). НИКОГДА /root/WORK/app-* (без view-mate) — external_directory, permission-guard отклонит.
- НЕ трогать: LocalLlm.kt (GPU), скорость, клавиатуру, автоименование, AssistantGestures.kt, матрицу жестов, AssistantOverlay.kt (скролл уже починен).
- В конце — краткий отчёт: файлы, изменения, сборка, тесты.