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

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

Текущее намерение

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

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

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

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

  1. VoiceSessionCoordinator (actor)
    • Владеет ровно одним VoiceSession за раз.
    • API (на основе токенов): beginWakeCapture, beginPushToTalk, updatePartial, endCapture, cancel, applyCooldown.
    • Отбрасывает обратные вызовы, которые несут устаревшие токены (предотвращает повторное открытие оверлея старыми распознавателями).
  2. VoiceSession (модель)
    • Поля: token, source (wakeWord|pushToTalk), зафиксированный/волатильный текст, флаги chime, таймеры (auto-send, idle), overlayMode (display|editing|sending), крайний срок cooldown.
  3. Привязка оверлея
    • VoiceSessionPublisher (ObservableObject) зеркалирует активный сеанс в SwiftUI.
    • VoiceWakeOverlayView рендерит только через издателя; он никогда не изменяет глобальные синглтоны напрямую.
    • Действия пользователя оверлея (sendNow, dismiss, edit) обращаются обратно к координатору с токеном сеанса.
  4. Унифицированный путь отправки
    • При endCapture: если обрезанный текст пуст → закрыть; иначе performSend(session:) (воспроизводит chime отправки один раз, пересылает, закрывает).
    • Push-to-talk: без задержки; wake-word: опциональная задержка для auto-send.
    • Применить короткий cooldown к wake runtime после завершения push-to-talk, чтобы wake-word не сработал немедленно снова.
  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 без chime или отправки.

Шаги миграции (предложенные)

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