음성 오버레이 생명주기 (macOS)

대상: macOS 앱 기여자. 목표: 웨이크 워드와 푸시 투 토크가 겹칠 때 음성 오버레이를 예측 가능하게 유지.

현재 의도

  • 웨이크 워드로 오버레이가 이미 표시 중일 때 사용자가 핫키를 누르면, 핫키 세션이 기존 텍스트를 _채택_하고 초기화하지 않습니다. 핫키가 눌린 동안 오버레이가 유지됩니다. 사용자가 놓으면: 정리된 텍스트가 있으면 전송, 없으면 닫기.
  • 웨이크 워드만 사용하면 여전히 무음 시 자동 전송; 푸시 투 토크는 놓는 즉시 전송.

구현됨 (2025년 12월 9일)

  • 오버레이 세션은 캡처(웨이크 워드 또는 푸시 투 토크)마다 토큰을 가집니다. 부분/최종/전송/닫기/레벨 업데이트는 토큰이 일치하지 않으면 삭제되어, 오래된 콜백을 방지합니다.
  • 푸시 투 토크는 보이는 오버레이 텍스트를 접두사로 채택합니다 (웨이크 오버레이가 표시된 상태에서 핫키를 누르면 텍스트를 유지하고 새 음성을 추가). 현재 텍스트로 대체하기 전에 최종 트랜스크립트를 최대 1.5초 대기합니다.
  • 치임/오버레이 로깅이 카테고리 voicewake.overlay, voicewake.ptt, voicewake.chime에서 info 레벨로 출력됩니다 (세션 시작, 부분, 최종, 전송, 닫기, 치임 이유).

다음 단계

  1. VoiceSessionCoordinator (actor)
    • 한 번에 정확히 하나의 VoiceSession을 소유합니다.
    • API (토큰 기반): beginWakeCapture, beginPushToTalk, updatePartial, endCapture, cancel, applyCooldown.
    • 오래된 토큰을 가진 콜백을 삭제합니다 (이전 인식기가 오버레이를 다시 여는 것 방지).
  2. VoiceSession (model)
    • 필드: token, source (wakeWord|pushToTalk), committed/volatile 텍스트, 치임 플래그, 타이머 (자동 전송, 유휴), overlayMode (display|editing|sending), 쿨다운 기한.
  3. 오버레이 바인딩
    • VoiceSessionPublisher (ObservableObject)가 활성 세션을 SwiftUI로 미러링합니다.
    • VoiceWakeOverlayView는 퍼블리셔를 통해서만 렌더링합니다. 전역 싱글톤을 직접 변경하지 않습니다.
    • 오버레이 사용자 액션 (sendNow, dismiss, edit)이 세션 토큰과 함께 코디네이터에 콜백합니다.
  4. 통합 전송 경로
    • endCapture 시: 정리된 텍스트가 비어 있으면 → 닫기; 아니면 performSend(session:) (전송 치임 한 번, 전달, 닫기).
    • 푸시 투 토크: 지연 없음; 웨이크 워드: 자동 전송을 위한 선택적 지연.
    • 푸시 투 토크 완료 후 웨이크 런타임에 짧은 쿨다운을 적용하여, 웨이크 워드가 즉시 재트리거되지 않게 합니다.
  5. 로깅
    • 코디네이터가 서브시스템 ai.openclaw, 카테고리 voicewake.overlayvoicewake.chime에서 .info 로그를 출력합니다.
    • 주요 이벤트: session_started, adopted_by_push_to_talk, partial, finalized, send, dismiss, cancel, cooldown.

디버깅 체크리스트

  • 고정 오버레이 재현 시 로그를 스트리밍합니다:

    sudo log stream --predicate 'subsystem == "ai.openclaw" AND category CONTAINS "voicewake"' --level info --style compact
  • 활성 세션 토큰이 하나뿐인지 확인합니다. 오래된 콜백은 코디네이터에 의해 삭제되어야 합니다.

  • 푸시 투 토크 놓기가 항상 활성 토큰으로 endCapture를 호출하는지 확인합니다. 텍스트가 비어 있으면 치임이나 전송 없이 dismiss가 예상됩니다.

마이그레이션 단계 (제안)

  1. VoiceSessionCoordinator, VoiceSession, VoiceSessionPublisher를 추가합니다.
  2. VoiceWakeRuntimeVoiceWakeOverlayController를 직접 건드리는 대신 세션을 생성/업데이트/종료하도록 리팩터링합니다.
  3. VoicePushToTalk를 기존 세션을 채택하고 놓기 시 endCapture를 호출하도록 리팩터링합니다. 런타임 쿨다운을 적용합니다.
  4. VoiceWakeOverlayController를 퍼블리셔에 연결합니다. 런타임/PTT에서의 직접 호출을 제거합니다.
  5. 세션 채택, 쿨다운, 빈 텍스트 닫기에 대한 통합 테스트를 추가합니다.