• #Stream Deck
  • #AutoHotkey
  • #Claude Code
  • #音声入力
  • #自動化
開発misc-devメモ

Stream Deck × AutoHotkeyでClaude Codeの音声入力をトグル化する

Claude Codeの音声入力はスペースキーを長押しする。押している間だけ認識が走り、指を離すと止まる。短いプロンプトなら問題ないが、長文を口述するとき、スペースキーを左手で押さえ続けるのが地味につらい。Stream Deckのボタンを1回押すだけでトグルできないか――そう考えて試行錯誤が始まった。


Stream Deckの「ホットキー」アクションでは解決しない

最初に思いつくのはStream Deck標準の「ホットキー」アクションだ。しかしこれは「ボタンを物理的に押している間だけキーを送る」仕組みで、指を離すとキーも離れる。Claude Codeの音声入力が求めるのは「長押しし続ける」状態の維持なので、標準機能ではトグルにできない。

ここでAutoHotkeyの出番になる。


最初の試み: Space downを1回送る → 失敗

AutoHotkeyで Send "{Space down}" を1回送れば、OSがキーを押しっぱなしと認識してくれるだろう――そう考えてスクリプトを書いた。

; 最初の案(動かない)
Send "{Space down}"

実行するとClaude Codeの音声入力アイコンが一瞬点灯して、すぐ消える。Space down を1回送っただけでは、物理キーのような連続的なキーリピートイベントが発生しない。OSは「一瞬押されて離された」と解釈してしまう。

ターミナルのカーソルが微動だにしない時点で、この方式は無理だと悟った。


解決策: ループでSpace downを送り続ける

物理キーの長押しを模倣するには、スクリプトが終了せずにループで Space down を送り続ける必要がある。

; toggle-voice.ahk の核心部分
#Requires AutoHotkey v2.0

stateFile := A_Temp "\voice-input-pane1.lock"

; 既に動いているインスタンスがあれば、状態ファイルを消して停止させる
if FileExist(stateFile) {
    FileDelete stateFile
    ExitApp
}

; 状態ファイルを作成して「録音中」を示す
FileAppend("", stateFile)

; 状態ファイルが存在する限り、Space downを送り続ける
while FileExist(stateFile) {
    Send "{Space down}"
    Sleep 50
}

; ループを抜けたらキーを離して終了
Send "{Space up}"
ExitApp

仕組みはシンプルだ。

  1. 1回目の呼び出し: 状態ファイル(.lock)を作成し、50msごとに Space down を送るループに入る
  2. 2回目の呼び出し: 別のインスタンスが起動し、状態ファイルを削除して即終了する
  3. 1回目のインスタンス: ループの FileExist チェックで .lock が消えたことを検知し、Space up を送って終了する

Stream Deckのボタンには、このAHKスクリプトを呼ぶbatファイルを割り当てる。既存のStream Deck → bat → AHKというパターンをそのまま踏襲した。

@echo off
start "" "C:\tools\AutoHotkey\v2\AutoHotkey64.exe" "C:\tools\scripts\toggle-voice.ahk" pane1

ボタンを押す。Claude Codeの音声入力アイコンが点灯し、マイクが拾い始める。もう一度押す。アイコンが消え、入力が確定される。手がキーボードから離れた状態で音声入力のON/OFFを切り替えられるようになった。


ペイン間の自動切り替え: 4分割画面での運用

4分割画面で複数のClaude Codeインスタンスを動かしている場合、ペインごとにStream Deckのボタンを割り当てたい。ペイン1で音声入力中にペイン2のボタンを押したら、ペイン1が止まってペイン2が始まる――という挙動が理想だ。

状態ファイルをペインごとに分ける

状態ファイルの名前にペイン番号を含める。

voice-input-pane1.lock
voice-input-pane2.lock
voice-input-pane3.lock
voice-input-pane4.lock

他ペインの状態ファイルを先に消す

あるペインのボタンが押されたとき、まず他の全ペインの .lock ファイルを削除する。これで他ペインで走っているインスタンスが次のループチェックで自動停止する。

; 自分以外のペインの状態ファイルを全て消す
panes := ["pane1", "pane2", "pane3", "pane4"]
for _, p in panes {
    if (p != myPane) {
        otherFile := A_Temp "\voice-input-" p ".lock"
        if FileExist(otherFile)
            FileDelete otherFile
    }
}

この仕組みにより、ペイン間の切り替えがボタン1回で完結する。ペイン1のボタンを押して口述を始め、途中でペイン3に切り替えたくなったらペイン3のボタンを押すだけだ。ペイン1の音声入力は自動で止まり、ペイン3が即座に始まる。


動作の流れまとめ

[Stream Deck] ボタン押下
    ↓
[bat] AHKスクリプト起動(ペイン番号を引数で渡す)
    ↓
[AHK] 他ペインの .lock を削除(排他制御)
    ↓
[AHK] 自ペインの .lock が存在する?
    ├─ Yes → .lock を削除して終了(= 停止トグル)
    └─ No  → .lock を作成してループ開始(= 開始トグル)
    ↓
[AHK] ループ: 50msごとに Space down を送信
    ↓
[AHK] .lock が消えたらループ脱出 → Space up → 終了

振り返り

Space down を1回送って沈黙するターミナルを眺めた時間が一番長かった。物理キーの長押しとソフトウェアからのキーイベント送信は、見た目は同じでもOSの扱いが違う。キーリピートは物理キーボードのコントローラーが生成するもので、SendInput で1回送っただけでは再現されない。

状態ファイルによるプロセス間通信は原始的だが、AutoHotkeyのようにプロセス間のメッセージングが面倒な環境では手堅く動く。ファイルの有無だけで状態を管理するので、デバッグも .lock ファイルをエクスプローラーで確認するだけで済む。

Stream Deckに4つのペインボタンが並んでいる画面を見ると、音声入力のハードルが目に見えて下がった実感がある。キーボードに手を伸ばさずに口述を切り替えられるのは、思った以上に作業のリズムを変える。