このサイトでの挿絵は、Gemini Proによる生成が大半です。 また、情報収集・まとめなどのサイトについては、検索結果をベースとしたものを使用しています。可能な限り出典を明示するよう努めますが完全ではありません。各自で調べる事もお忘れなく。

podcast-tool 開発日記 #10 — 抑揚への探求

前回(第9回)は、81日間の沈黙から1月の「様子見の一往復」を経て、2月にまた0件に戻るという話だった。休眠と復活を繰り返しながらも、cast形式統合や禁則処理の改善は着実に積み上がっていた。今回はその少し先、2026年4月に開いた新しい取り組みを追う。VOICEVOXの**抑揚(イントネーション)**そのものに手を入れるための仕組みだ。

「読み上げてみないと分からない」問題

VOICEVOXのようなエンジンにテキストを渡すと、音声合成のリクエストの裏側では audio_query というJSONが一度組み立てられる。アクセント句の区切りやモーラごとのピッチ、話速——抑揚を決めるパラメータ一式がここに入っている。これまでの podcast-tool は、このJSONを毎回その場で作って、そのまま音声合成に投げていた。つまり「読み上げてみないと、イントネーションがおかしいかどうか分からない」。気になる抑揚があっても、確認する手段は完成した音声を聞く以外になかった。

4月26日、この構造に楔を打つコミットが並ぶ。狙いはシンプルで、audio_queryを生成・保存する工程と、それを使って音声合成する工程を分離すること。事前にJSONを書き出してレビューし、必要なら手で直してから合成できるようにする。

テストから書く、という選択

面白いのは着手の順番だ。朝12時03分、4341894「query_pipeline テストを追加(RED)」でまず433行のテストファイルが入る。実装はまだ存在しない。2分後の12時05分、9edd985src/podcast_tool/audio/query_pipeline.py(238行)が実装として追加される。normalize_tts_text(改行や置換辞書の正規化)、classify_utterance(発話をintro/technical_list/defaultに分類)、build_query_idsave_query/load_query_for_itemload_manifest/save_manifestといった純粋関数群が並ぶ、クエリ操作専用のモジュールだ。同じ分で cache_utils.py にAudioQuery用のキャッシュキー関数(a3c32cc)、続けて sample/justfile にAudioQuery関連コマンドのテンプレート(e935678)が入る。テスト→実装→周辺整備までを1時間弱でひとまとまりにしている。

Phase2・Phase3、そして同日中の2つのバグ

同日16時39分、bbcfb9c「–prepare-audio-queries CLI を追加(Phase2)」。台本を読み込み、話者ごとにaudio_queryを生成して queries/ に保存するだけで、音声合成までは進まないコマンドだ。2分後の16時41分、ccadad7「–use-audio-queries CLI を追加(Phase3)」。保存済みのマニフェストとJSONを読み込み、それを使って合成する側の経路が入る。

ただ、Phase2・3を追加した直後の17時台に、立て続けに2つの修正が入っているのが実態を物語っている。17時01分の f500c78 は、「--prepare-audio-queries を指定したのに音声合成まで走ってしまう」というPhase2側の不具合修正。podcast_orchestrator.py に専用の分岐が抜けていて、既存の合成フローに素通りしていたらしい。17時10分の 74d2f4b は逆にPhase3側で、マニフェストのキー名が実装と食い違っていた(speakerspeaker_nametextprocessed_textquery_idid)ことに加え、main.pyAPP_CONFIG.clear() した後に _runtime フラグが消えてしまう問題の修正。新設したパイプラインを両端(生成側・消費側)から通しで動かしてみて、初めて噛み合わせのズレが見えてきた、という順番だ。

運用性向上とパストラバーサル修正が同じ夜に並ぶ

翌27日19時15分、208501e「AudioQueryパイプラインの運用性向上」。--audio-query-id で特定のクエリだけを部分再生成できるようにし、check_audio_queries_fresh という整合性チェックを追加した。台本やreplace辞書が変わったのに古いAudioQuery JSONを使い回してしまう事故を防ぐための仕組みだ。設定ファイル側でも prepare_if_missingmode から use_audio_queries を立てられるようにして、CLIオプションだけに頼らない運用を意識している。

その7分後、19時22分に f5b4f96「AudioQueryパストラバーサル脆弱性を修正」が入る。audio_query.directory にプロジェクトディレクトリ外を指す値を渡せてしまう問題と、マニフェストの query_path に絶対パスを書けば任意のファイルを読み込めてしまう問題——Path の「右辺が絶対パスなら左辺を無視する」という挙動を悪用できる典型的な穴だ。commands.pylinter/checks.py の両方に、解決後のパスがプロジェクトディレクトリの配下にあるかを検証する処理を足している。運用性を上げるためにパスまわりの自由度を広げた直後に、その自由度が攻撃面にもなり得ることに気づいて塞ぐ——同じ夜のうちにこの順序で入っているのが、地味だが誠実な仕事に見えた。

19時29分の 9fe0bed でドキュメントを追加し、この日の作業は締めくくられる。

見えてくるもの

第10回で扱った一連のコミットは、機能追加そのものよりも「作った直後に自分で通しで使ってみて、ズレとリスクの両方を拾っていく」というリズムが目立った。RED→実装→周辺整備で土台を組み、Phase2・3で両端を繋ぎ、同日中にバグを2つ潰し、翌日には運用性とセキュリティを同時に見直す。VOICEVOXの抑揚という、聞いてみないと分からない曖昧な対象に対して、JSONという確認可能な中間表現を挟み込んだこと自体が、この回のいちばんの発明だったように思う。

なお、来週はお盆のため一週間スキップします。次回(第11回)は8月19日の更新です。

Hugo で構築されています。
テーマ StackJimmy によって設計されています。