はじめに
Twitter/X で以下のような投稿をみつけました。
つまり、Claude Code とかの生成 AI エディタでは、AI がやったことをツラツラと報告してくれますが、
この報告を実際の音声に出来たら相棒感が出て vibe coding が楽しいんじゃないだろうか?
という内容でありますね。(違ったらゴメン)
なんだか面白そう!と思ったので、真似をしてみようと思ったのですが、
VOICEVOX と Visual Studio Code なんもわからんので調べて整理しました。
で、結果出来たので、成果物を『レシピウェア』として公開するものであります。
今回はコーディング環境は Mac + Visual Studio Code+Codex プラグインです。
特にメインで使っているわけでもなく、ChatGPT Plus が1か月無料キャンペーンやってたから釣られた(おまけで Codex がついてる)ものであり、Codex 導入して1週間も経っていません。
キャンペーン終わったら使わなくなると思います。この AI 乞食め。
作ってないけど Antigravity でも似たようなことできるんじゃないですかね。
ちなみに MacbookNeo でも動きます。すげえな。Neo。
動作サンプル
00:49〜頃にテトリスが完成
フロー構成図
AI コーディング処理が終わったあとの報告なのですが、VSCode + Codex では config.toml という設定ファイルを設定することで、それ契機に、テキスト内容を他のアプリケーションに JSON で引き渡せるようなのです。(notify hook というらしい)
じゃあ引き渡された JSON をもとに、VOICEVOX(ずんだもん) で発話するシェルスクリプトを書いておけば、音声報告してもらえるだろう、という構成。

