1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「ボス、テストが全部通ったのだ」── Claude Code の作業完了をずんだもんに喋らせた

1
Posted at

はじめに

Claude Code に大きめのリファクタリングを任せて席を外した。しばらくしてキッチンから戻ってくると、スピーカーから声が聞こえてきた。

「ボス、テストが全部通ったのだ。」

ずんだもんだった。

これは Claude Code の hook という機能を使って組んだ完了通知の仕組みだ。構成はシンプルで、「Claude Code が作業を終えたら Haiku が transcript を要約し、VOICEVOX のずんだもんが声で読み上げる」というだけ。全部シェルスクリプト 150 行で動く。

ターミナルを覗きに行かなくて済む、という話ではない。作業の完了が音として空間に届くという体験が根本的に違う。デスクトップ通知は目が向いていなければ気付けないが、声は席を外していても耳が拾ってくれる。

この記事では、その仕組みの作り方と、設計の中で面白かった部分を書く。


全体像:AI が AI を使って AI に喋らせる

まず全体の流れを見てほしい。

Claude Code に指示する
  │
  └─ UserPromptSubmit hook 発火
        │
        └─ VOICEVOX コンテナが起動する(ずんだもんが目覚める)

Claude が作業する(人間は席を外す)
  │
  └─ Stop hook 発火
        │
        ├─ Haiku が transcript を要約(AI #2)
        │
        └─ ずんだもんが要約を読み上げる(AI #3)

Claude Code が作業し(AI #1)、Haiku が要約し(AI #2)、VOICEVOX のずんだもんが話す(AI #3)。三段のパイプラインが全部シェルスクリプトで繋がっている。

そして、話しかけたらずんだもんが起きて、報告が終わったら眠る。この「ライフサイクルの連動」が一番面白い部分で、後のセクションで詳しく説明する。


セットアップ

リポジトリを公開しているので、クローンするだけで使える。

git clone https://github.com/ryokwkm/claude-task-reporter.git ~/.claude/hooks/claude-task-reporter

~/.claude/settings.json に 2 つの hook を登録する。

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/claude-task-reporter/voicevox_sync.sh",
            "timeout": 10
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/claude-task-reporter/session_summary.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

設定ファイルを作成する。

cp ~/.claude/hooks/claude-task-reporter/session_summary.conf.example \
   ~/.claude/hooks/claude-task-reporter/session_summary.conf

VOICEVOX を使う場合、初回は Docker イメージ(約 2〜3GB)のダウンロードが走る。2 回目以降は数秒で起動する。

bin/ を PATH に追加しておくと、ON/OFF の操作が楽になる。

export PATH="$HOME/.claude/hooks/claude-task-reporter/bin:$PATH"
コマンド 説明
csconfig ON/OFF をトグル
csstatus 現在の状態を表示
csedit 設定ファイルを vi で開く

一番の工夫:話しかけた瞬間にずんだもんが起動する

VOICEVOX は Docker コンテナで動かす。常時起動でもいいが、使っていないときにメモリを食い続けるのは無駄だ。

そこで Claude Code に話しかけた瞬間にコンテナを起動し、通知を無効化したら止めるという設計にした。

Claude Code には UserPromptSubmit という hook がある。ユーザーがプロンプトを送信した瞬間に任意のコマンドを実行できる。これと作業完了時の Stop hook を組み合わせると、こういう世界が作れる。

hook タイミング やること
UserPromptSubmit 話しかけた瞬間 VOICEVOX コンテナを起動(必要なら)
Stop Claude が応答し終わった瞬間 transcript を要約してずんだもんに喋らせる

実装は voicevox_sync.sh。「設定ファイルの CS_ENABLED の値」と「コンテナの実際の稼働状態」を見比べて、ズレていたら補正するだけのシンプルな設計だ。

if [ "${CS_ENABLED:-false}" = "true" ]; then
    if ! is_running; then
        start_container  # docker run
    fi
else
    if is_running; then
        stop_container   # docker stop
    fi
fi

ただし、UserPromptSubmit hook はユーザーの入力をブロックする。コンテナ起動に数秒かかるので、そのまま書くとプロンプトを送るたびに待たされてしまう。

解決策は self-fork。親プロセスを即座に終了させ、実処理を子プロセスに引き継ぐ。

# 親プロセスは即座に exit、子で処理を継続
if [ -z "${VOICEVOX_SYNC_FORKED:-}" ]; then
    VOICEVOX_SYNC_FORKED=1 nohup "$0" "$@" >/tmp/voicevox_sync.log 2>&1 &
    exit 0
fi

これで hook の応答は数ミリ秒で返り、コンテナ起動はバックグラウンドで進む。Claude Code に話しかけながら、裏でずんだもんが静かに目を覚ます。


ずんだもんに喋らせる

VOICEVOX はローカルで動く音声合成エンジンで、HTTP API を持っている。curl で音声データ(WAV)を生成して afplay で再生するだけで動く。

speak_with_zundamon() {
    local text="$1"
    local wav="/tmp/claude_zunda_$$.wav"

    # ① テキストから音声クエリを生成
    local query
    query=$(curl -s -X POST "${VOICEVOX_URL}/audio_query?speaker=${VOICEVOX_SPEAKER}" \
        --get --data-urlencode "text=${text}") || return 1

    # ② クエリから WAV を合成
    curl -s -X POST "${VOICEVOX_URL}/synthesis?speaker=${VOICEVOX_SPEAKER}" \
        -H "Content-Type: application/json" \
        -d "$query" -o "$wav" || return 1

    # ③ 再生
    afplay "$wav"
    rm -f "$wav"
}

ステップは 3 つだけ。/audio_query でテキストをパラメータ化し、/synthesis で WAV に変換し、afplay で鳴らす。特別なライブラリもランタイムも不要で、シェルスクリプトの範囲で完結する。

フォールバックの設計

VOICEVOX は Docker が動いていないと使えない。そのため CS_VOICE_MODE で 3 段階の動作を設定できるようにしてある。

動作
auto(デフォルト) VOICEVOX が動いていればずんだもん、止まっていれば say
say 常に macOS 標準の say
zundamon 常にずんだもん(起動していなければ読み上げをスキップ)

デフォルトの auto は、Docker が入っていない環境や、コンテナが何らかの理由で起動に失敗した場合でも say で報告してくれる。道具が壊れても動き続ける、という小さな安心感がある。


プロンプトで「聴かれること」を前提にする

Stop hook で transcript の末尾 30 行を Haiku に渡して要約させるのだが、このプロンプトに一工夫した。

通常、LLM へのプロンプトは「これを読む人間」を前提に書く。しかし今回の出力は誰も読まない。そのまま音声合成エンジンに渡され、スピーカーで流れる。

プロンプトの冒頭にこう書いた。

「あなたが生成する文章は、人間に読まれることはなく、そのまま音声合成エンジン VOICEVOX(声: ずんだもん)に渡されて、ボスのスピーカーで読み上げられます。誰も途中で校正しません。したがって、求められているのは『文字として正しい文章』ではなく『耳で聞いたときに自然で聞き取りやすい話し言葉』です。」

読者は人間ではなく音声合成エンジンだ、と明示する。

これを書く前後で出力の自然さが大きく変わった。マークダウンの記号が混入しなくなり、英単語の読み方(haiku → 「ハイク」)も自然に変換されるようになった。文字数も「80〜100文字、15秒以内」と数値で指定することで、ずんだもんの読み上げ速度とテンポが合うようになった。

LLM への指示は「誰が・どんな文脈で受け取るか」を書くだけで品質が変わる。今回の場合、受け取るのが人間ではなく音声合成エンジンだというのは、書いてみて初めて気付いた視点だった。


おわりに

Claude Code の hook は「LLM が応答を返したタイミングで任意のシェルスクリプトを実行する」というシンプルな仕組みだ。

今回作ってみて面白かったのは、hook を 1 本ではなく 2 本組み合わせると「ライフサイクル」を定義できるということだった。UserPromptSubmit(会話の開始)と Stop(会話の終了)のペアで、「Claude と話している間だけ動くインフラ」が作れる。今回は VOICEVOX コンテナを対象にしたが、同じ発想でデータベースを起こしたり、監視ツールを動かしたりといった応用も考えられる。

Claude Code に複雑な作業を任せながら席を外せるようになった。ずんだもんが「ボス、〇〇が完了したのだ」と声をかけてくれる環境は、慣れると手放せなくなる。

クレジット: 本記事の音声読み上げ機能は [VOICEVOX:ずんだもん] を利用しています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?