Files
view-mate/TASK-cancel-listen.md

8.8 KiB
Raw Permalink Blame History

TASK-cancel-listen: отмена слушания двойным тапом

Симптом (с железа, 24.08)

Пользователь в чате: сказал фразу → ассистент ответил. Далее один тап (чтобы снова начать слушать) → очки начали слушать (микрофон открыт), пользователь молчит. Теперь хочет отменить слушание — по замыслу это двойной тап — но отмена НЕ происходит: либо ничего не меняется, либо микрофон снова открывается, кружок записи гаснет/возвращается непонятно. Состояние «слушаю» залипает.

Причина (расследование — opencode)

Ключевой дефект: на стороне очков ветка SttCancel реагирует на reason некорректно.

  • «Пустой клик» (тап без голоса) и «двойной клик-отмена» на телефоне приходят на очки ОДИНАКОВО (SttCancel с reason="user"), и очки на любой делают resumeSttListening() → микрофон снова открыт, из режима не выходим.
  • После закрытия микрофона (sendPhrase()/cancelDictation(), MainActivity.kt ~279-321) флаг listening остаётся true, и в AssistantGestures.kt (~34-39) listening побеждает все остальные ветки → никакого двойного тапа в ANSWER/THINKING не видно, «выход из режима» недоступен.

Целевое поведение (договорно)

  1. Один тап (в режиме общения, не слушаю) → начать слушать (микрофон открыт, listening=true, красный кружок). Без изменений.
  2. Пустой клик без голоса (тап, но человек ничего не сказал) → телефон шлёт StopStt(cancel=false) с пустым текстом → очки возвращаются в «слушаю» (как сейчас, resumeSttListening) — это желаемое поведение, НЕ трогать.
  3. Двойной тап в состоянии «слушаю» → отмена диктовки: микрофон закрыт, listening=false, thinking=false, остаёмся в режиме общения (показываем последний ответ), НЕ выходим из режима целиком. И НЕ открываем микрофон заново. Это новое поведение — сейчас оно не работает.
  4. Таймаут тишины 30с (reason="timeout") → выход ИЗ режима общения (exitAssistantMode), как сейчас. НЕ трогать.
  5. После любой посылки/отмены микрофон должен быть закрыт, listening сбрасывать на false.

Механика правки (семантика протокола)

SttCancel.reason — свободная строка, значения:

  • "user" — пустой клик без голоса → очки возвращаются в «слушаю» (resume). (без изменений)
  • "timeout" — тишина 30с → очки ВЫХОДЯТ из режима (exitAssistantMode). (без изменений)
  • "cancel" — НОВОЕ: явная отмена диктовки двойным тапом → очки закрывают микрофон, listening=false, thinking=false, остаются в режиме (показывают последний ответ), НЕ открывают микрофон заново.

Точки правки (подтверди точные строки/логику при чтении)

  1. lib-core .../core/protocol/Messages.kt — SttCancel уже несёт reason: String = "user". Ввести константу-компаньон SttCancel.REASON_CANCEL = "cancel" (и REASON_USER/REASON_TIMEOUT для читабельности). Протокол меняется на ОБЕИХ сторонах — после правки переустанавливать И app-glasses, И app-phone (рассинхрон версий вернёт старый баг).
  2. app-phone .../phone/GlassesServer.kt (~206) — обработчик StopStt:
    • StopStt(cancel=true) → бродкастить SttCancel(reason=REASON_CANCEL) (ранее шло "user").
    • StopStt(cancel=false) с пустым full → SttCancel(reason=REASON_USER) (возврат в «слушаю»).
    • StopStt(cancel=false) с непустым текстом → SttDone(...) (без изменений).
  3. app-glasses .../glasses/.../HostConnection.kt (~362-377) — ветка SttCancel:
    • reason == REASON_CANCEL → закрыть микрофон + listening=false + thinking=false, НЕ resumeSttListening(), НЕ выход из режима — остаться в режиме со следующим ответом.
    • reason == REASON_USER → как сейчас (thinking=false + resumeSttListening()).
    • reason == REASON_TIMEOUT → exitAssistantMode() (как сейчас).
  4. app-glasses .../glasses/MainActivity.kt (~279-321) — sendPhrase() и cancelDictation(): после закрытия микрофона обязательно listening=false. Ввести/использовать cancelDictation() как точку «отмена слушания» (закрывает микрофон, сбрасывает listening).
  5. app-glasses .../glasses/gestures/AssistantGestures.kt (~34-39, ~103-107) — проверить, что двойной тап в ветке LISTENING маршрутизируется в cancelDictation() (которое шлёт StopStt(cancel=true) → на телефоне StopStt(cancel=true) → SttCancel(REASON_CANCEL)). Убедиться, что приоритет listening не перебивает двойной тап и не «залипает».

Подтвердить при чтении (не догадываться)

  • Реально ли GestureDetectorWithSound при двойном тапе НЕ дёргает промежуточный single-Click (гонка таймингов). Если дёргает — двойной тап превращается в single+single и отмена не срабатывает. При необходимости поправить дискриминацию single/double.
  • Точные имена полей/функций в Messages.kt и номера строк в HostConnection.kt/GlassesServer.kt — сверить с реальным кодом, не брать на веру из описания выше.

Ограничения

  • НЕ менять поведение reason="user" (пустой клик → возврат в «слушаю») и reason="timeout" (выход из режима) — это зафиксированные решения от 23.08.
  • Рабочая копия: /root/WORK/view-mate. Не трогать абсолютные пути вне workspace.
  • Картинки/red-индикатор (504e66f) и историю диалога (706cc10) не ломать.
  • Язык кода — только Kotlin.

Тесты

  • JVM-юнит (lib-core + app-glasses gestures): новый SttCancel(reason="cancel"):
    • очки НЕ вызывают resumeSttListening(), НЕ делают exitAssistantMode(), listening=false.
    • reason="user" → resumeSttListening() (регресс).
    • reason="timeout" → exitAssistantMode() (регресс).
  • app-phone test (GlassesServerTest): StopStt(cancel=true) → бродкастится SttCancel("cancel"); StopStt(cancel=false) пустой → SttCancel("user"); непустой → SttDone.
  • Гейт: ./gradlew :lib-core:jvmTest :app-glasses:testDebugUnitTest :app-phone:testDebugUnitTest.
  • Полная сборка: ./gradlew :app-phone:assembleDebug :app-glasses:assembleDebug :lib-core:jvmTest.

Примечание по приёмке на железе

DEBUG-крюк: adb -s 7687f645 shell 'am broadcast -a pw.binom.viewmate.glasses.DEBUG_GESTURE --es type double_click' проверяет, как машина жествов реагирует на двойной тап в состоянии «слушаю». Проверка на очках после установки — за Hermes-ом (не агента).