報告を読み上げてくれるのは良いのですが、長い通知を全文ダラダラと読み上げられるのはそれはそれでイヤです。
このため、2つの工夫を行います。
・実行した作業内容を ずんだもん口調で端的に要約してもらう。
・要約部分とそれ以外で切り分ける。
例えば、
依頼:HTML/JS/CSS で低機能なテトリス作ってください。
〜〜〜
回答:
HTML/CSS/JS だけで動く、低機能なテトリスを作成しました。
index.html に画面構成を追加しました。
style.css で簡単な見た目とレスポンシブ対応を入れました。
script.js で盤面生成、ブロック生成、移動、回転、落下、ライン消去、ゲームオーバー、リスタートを実装しました。
注意点として、この環境では node が入っていなかったため、node --check による構文確認はできませんでした。ブラウザで index.html を開けば動作します。
【完了要約】
HTML/JS/CSSだけの低機能テトリスを作成し、移動・回転・消去・再開まで入れたのだ
これによって、目印となる【完了要約】以前を切り捨てることができるため、
発話されるのは上記の赤字部分のみとなります。
登場人物
talk.sh
JSON を受け取って parse して、VOICEVOX API 叩いて再生する今回の主役。
今回の思いつきの ために生成 AI によって雑に生み出された存在。
VOICEVOX
フリーの音声合成ソフト。超有名な ずんだもんエンジン。
実は起動するだけで http://localhost:50021 で音声合成 API を受け付けることがわかった今回の本当の主役。
えっ、すごない??
Visual Studio Code (VSCode)
みんな使ってるMicrosoftの神アプリ。Mac でも動く。
Codex をプラグインで動かせるすごいエディタ。
VSCode に行うべき設定
~/.codex/config.toml
一番上の方にグローバル設定として以下を追加します。(パスは書き換えてネ)
# ずんだもん通知(全プロジェクト共通)
notify = ["zsh", "/Users/your-directory/talk.sh"]
これにより、通知のタイミングで 設定したシェルスクリプトが毎回実行されます。
このため実行権をあらかじめ付与しておく必要があります。chmod 755 talk.sh。
~/.codex/AGENTS.md
前述した 工夫 を記載します。ずんだもんルール。
# 通知報告用ルール
あなたは通常のコーディング作業では**普通の丁寧で自然な日本語**で応答してください。
「~のだ」「~なのだ」などの口語は、**通知用の要約ブロックの中だけ**に限定してください。
### 通知ルール(重要なポイント)
- **大きな節目・切りが良いタイミング**でだけ、**最後に**通知用の要約を出力してください。
- 毎ターン必ず出す必要はありません。以下のケースで出すのが理想です:
- タスクが完全に完了したとき
- 重要なファイルの編集・追加・削除が終わったとき
- 調査・分析が一段落したとき
- テストが通った・エラーが解消されたとき
- ユーザーが待ちそうな長い処理が終わったとき
### 出力形式(2種類を使い分ける)
1. **途中状況報告**(作業中でも報告したいとき)
【途中報告】
ここに30〜50文字程度で、今の進捗を簡潔に書くのだ。
例: 「メインのロジックをリファクタリングし終わったのだ」
2. **最終完了報告**(タスクが完全に終わったとき)
【完了要約】
ここに30〜60文字程度で、何が終わったかを簡潔に書くのだ。
例: 「ログイン機能のバグを修正して全テストが通ったのだ」
### 厳守事項
- 通知ブロックは**応答の最後に1つだけ**置く(途中報告と完了要約を同時に出さない)
- 通知ブロック以外では「~のだ」口調を一切使わない
- 通知は長くせず、1〜2文以内に収める
- 頻繁に出し過ぎないよう、**本当に切りが良いところだけ**に出力する
それ以外は自然で読みやすい普通の日本語で、思考プロセスやコード説明をしてください。
これらの設定によって、先述のデータフローの流れが再現され、
バイブコーディングと ずんだもんの連携ができるようになってしまいます。すげえな。
レシピウェア配布
他人に公開するってなった瞬間にセキュリティ周りの考えが100倍くらい面倒くさくなるし、端金もらって問い合わせサポートなんて割に合わなさすぎてやってられませんから公開しません。
が、公開するかわりに最後にプロンプト代わりの README.md を置いておくので、
興味があれば、生成 AI 様に突っ込んで各自で作ってもらってください。
みたいな考え方のソフトウェア配布形式。
お料理のレシピ公開するみたいなのでレシピウェアって勝手に名付けました。
生成 AI に投入するための文章
# zunda talk
`talk.sh` は、Codex の notify hook から呼ばれるずんだもん読み上げスクリプトです。
Codex から届く JSON を受け取り、`【完了要約】` または `【途中報告】` の本文を取り出して `VOICEVOX` で再生します。
`VOICEVOX` が起動していない場合は、何もせず静かに終了します。
公開用に共有する場合は、個人の絶対パスを使わず、`YOUR_REPO_DIR` のようなプレースホルダに置き換えてください。
## できること
- Codex から渡された JSON を受け取る
- `last-assistant-message` / `last_assistant_message` / `message` を順に参照する
- `【完了要約】` があれば、その後ろの文面を優先して読み上げる
- `【完了要約】` がなければ、`【途中報告】` の後ろの文面を読み上げる
- `VOICEVOX` の起動を `http://127.0.0.1:50021/version` で確認する
- 話者 `3` で音声を生成し、`afplay` で再生する
- 音声速度を `1.2` 倍にしている
## 必要なもの
- macOS
- `zsh`
- `curl`
- `jq`(推奨。なくても簡易パースで動く)
- `afplay`
- `VOICEVOX` 本体
## 使い方
1. `talk.sh` に実行権限を付ける
```sh
chmod +x talk.sh
```
2. `~/.codex/config.toml` に notify hook を登録する
```toml
# macOS の例。`YOUR_REPO_DIR` は自分の配置先に置き換えてください。
notify = ["zsh", "/Users/your-name/path/to/zunda/talk.sh"]
```
3. Codex を再実行して、作業完了時に読み上げが動くか確認する
## 環境変数
`talk.sh` は、必要に応じて次の値を上書きできます。
- `TALK_VOICEVOX_URL`
- 既定値: `http://127.0.0.1:50021`
- `VOICEVOX` の接続先を変えたいときに使う
- `TALK_SPEAKER_ID`
- 既定値: `3`
- ずんだもん以外の話者に変えたいときに使う
- `TALK_SPEED_SCALE`
- 既定値: `1.2`
- 読み上げ速度を変えたいときに使う
## 読み上げの決まり方
`talk.sh` は、受け取った JSON から次の順で読み上げ用テキストを探します。
1. `last-assistant-message`
2. `last_assistant_message`
3. `message`
4. 取り出した文面の中に `【完了要約】` があれば、その後ろを使う
5. `【完了要約】` がなければ、`【途中報告】` の最後のもの以降を使う
6. それでも空なら `Codexの作業が完了したのだ!` を使う
引数が空でも、既定文で読み上げ処理は進みます。
## 動作の流れ
1. 引数の JSON を受け取る
2. `jq` で読み上げ文を抽出する
3. `【完了要約】` を優先し、なければ `【途中報告】` を使って本文を整える
4. `http://127.0.0.1:50021/version` で `VOICEVOX` の起動を確認する
5. `audio_query` を取得し、`speedScale` を `1.2` に変更する
6. `synthesis` で wav を生成し、`afplay` で再生する
## 手動テスト
```sh
./talk.sh '{"event":"agent-turn-complete","message":"【完了要約】読み上げ動作を確認したのだ"}'
```
`VOICEVOX` が起動していない場合は、何も再生されずに終了します。
## トラブルシュート
- 音声が出ない場合は、`VOICEVOX` が `http://127.0.0.1:50021` で起動しているか確認する
- `jq` がないと JSON の抽出精度が落ちるので、できれば入れておく
- `afplay` がない環境では再生できない
- `VOICEVOX` の話者 ID を変えると、読み上げの声質も変わる
## 補足
- ずんだもんの話者 ID は `3` を使っている
- 音声速度は `1.2` 倍に設定している
## 生成AI への指示
ユーザーの実行環境(Windows / Mac / Linux など)や使用エディタ、目的を確認し、必要な点だけ順番に質問して不明点を埋めて、talk.sh の実装を進めてください。
また、質問の回答結果に基づき、notify hook の利用に必要となる設定を提案してください。
不明点が残る場合も、まずは最小構成で VOICEVOX とシェルスクリプトまたはバッチで発声できる状態を作り、その後にエディタ連携へ進めるのがスムーズです。
talk.sh のコード
早速、レシピウェアの思想に反するのですが、Mac で動いているので参考までに掲載します。
#!/bin/zsh
# ======================
# VOICEVOX通知スクリプト(1.2倍速版)
# ======================
JSON="${1:-}"
VOICEVOX_URL="${TALK_VOICEVOX_URL:-http://127.0.0.1:50021}"
#SPEAKER_ID
#四国めたん → ノーマル(ID: 2)
#ずんだもん → ノーマル(ID: 3)
#東北ずん子 → ノーマル(ID: 107)
#東北きりたん → ノーマル(ID: 108)
#東北イタコ → ノーマル(ID: 109)
SPEAKER_ID="${TALK_SPEAKER_ID:-3}"
SPEED_SCALE="${TALK_SPEED_SCALE:-1.2}"
# メッセージ抽出
if command -v jq >/dev/null 2>&1; then
MESSAGE=$(printf '%s' "$JSON" | jq -r '."last-assistant-message" // .last_assistant_message // .message // ""')
else
MESSAGE=$(printf '%s' "$JSON" | grep -o '"last-assistant-message" *: *[^,}]*' | sed 's/.*"last-assistant-message" *: *"\?//' | sed 's/"\?$//' | tr '\n' ' ')
if [ -z "$MESSAGE" ]; then
MESSAGE=$(printf '%s' "$JSON" | grep -o '"last_assistant_message" *: *[^,}]*' | sed 's/.*"last_assistant_message" *: *"\?//' | sed 's/"\?$//' | tr '\n' ' ')
fi
fi
# 【完了要約】を優先し、なければ【途中報告】の最後のものを抽出
SUMMARY=$(printf '%s\n' "$MESSAGE" | awk '
{
pos = index($0, "【完了要約】")
if (pos > 0) {
complete_summary = substr($0, pos + length("【完了要約】"))
mode = "complete"
complete_found = 1
next
}
pos = index($0, "【途中報告】")
if (pos > 0) {
progress_summary = substr($0, pos + length("【途中報告】"))
mode = "progress"
progress_found = 1
next
}
if (mode == "complete") {
if (complete_summary == "") {
complete_summary = $0
} else {
complete_summary = complete_summary ORS $0
}
} else if (mode == "progress") {
if (progress_summary == "") {
progress_summary = $0
} else {
progress_summary = progress_summary ORS $0
}
}
}
END {
if (complete_found) {
print complete_summary
} else if (progress_found) {
print progress_summary
}
}
' | tr '\n' ' ' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
# 要約されなかった場合のフォールバック。個人的にいらないのでコメントアウト。
#if [ -z "$SUMMARY" ] || [ ${#SUMMARY} -lt 8 ]; then
# SUMMARY="Codexの作業が完了したのだ!"
#fi
if ! curl -fsS --connect-timeout 1 --max-time 2 "$VOICEVOX_URL/version" >/dev/null 2>&1; then
exit 0
fi
QUERY_FILE=$(mktemp /tmp/zunda_query.XXXXXX.json)
WAV_FILE=$(mktemp /tmp/zunda_temp.XXXXXX.wav)
cleanup() {
rm -f "$QUERY_FILE" "$WAV_FILE"
}
trap cleanup EXIT
# 1. audio_query取得
QUERY_JSON=$(curl -fsS --connect-timeout 2 --max-time 5 -X POST "$VOICEVOX_URL/audio_query?speaker=$SPEAKER_ID" \
--get --data-urlencode text="$SUMMARY")
# 2. speedScaleを設定値に変更
printf '%s' "$QUERY_JSON" | jq --argjson speed_scale "$SPEED_SCALE" '.speedScale = $speed_scale' > "$QUERY_FILE"
# 3. synthesis(設定値で合成)
curl -fsS --connect-timeout 2 --max-time 10 -H "Content-Type: application/json" \
-X POST -d @"$QUERY_FILE" "$VOICEVOX_URL/synthesis?speaker=$SPEAKER_ID" > "$WAV_FILE"
# 再生
afplay "$WAV_FILE"
cleanup
削除処理等も含んでいるので意図しない動作があった場合など、一切の責任は負いません。(レシピウェアでも負わんけど。)
