음성 오버레이 생명주기 (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.overlay 및 voicewake.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. VoiceWakeRuntime을 VoiceWakeOverlayController를 직접 건드리는 대신 세션을 생성/업데이트/종료하도록 리팩터링합니다.
  3. VoicePushToTalk를 기존 세션을 채택하고 놓기 시 endCapture를 호출하도록 리팩터링합니다. 런타임 쿨다운을 적용합니다.
  4. VoiceWakeOverlayController를 퍼블리셔에 연결합니다. 런타임/PTT에서의 직접 호출을 제거합니다.
  5. 세션 채택, 쿨다운, 빈 텍스트 닫기에 대한 통합 테스트를 추가합니다.