Перейти к основному содержанию

Жизненный цикл голосового оверлея (macOS)

Аудитория: участники разработки приложения для macOS. Цель: сделать поведение голосового оверлея предсказуемым, когда пересекаются ключевое слово активации и push-to-talk.

Текущий замысел

  • Если оверлей уже виден из‑за ключевого слова активации и пользователь нажимает горячую клавишу, сеанс по горячей клавише подхватывает существующий текст вместо его сброса. Оверлей остаётся на экране, пока удерживается горячая клавиша. Когда пользователь отпускает её: отправить, если есть обрезанный текст, иначе закрыть.
  • Ключевое слово активации само по себе по‑прежнему автоматически отправляет по тишине; push-to-talk отправляет сразу при отпускании.

Реализовано (9 декабря 2025)

  • Сеансы оверлея теперь несут токен на каждый захват (ключевое слово активации или push-to-talk). Обновления partial/final/send/dismiss/level отбрасываются, если токен не совпадает, что предотвращает устаревшие колбэки.
  • Push-to-talk подхватывает любой видимый текст оверлея как префикс (поэтому нажатие горячей клавиши, когда оверлей от ключевого слова уже открыт, сохраняет текст и добавляет новую речь). Он ждёт до 1,5 с финальной транскрипции, прежде чем откатиться к текущему тексту.
  • Логирование звукового сигнала/оверлея выводится под info в категориях voicewake.overlay, voicewake.ptt и voicewake.chime (начало сеанса, partial, final, send, dismiss, причина звукового сигнала).

Следующие шаги

  1. VoiceSessionCoordinator (actor)
    • Владеет ровно одним VoiceSession в каждый момент времени.
    • API (на основе токенов): beginWakeCapture, beginPushToTalk, updatePartial, endCapture, cancel, applyCooldown.
    • Отбрасывает колбэки со старыми токенами (предотвращает повторное открытие оверлея старыми распознавателями).
  2. VoiceSession (model)
    • Поля: token, source (wakeWord|pushToTalk), зафиксированный/временный текст, флаги звуковых сигналов, таймеры (автоотправка, простой), overlayMode (display|editing|sending), дедлайн кулдауна.
  3. Привязка оверлея
    • VoiceSessionPublisher (ObservableObject) зеркалирует активный сеанс в SwiftUI.
    • VoiceWakeOverlayView рендерит только через publisher; он никогда напрямую не мутирует глобальные синглтоны.
    • Действия пользователя в оверлее (sendNow, dismiss, edit) вызывают координатор обратно с токеном сеанса.
  4. Единый путь отправки
    • При endCapture: если обрезанный текст пуст → закрыть; иначе performSend(session:) (проигрывает звуковой сигнал отправки один раз, пересылает, закрывает).
    • Push-to-talk: без задержки; ключевое слово активации: опциональная задержка для автоотправки.
    • Применить короткий кулдаун к рантайму ключевого слова активации после завершения push-to-talk, чтобы ключевое слово не срабатывало сразу же повторно.
  5. Логирование
    • Координатор выводит логи .info в подсистеме bot.molt, категориях voicewake.overlay и voicewake.chime.
    • Ключевые события: session_started, adopted_by_push_to_talk, partial, finalized, send, dismiss, cancel, cooldown.

Контрольный список отладки

  • Стримьте логи при воспроизведении «залипшего» оверлея:
    sudo log stream --predicate 'subsystem == "bot.molt" AND category CONTAINS "voicewake"' --level info --style compact
    
  • Убедитесь, что активен только один токен сеанса; устаревшие колбэки должны отбрасываться координатором.
  • Убедитесь, что отпускание push-to-talk всегда вызывает endCapture с активным токеном; если текст пуст, ожидайте dismiss без звукового сигнала и отправки.

Шаги миграции (рекомендуется)

  1. Добавить VoiceSessionCoordinator, VoiceSession и VoiceSessionPublisher.
  2. Рефакторить VoiceWakeRuntime, чтобы создавать/обновлять/завершать сеансы вместо прямого обращения к VoiceWakeOverlayController.
  3. Рефакторить VoicePushToTalk, чтобы подхватывать существующие сеансы и вызывать endCapture при отпускании; применить кулдаун рантайма.
  4. Подключить VoiceWakeOverlayController к publisher; убрать прямые вызовы из рантайма/PTT.
  5. Добавить интеграционные тесты для подхвата сеанса, кулдауна и закрытия при пустом тексте.