Claude Codeのフックでずんだもんボイスをランダム再生する
Claude Codeのフック機能を使って、作業の開始・完了時にVOICEVOXのずんだもんボイスを鳴らす。ボイスのパターンを複数用意してランダムに再生する仕組みを作った。
完成イメージ
プロンプトを送信すると「了解なのだ!さっそく取りかかるのだ」などの作業開始ボイスがランダムに流れ、応答が完了すると「できたのだ!確認してほしいのだ」などの作業完了ボイスがランダムに流れる。
用意したボイスパターンは以下の通り。

作業開始(10パターン)
- 了解なのだ!さっそく取りかかるのだ
- おっけーなのだ!やるのだ
- 任せるのだ!ずんだパワー全開なのだ
- よーしやるのだ!腕がなるのだ
- ふっふっふ、このくらい朝飯前なのだ
- スイッチオンなのだ!集中モード発動なのだ
- まかせろなのだ!ずんだもんの本気を見せるのだ
- キックオフなのだ!試合開始の笛が鳴ったのだ
- ウォーミングアップ完了なのだ!全力で行くのだ
- 筋トレ前のストレッチみたいに準備万端なのだ
作業完了(10パターン)
- できたのだ!確認してほしいのだ
- 完了なのだ!
- どやっ!完璧に仕上げたのだ
- ミッションコンプリートなのだ!褒めてほしいのだ
- ふぅー、やりきったのだ!ずんだもちで乾杯なのだ
- はいできたのだ!天才かもしれないのだ
- お届けものなのだ!出来立てほやほやなのだ
- ゴーーール!見事に決めたのだ!
- 試合終了なのだ!完封勝利なのだ
- 筋トレ後のプロテインみたいに達成感マックスなのだ
前提
- Windows環境
- VOICEVOXがインストールされ、エンジンが起動している(
http://localhost:50021) - Python 3.x
- Claude Code CLI
ファイル構成
voicevox-tts/
└── .claude/
└── hooks/
├── play_voice.py # 再生スクリプト(フックから呼ばれる)
├── generate_voice_cache.py # wav生成スクリプト
└── voice_cache/
├── start_01.wav 〜 start_10.wav # 作業開始
├── stop_01.wav 〜 stop_10.wav # 作業完了
├── notification.wav # 通知
├── ask_user.wav # 質問
└── subagent_stop.wav # サブエージェント完了
使用するフックイベント
Claude Codeには複数のフックイベントが用意されている。今回使うのは以下の2つ。
| イベント | タイミング | 用途 |
|---|---|---|
UserPromptSubmit | ユーザーがプロンプトを送信したとき | 作業開始ボイス |
Stop | Claudeの応答が完了したとき | 作業完了ボイス |
その他のイベント(PreToolUse、PostToolUse、Notification、SubagentStopなど)も利用できる。ただし、ツール実行のたびに鳴らすと鬱陶しいので、開始と完了だけにとどめるのが実用的。
設定手順
1. 音声ファイルの生成スクリプト
VOICEVOXのAPIを叩いてwavファイルを事前生成する。
# generate_voice_cache.py
"""イベント通知用の音声ファイルを事前生成するスクリプト
Usage: python generate_voice_cache.py [--only start|stop|single]
"""
import argparse
import json
import os
import urllib.parse
import urllib.request
CACHE_DIR = os.path.join(os.path.dirname(__file__), "voice_cache")
# 単発イベント(1イベント1ファイル)
SINGLE_MESSAGES = {
"pre_tool_use": "ツール使うのだ",
"post_tool_use": "終わったのだ",
"subagent_stop": "サブエージェント終わったのだ",
"notification": "お知らせなのだ",
"ask_user": "選んでねなのだ",
}
# 作業開始パターン(ファイル名: start_01.wav 〜 start_10.wav)
START_MESSAGES = [
"了解なのだ!さっそく取りかかるのだ",
"おっけーなのだ!やるのだ",
"任せるのだ!ずんだパワー全開なのだ",
"よーしやるのだ!腕がなるのだ",
"ふっふっふ、このくらい朝飯前なのだ",
"スイッチオンなのだ!集中モード発動なのだ",
"まかせろなのだ!ずんだもんの本気を見せるのだ",
"キックオフなのだ!試合開始の笛が鳴ったのだ",
"ウォーミングアップ完了なのだ!全力で行くのだ",
"筋トレ前のストレッチみたいに準備万端なのだ",
]
# 作業完了パターン(ファイル名: stop_01.wav 〜 stop_10.wav)
STOP_MESSAGES = [
"できたのだ!確認してほしいのだ",
"完了なのだ!",
"どやっ!完璧に仕上げたのだ",
"ミッションコンプリートなのだ!褒めてほしいのだ",
"ふぅー、やりきったのだ!ずんだもちで乾杯なのだ",
"はいできたのだ!天才かもしれないのだ",
"お届けものなのだ!出来立てほやほやなのだ",
"ゴーーール!見事に決めたのだ!",
"試合終了なのだ!完封勝利なのだ",
"筋トレ後のプロテインみたいに達成感マックスなのだ",
]
def generate_wav(text, output_path, speaker=1, speed=1.0):
base = "http://localhost:50021"
encoded = urllib.parse.quote(text)
req = urllib.request.Request(
f"{base}/audio_query?text={encoded}&speaker={speaker}",
method="POST",
)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as resp:
query = json.loads(resp.read())
query["speedScale"] = speed
query["intonationScale"] = 1.3
query["pitchScale"] = -0.02
query["prePhonemeLength"] = 0.05
query["postPhonemeLength"] = 0.05
body = json.dumps(query).encode()
req2 = urllib.request.Request(
f"{base}/synthesis?speaker={speaker}",
data=body,
method="POST",
)
req2.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req2) as resp2:
with open(output_path, "wb") as f:
f.write(resp2.read())
print(f" {output_path} ({os.path.getsize(output_path)} bytes)")
def generate_numbered(prefix, messages):
for i, text in enumerate(messages, 1):
filename = f"{prefix}_{i:02d}.wav"
path = os.path.join(CACHE_DIR, filename)
print(f" [{filename}] {text}")
generate_wav(text, path)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--only", choices=["start", "stop", "single"])
args = parser.parse_args()
os.makedirs(CACHE_DIR, exist_ok=True)
print("Generating voice cache files...")
if args.only is None or args.only == "single":
for name, text in SINGLE_MESSAGES.items():
path = os.path.join(CACHE_DIR, f"{name}.wav")
generate_wav(text, path)
if args.only is None or args.only == "start":
generate_numbered("start", START_MESSAGES)
if args.only is None or args.only == "stop":
generate_numbered("stop", STOP_MESSAGES)
print("Done!")
VOICEVOXを起動した状態で実行する。
python generate_voice_cache.py # 全部生成
python generate_voice_cache.py --only start # 作業開始のみ
python generate_voice_cache.py --only stop # 作業完了のみ
2. ランダム再生スクリプト
番号付きファイル({event}_01.wav 〜 {event}_NN.wav)があればランダムに1つ選んで再生する。番号付きファイルがなければ {event}.wav にフォールバックする。
# play_voice.py
"""キャッシュ済み音声ファイルを再生する(フックから呼ばれる)
Usage: python play_voice.py <event_name>
"""
import glob
import os
import random
import subprocess
import sys
import winsound
CACHE_DIR = os.path.join(os.path.dirname(__file__), "voice_cache")
def pick_wav(event):
"""番号付きファイルからランダム選択、なければ単一ファイルにフォールバック"""
pattern = os.path.join(CACHE_DIR, f"{event}_[0-9][0-9].wav")
numbered = glob.glob(pattern)
if numbered:
return random.choice(numbered)
single = os.path.join(CACHE_DIR, f"{event}.wav")
return single if os.path.exists(single) else None
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit(1)
# --play <path>: デタッチドプロセスとして再起動された側 → 同期再生
if sys.argv[1] == "--play" and len(sys.argv) > 2:
winsound.PlaySound(sys.argv[2], winsound.SND_FILENAME)
sys.exit(0)
event = sys.argv[1]
path = pick_wav(event)
if not path:
sys.exit(0)
# デタッチドプロセスで自身を再起動して即リターン
subprocess.Popen(
[sys.executable, __file__, "--play", path],
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW,
close_fds=True,
)
デタッチドプロセスとして再起動する仕組みにしている。フックから呼ばれた時点では即リターンし、音声再生は別プロセスで行う。こうするとClaude Codeの処理をブロックしない。
3. settings.json のフック設定
~/.claude/settings.json の hooks セクションに以下を追加する。
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "python /path/to/play_voice.py start"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "python /path/to/play_voice.py stop"
}
]
}
]
}
}
/path/to/play_voice.py は実際のパスに置き換える。Windowsの場合はフォワードスラッシュ(C:/Users/.../play_voice.py)で記述する。
仕組みのまとめ
ユーザーがプロンプト送信
→ UserPromptSubmit フック発火
→ play_voice.py start
→ start_01.wav 〜 start_10.wav からランダム選択
→ デタッチドプロセスで再生
Claudeが応答完了
→ Stop フック発火
→ play_voice.py stop
→ stop_01.wav 〜 stop_10.wav からランダム選択
→ デタッチドプロセスで再生
VOICEVOXの音声パラメータ
generate_voice_cache.py では以下のパラメータを設定している。
| パラメータ | 値 | 説明 |
|---|---|---|
speaker | 1 | ずんだもん(ノーマル) |
speedScale | 1.0 | 話速 |
intonationScale | 1.3 | 抑揚(デフォルトより強め) |
pitchScale | -0.02 | ピッチ(わずかに低め) |
prePhonemeLength | 0.05 | 発話前の無音(短め) |
postPhonemeLength | 0.05 | 発話後の無音(短め) |
speaker IDはVOICEVOXのキャラクターごとに異なる。http://localhost:50021/speakers で一覧を確認できる。
パターンを追加するには
generate_voice_cache.pyのSTART_MESSAGESまたはSTOP_MESSAGESリストにテキストを追加- ファイル名の連番は自動で振られる
python generate_voice_cache.py --only startで再生成play_voice.pyのpick_wavはglob({event}_[0-9][0-9].wav)で拾うので、ファイルを追加するだけで動く