はじめに
Mac で Claude Code を声だけで操るために作ったVoiceTerminalアプリ。
👤 声で頼む → 🤖 Claude Codeが作業 → 🔊 結果を読み上げ → 👤 声で直しを頼む → …
仕組みとしては「聞き取り(音声認識)」と「読み上げ(音声合成)」をつなぐだけです。/voiceコマンドに似ていますが実際に使ってみると、会話として気持ちよく回るまでに驚くほどたくさんの壁がありました。
人間が気持ち悪いと感じる「不気味の壁」をどう乗り越えたかを、会話のBefore/Afterを比較しながら紹介します。
現在までのロードマップ
大きな流れは次の3つのステップです。
- ステップ1:喋れるようにする(読み上げと聞き取りをつなぐ)
- ステップ2:間違えないようにする(回り込み・雑音・誤読をなくす)
- ステップ3:待たせないようにする(最初の返事と作業中の沈黙を縮める)
| 順番 | テーマ | 会話で変わったこと |
|---|---|---|
| 1 | フックで読み上げ | 入力はClaude Code組み込みの/voice 。発話はClaude CodeのフックからmacOSのsayで喋らせる。 |
| 2 | ハンズフリー・常時聞き取り | キーを押さずに話しかけられる。ここで自分の声を拾う「回り込み」が出始める |
| 3 | VoiceTerminal誕生 | Claude Codeのターミナルを内蔵したアプリに作り替え、聞き取りと読み上げをアプリの中に持つ |
| 4 | 声で許可・声の担当・ストップ | 「承認」と言えば実行許可が通る。プロジェクトごとに違う声。「ストップ」で止まる |
| 5 | 読み仮名 | Claude自身に読み方とアクセントを書かせ、AI音声でありがちな誤読をなくす |
| 6 | あだ名・iPad・早く読む | マルチウィンドウ対応。名前を呼ぶと担当AIが前に出る。iPadが遠隔ウィンドウになる。返事の読み始めが数秒早まる |
| 7 | 復唱・声紋・雑音対策 | 即座に復唱。本人の声だけを通す。周囲の喋り声を切り落とす |
| 8 | 外出先・iPhone・考え中のBGM | 外からiPad/iPhoneで繋ぐ。AIが考えている間のBGM。AIに聞き直しできる |
| 9 | Claudeの沈黙を埋める | 作業の途中を要約して読む。たまに一言しゃべる。作業完了の見込み時間を伝える |
工夫したこと
1. 最初の返事を10秒 → 約2秒に(復唱)
AIに意思が伝わったか確認できて、人間が安心できる工夫です。
Before
👤「設定画面の余白を詰めて」
│
│ ……(6〜15秒無音)……
│ 「届いたのかな?」「もう一回言うべき?」
▼
🔊「設定画面の余白を直します」
無音の中身はほとんどがClaudeが文章を考えている時間でした。ここは仕組みでは縮められません。
After
👤「設定画面の余白を詰めて」
│
├──→ 🤖 Claude Codeへ即送信(待たない)
│
└──→ 🧠 手元の小さなAIで即答(約0.5秒)
│
▼
🔊「設定画面の余白を詰めます」 ← 話し終わりから約2秒
……
🔊「余白を8ポイント詰めました。次はどうしますか」 ← Claudeの報告
Claudeを待たずに、Macの中で動く小さなAIに「何を聞き取ったか」だけを先に言わせます。中身のある返事は今までどおりClaudeが返します。
復唱には「聞き取りが合っているか確かめる」役目もあります。聞き間違えていればその場で分かります。
調整のなかで分かったこと
- 小さすぎるモデルは速いが、質問を作業にすり替える
- 「作業ない?」→「完了します」(逆の意味を言うので人は通じたと思い込む)
- 頼みごと・質問・人の報告で返し方を分けた
- 「これって前に作ったよね」→「そうだね、前に作ったね」(同意で返す)
- 「リポジトリはクリーンだよね」→「状態を確かめます」(見ないと分からないものは確かめる)
- 直前のClaudeの一言を添えて渡す
- 「1番目の方法で」「さっきのやつ」でも何の話か踏まえて復唱できるようになる
- 使っていない間に手元のAIモデルがメモリから追い出されて遅くなるので、人間が話し始めた瞬間に温めて準備する
- 人間が「その復唱は不自然。こう返してほしい」と言ったら記録に残し学習する
キーワード:Ollama、gemma、Apple Foundation Models、few-shot
2. 話し終わりを早く見切る(でも途中で切れないように)
人間同士の会話なら、相手が話し終わればすぐに返事が来ます。AIにも同じ間合いで聞き取ってほしいところです。
Before
👤「ログを見てほしい」
│ 話し終わってから2.5秒(長いときは7秒)
▼
📝 確定 → 送信
最初の失敗
「文字起こしが0.8秒止まったら確定」にしたところ、話している途中で指示が2つに割れました。
👤「了解。あと、サイクルタイムのログを見てほしい」
▼
📝「了解。あと」 ← ここで送信されてしまう
📝「サイクルタイムの」
📝「ログを見てほしい。」
文字起こしは塊で届くので、話し続けていても止まって見えるときがあります。
After
👤「了解。あと、サイクルタイムのログを見てほしい」
│
│ 文字起こしが0.3秒止まった
│ → マイクの音量を確かめる:直近1秒は静か?
│ 静か → すぐ確定させる
│ 声がある → まだ待つ
▼
📝「了解。あと、サイクルタイムのログを見てほしい」 → 送信
「止まった」だけでなく「静かになった」を確かめてから確定させます。静けさの基準は部屋やマイクで変わるので、決め打ちせず直近5秒の音から測ります。考えながら話す人向けに待つ長さは設定で変えられます。
キーワード:SpeechAnalyzer、finalize、音量(dB)による無音判定
3. 自分の声を拾わない(回り込み)
これは長く苦しんだ問題で、直しても直しても違う原因で10回ほど再発しました。
症状
🔊「ビルドが通りました。次はどうしますか」
│(スピーカーから出た声を)
▼
🎤 マイクが拾う
▼
📝「ビルドが通りました。次はどうしますか」 → Claudeへ送信
▼
🤖「はい、次は何をしましょうか?」 → 🔊 → 🎤 → …(無限ループ)
After
🔊 読み上げ中 ────────────────┐
│ マイクのゲートを閉じる
🎤 ここで拾った音は捨てる ◀────┘
🔊 読み終わり+余韻
🎤 ゲートを開ける
言葉にすると簡単ですが、いくつもの原因の複合でした。
- 読み上げの予約と開始の間に一瞬すき間があり、「もう読み終わった」と誤解してマイクを開いた
- アプリを通らない経路で喋っていて、アプリは自分が喋っていることを知らなかった
- 複数ウィンドウを起動しているときに、隣のウィンドウの発話を拾っていた
- AirPodsなら耳の中で鳴るので拾わない…と思っていたが実測で拾っていた
いまは「直す前に必ずログでどこで鳴ったかを確かめる」を決まりにしています。
4. テレビや家族の声を私からの指示として受け取らない
家で使うと周りの音は避けられません。私の声だけをAIへの指示として通すようにしました。
Before
📺「明日の天気は晴れでしょう」
👤「テストを流して」
▼
📝「明日の天気は晴れでしょうテストを流して」 → 送信
After(2つの関門で止める)
📺 + 👤 が混ざった音
│
├─ ① 声紋で照合:本人の声か?
│ 別人 → 捨てる(画面には「本人の声ではないので採用しない」と出す)
│
└─ ② 音量で頭を切る:テレビは遠くて小さい、本人は近くて大きい
ただし切る側を声紋で確かめ、本人なら切らない
▼
📝「テストを流して」 → 採用
内蔵マイクとAirPodsでは同じ人でも音質が大きく違ったため、声紋はマイクごとに登録します。本人の言葉が捨てられるのは雑音が混ざるより困るので、迷ったときは通す側に倒しています。
雑音が「うん」「はい」のような相槌に変換される現象もあるので、これらの入力は無視します。
キーワード:sherpa-onnx、ERes2NetV2
5. 読み上げは「見出しだけ」
AIの出力をそのまま読ませると、耳では追いきれない量になります。声で伝えるのは要点だけにしました。
Before
🤖 画面:ビルドログ300行、エラー3件、表、コード…
🔊「ビルド失敗。エラー3件。1件目、Foo.swift 128行目。cannot find ...」
耳で聞いても判断に使えず、止めることもできません。
After
🤖 画面:(今までどおり全文表示)
【AI】ログイン画面の余白を8ポイント詰めました。次はどうしますか。
🔊「ログイン画面の余白を8ポイント詰めました。次はどうしますか」
Claudeに「応答の先頭に【AI】で始まる1行の要約を書く」という決まりを持たせ、アプリ側の音声合成ではその行だけを読みます。要点は声、詳細は画面という分担です。
決めごと
- ビルドとテストの結果は声に乗せない(成否も件数も)
- パスやコマンド、英単語の羅列を入れない
- 選択肢を聞くときは画面だけでなく声にも各案を一言ずつ乗せる
🔊「3案あります。1番は○○、短所は…。2番は…。3番は…」
👤「2番で」
ハマった落とし穴
「ビルドの結果を読まないで」と10回以上頼んでも直りませんでした。原因はClaudeではなく、フックがビルドの出力を自前で読み上げていた別の経路でした。「鳴らしているのはClaudeか仕組みか」をまず分けるのが教訓です。
6. 誤読をなくす(読み仮名をClaudeに書かせる)
AI音声でいちばん気になるのが読み間違いです。聞き手が引っかかると会話の流れが止まってしまいます。
Before
【AI】一行目の余白を直しました
🔊「いっこうめの…」「ひとぎょうめの…」
読み上げエンジンは文脈を見ないので、「一行」「対応」「行った」などで読みが割れます。
After
【AI】一行目の余白を8ポイント詰めました。
【読み】イチギョ'ウメノ/ヨハ'クヲ/ハチポイ'ント/ツメマ'シタ
🔊 正しい読みとアクセントで読む(【読み】行は画面から隠す)
読みを決められるのは文の意味を分かっている側(Claude)だけという考え方です。記法が1文字でも違うとエンジンに丸ごと捨てられるので、送る前に機械的に整える処理も挟んでいます(「ー」を母音に開く、助詞の「は」を「ワ」に直す、など)。
キーワード:VOICEVOX、AivisSpeech、AquesTalk風記法
7. 返事の読み始めを早める
AIはおしゃべりなので大量の文章を出力します。
そのあいだを待ってから発声したのでは間(ま)が空きすぎるので、先にしゃべらせます。
Before
🤖 【AI】修正しました。次はどうしますか。 ← 1行目
| 大きな表 | … ← 続く本文
…(全部流れ終わるまで待つ)…
🔊 読み始め ← 発話から4〜11秒
After
🤖 【AI】修正しました。次はどうしますか。
【読み】… ← この2行がそろった瞬間
🔊 読み始め
| 大きな表 | …(読んでいる間に画面に流れる)
人は返事を待っているので、まず返事、そのあと残りの本文という順にしました。
8. 待ち時間を「無音」にしない
復唱で最初の返事は速くなりましたが、その後のClaudeの作業中は相変わらず黙ったままでした。長いと数分の無音が発生します。
8-1. 考え中のBGM
👤「リファクタして」
🔊「リファクタします」
🎵 ♪〜(イヤホンでだけ小さなBGM) ← AIが作業中
🔊「終わりました」
🎵 (止まる) ← 人が指示する番
人間が話してAIが反応できるタイミングが分かります。Macのスピーカーで鳴らすとマイクが拾うので、イヤホンのときだけ鳴らすようにしています。
8-2. 作業の途中を要約して読む
AI側が小まめに報告してくる場合、ある程度まとめて文章にして発話するようにしています。
こうしないと人間側が指示を出す間(ま)が無くなります。
🤖(ツールの合間)「まず設定ファイルを読みます」
🤖「読み込みの順番がおかしいので直します」
🤖「テストを追加します」
│ 20秒以内の文をためて、まとめて要約
▼
🔊「設定の読み込み順を直して、テストを足しています」
8-3. 3分黙ったら一言
(AIが裏で作業中、3分経過)
🔊「まだ作業中です。始めてから3分です」
裏で回している作業の間はClaude自身は何も言えないので、アプリが代わりに言います。サブエージェントを複数起動している場合もそれを検知して経過時間を伝えます。
8-4. 見込み時間を言わせ、実績で補正する
Claude作業の見込み時間を推測して人間に伝えておきます。
推測と実績時間のズレを学習して、次回からの精度を上げます。
🔊「5分くらいかかるよ」
… 実際は1分40秒で終了 …
📊 記録:見込みの0.33倍
▼ 次のターンでClaudeに伝える
「あなたの見込みは実際の約3倍。0.36倍くらいで言って」
🔊「2分くらいで済むよ」 → 実際1分50秒
人間心理として10分と言って9分で終わるほうが嬉しいので、実績を重視しつつマージンを持たせています。
9. 止める・言い直させる・掘り下げる
人間同士なら「待って」「もう一回言って」は当たり前のやりとりです。AIにも同じように言えるようにしました。
👤「ストップ」 → 走っている処理を止める
👤「もう一回」 → 直前の報告をもう一度言わせる(Claudeには送らない)
👤「もっと詳しく」 → 長い説明を300文字以内で、耳で分かるように話す
👤「えっと、もう一回」 → 言いよどみを外して「もう一回」と判定
「もう一回」はアプリが自分で答えます。Claudeに送ると処理の順番待ちに並び、また時間が掛かってしまうからです。
10. 実行許可に声で答える
AIは人間に頻繁に承認を求めてきます。その承認に対しても声で回答したかったので承認ゲートを設けました。
このゲートでは通常の会話文章は拾わずに、特定の音だけを通すようにしています。
Before
🤖 実行許可を待っています(画面にダイアログ)
👤(画面を見ていないので気づかない)……
After
🔊「許可を待ってます。コマンド実行」
👤「承認」
▼
✅ 許可が下りて作業が続く
「承認」を受け付けるのは許可を待っている間だけです(承認ゲート)。いつでも受け付けると関係ない場面の「承認」で勝手に許可が下りてしまうので、質問への回答や計画の承認は許可とは別物として扱います。
11. どのAIの報告か、声で聞き分ける
プロジェクトごとに担当の声
🔊(女性の声)「アプリAのビルドが通りました」
🔊(男性の声)「設計書の下書きを保存しました」
複数の仕事を並行すると、声が全部同じだとどのプロジェクトの話か分かりません。
人間のチームのように、プロジェクトごとに担当の声を置きます。同じプロジェクトなら必ず同じ声です。
あだ名で呼ぶ
👤「ジャービス、状況を教えて」
▼ 該当のウィンドウが前面に出る
🔊「はい、ジャービスです。ログを分析しています」
12. どこからでも話しかける(iPad / iPhone)
Macの前に座っていなくても、手元のiPadやiPhoneから同じように話しかけられます。
📱 外出先のiPad / iPhone(画面・マイク・スピーカー)
│ 聞き取った文字、ボタン操作
▼
💻 自宅のMac(Claude Codeが動く本体。読み上げの声を作る)
│ 端末の画面、読み上げの音、写真、動画
▼
📱 外出先のiPad / iPhoneで表示・再生
- 家の中でも外出先からでも繋がる(VPN)
- 質問の選択肢は画面の番号と文言がそのままSwiftUIボタンになる
- AIが考えている間のBGMもiPad側で鳴る
キーワード:Network.framework、Bonjour、Tailscale、SwiftTerm
数字で見る改善
| 項目 | Before | After |
|---|---|---|
| 人間が話しかけてから最初のAIの声 | 6.4〜14.6秒 | 約2秒 |
| 人間の話し終わりを確定 | 中央値2.5秒・最大6.6秒 | 静かなら0.3秒後に確定を促す |
| 作業後のAIの返事 | 本文が流れ終わってから(4〜11秒) | 約2秒 |
| AI作業中の無音 | 中央値16秒、長いと数分 | 20秒ごとの要約・3分ごとの一言・BGM |
| AI作業時間の見込み | 全く分からず人間は待つだけ | 作業前に見込みを伝達。人間は他の作業ができる |
やってみて分かったこと
相手の話し終わりを検知し、間髪入れずに返事を返す人間って凄い。
- 声の会話では「速さ」より「黙らないこと」が大事。言葉が届いたことが分かるだけでも人間は安心できる
- ただし喋らせすぎると話しかけるすきが無くなる。少し溜めて要約するくらいがちょうどいい
- 聞き取れなかったときは、迷ったら「通す」「待つ」側に倒す。本人の指示が黙って消えるのがいちばん困る
- 同じ不具合を何度も直すときは、直した内容より「原因・切り分け・却下案・実測値」を残して知識化するほうが後々効いてくる
- 声で操っている人は画面を見ていない。画面にだけ出る情報は無いのと同じ。実際に画面を見ずに1時間ほど使ってみる
使った技術(キーワード)
| 役割 | 技術 |
|---|---|
| 本体 | Swift、SwiftUI / AppKit、SwiftTerm(端末描画) |
| 聞き取り | Apple SpeechAnalyzer |
| 読み上げ | VOICEVOX、AivisSpeech、macOS say
|
| 復唱・要約 | Ollama(gemma)、Apple Foundation Models |
| 話者照合 | sherpa-onnx(ERes2NetV2) |
| Claude Code連携 | Hooks(MessageDisplay / PreToolUse / Stopなど)、MCP |
| iPad / iPhone連携 | Network.framework、Bonjour、TLS-PSK、Tailscale |
これから
- 人間がしゃべっている間はAIが話を譲るようにする
- 話の途中で確定されて続きが消えるケースを極力なくしていく
- 人の指摘を参考にして復唱の精度を改善し続ける(ここ意外と大事)
- iPhoneをポケットに入れて、AirPodsを繋いで声のやりとりだけで作業できるよう改善を続ける