macOS companion app

音声オーバーレイ

音声オーバーレイのライフサイクル(macOS)

対象読者:macOS アプリのコントリビューター。目標:ウェイクワードとプッシュトゥトークが重なったときでも、音声オーバーレイが予測可能に動作するようにする。

動作

  • ウェイクワードによってオーバーレイがすでに表示されている状態でユーザーがホットキーを押すと、ホットキーセッションはテキストをリセットせず、既存のテキストを引き継ぐ。ホットキーを押している間、オーバーレイは表示されたままになる。キーを離した時点で、前後の空白を除去したテキストがあれば送信し、なければ閉じる。
  • ウェイクワードのみの場合は、引き続き無音を検出すると自動送信する。プッシュトゥトークの場合は、キーを離すと即座に送信する。

実装

  • VoiceSessionCoordinatorapps/macos/Sources/OpenClaw/VoiceSessionCoordinator.swift)は、アクティブな音声セッションを一元管理する。これはアクターではなく、@MainActor @Observable シングルトンである。API:startSessionupdatePartialfinalizesendNowdismissupdateLevelsnapshot。各セッションは UUID トークンを保持し、古いトークンまたは一致しないトークンを使用した呼び出しは破棄される。
  • VoiceWakeOverlayControllerVoiceWakeOverlayController+Session.swift)はオーバーレイを描画し、ユーザー操作(requestSenddismiss)をセッショントークン経由でコーディネーターに転送する。セッション状態自体を管理することはない。
  • プッシュトゥトーク(VoicePushToTalk.begin())は、表示中のオーバーレイにあるテキストを(VoiceSessionCoordinator.shared.snapshot() 経由で)adoptedPrefix として引き継ぐ。そのため、ウェイクオーバーレイの表示中にホットキーを押してもテキストが維持され、新しい発話が追加される。キーを離すと、最終文字起こしを最大 1.5s 待ってから、現在のテキストを使用する。
  • dismiss の際、オーバーレイは VoiceSessionCoordinator.overlayDidDismiss を呼び出し、それによって VoiceWakeRuntime.refresh(state:) がトリガーされる。このため、X による手動での閉じる操作、空テキストによる終了、送信後の終了のいずれの場合も、ウェイクワードの待ち受けが再開される。
  • 統一された送信経路:前後の空白を除去したテキストが空なら閉じる。それ以外の場合は、sendNow が送信チャイムを一度再生し、VoiceWakeForwarder 経由で転送してから閉じる。

ログ

音声サブシステムは ai.openclaw であり、各コンポーネントはそれぞれのカテゴリにログを記録する。

カテゴリ コンポーネント
voicewake.coordinator VoiceSessionCoordinator
voicewake.overlay VoiceWakeOverlayController/VoiceWakeOverlay
voicewake.ptt プッシュトゥトークのホットキーと音声キャプチャ
voicewake.runtime ウェイクワードランタイム
voicewake.chime チャイムの再生
voicewake.sync グローバル設定の同期
voicewake.forward 文字起こしの転送
voicewake.meter マイクレベルモニター

デバッグチェックリスト

  • オーバーレイが消えずに残る問題を再現しながら、ログをストリーミングする。

    bash
    sudo log stream --predicate 'subsystem == "ai.openclaw" AND category CONTAINS "voicewake"' --level info --style compact
  • アクティブなセッショントークンが 1 つだけであることを確認する。古いコールバックはコーディネーターによって破棄される。

  • プッシュトゥトークのキーを離した際に、アクティブなトークンを指定して必ず end() が呼び出されることを確認する。テキストが空の場合は、チャイムの再生や送信を行わずに閉じることが期待される。

関連項目

Was this useful?
On this page

On this page