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

podcast-tool 開発日記 #1 誕生 — 音が出た日

このシリーズについて

podcast-tool という自作ツールがある。台本と話者定義、BGM設定を放り込むと、音声合成エンジンで読み上げて、BGMを混ぜて、最終的に動画付きの一本に仕上げてくれる——そういうものだ。気づけば1年近く、1000以上のコミットを積み重ねていた。

この連載は、その積み重ねを コミット履歴という一次資料 から振り返る開発日記だ。「この頃、何を考えて、どう作っていたのか」を、当時のソースコードと差分を引きながら掘り起こしていく。扱うのはあくまでシステム・コードの話。ポッドキャストそのものの中身には踏み込まない。

進め方の建て付けも書いておく。ネタを決めて一次資料を当たり、初稿を起こすところまでは自分の手で書く。そこから先のファクトチェックや校正は、ワークフロー化して AI に手伝ってもらっている。コミットハッシュや関数名がコードと食い違っていないか、日付の前後関係が正しいか——その手の照合は機械の得意分野だ。

第1回は、すべての始まり。最初のコミットが置かれた日を追う。

ゼロコミットの景色

リポジトリの最初のコミットは、2025年7月11日の朝10時18分。中身は pyproject.toml がたった一つあるだけだった。src/podcast_tooluv のパッケージとして登録する、それだけのコミットだ。

1
856373a  2025-07-11 10:18  [feat] pyproject.toml: Add src/podcast_tool to uv.packages

ツールでもなんでもない。まだ「箱」を用意しただけ。だがこの日の午後、この箱は一気に中身で満たされることになる。

26分の怒涛

午後4時25分、手が動き出す。ここからのコミットログを時刻つきで並べてみる。

1
2
3
4
5
6
7
8
16:25  548b080  プロジェクトの初期セットアップとCLIの基本構造を構築
16:29  28efd5a  コマンドライン引数の追加と typer の動作確認
16:30  b5094d2  uv run時の typer 引数解析問題の調査と回避策の確認
16:31  c50f32d  speakers.json と replace.csv の読み込み機能を追加
16:32  219ece2  script.txt と bgm_config.json の読み込み機能を追加
16:33  7dce293  Voicevoxエンジンとの連携と音声生成の並列処理を実装
16:46  e737f1b  音声ミキシングとffmpegによるm4a出力機能を追加
16:51  273f021  typer→argparse へ移行し、uv runでの引数処理を修正

26分だ。CLIの骨格を組み、設定ファイルを読み込めるようにし、VOICEVOX で音声を合成し、それを混ぜて m4a で書き出す——「ツールとして動く」最小形が、この短い時間で立ち上がっている。16時46分のコミット、音声ミキシングとffmpegによるm4a出力機能を追加 が、事実上の「音が出た瞬間」だった。

勢いがそのままログに残っているのが面白い。1分刻みでコミットが積まれ、設計を頭の中で組み上げながら手を動かしていた様子がうかがえる。

typer をやめて argparse にした話

この日いちばん「生々しい」のは、16:29 から16:51 にかけての出来事だ。

最初はCLIフレームワークに typer を使おうとしていた(16:29)。ところが直後の16:30、uv run で実行したときに引数解析がうまくいかない問題にぶつかる。コミットメッセージにはっきり「調査と回避策の確認」と書いてある。そして16:51、最終的に標準ライブラリの argparse へ乗り換えた。

華やかなライブラリより、uv run という実行環境と素直に噛み合うほうを選ぶ。30分足らずで下したこの判断は、その後のツール全体の地に足のついた作りを象徴しているように思う。

最初から「データ駆動」だった

誕生時点(16:33 のコミット)のファイル構成を見ると、設計の骨格がもう見える。

1
2
3
4
5
script.txt          # 台本
speakers.json       # 話者定義
replace.csv         # 読み替え辞書
bgm_config.json     # BGM設定
src/podcast_tool/main.py

入力は 4つの設定ファイル、出力は一本の音声。ロジックではなくデータで振る舞いを決める、という発想が初日から入っていた。台本・話者・読み替え・BGM をそれぞれ独立したファイルに分けたこの形は、このあと長く土台であり続ける。

295行、ぜんぶ入りの main.py

中身の main.py は、この時点で 単一ファイル・295行。関数を並べてみるとこうなっている。

関数役割
load_speakers() / load_replace_dict() / load_script() / load_bgm_config()4つの設定ファイルの読み込み
generate_voice(text, speaker_id, replace_dict)1セリフ → 音声(VOICEVOX へHTTP)
mix_audio(voice_paths, bgm_config, output_file)音声群とBGMをミックス
main(...)並列で合成 → ミックス → ffmpeg で m4a 化

依存ライブラリは httpx(エンジンへのHTTP)、concurrent.futures(並列合成)、pedalboardnumpy(音響処理)、そして subprocess 経由の ffmpeg。注目したいのは、初日から ThreadPoolExecutor で音声合成を並列化していることだ。セリフが増えても待たされにくいように、という速度への意識が出発点からあった。

いわゆる「全部入りの一枚岩」。今読むと分割したくなる作りだが、まずは動くものを一本のファイルで通しきる、というのは立ち上げ期の正しい姿だと思う。この295行が、のちの大規模なリファクタリング(core / models / audio / video への分割)の出発点になる——その話はずっと先の回で。

この日、できたこと・できなかったこと

誕生時点の README は、自分のツールをこう紹介している。

台本、話者定義、BGM設定などから、VOICEVOXを利用してポッドキャストを自動生成するPythonツール

前提は Python 3.12 以上、uv、ローカルで動く VOICEVOX Engine。出力は当初「1つのWAVファイル」とあり、m4a 対応はこの直後に入ったのが分かる。

できたこと: 台本から音声を合成し、BGMと混ぜ、ファイルに書き出す。一連の流れがこの日のうちに通った。

できなかったこと: まだ単一エンジン専用で、キャッシュもなく、当然ながら動画も字幕もない。複数エンジン対応も、超解像も、YouTube Shorts も、AIによる抑揚付けも——いま README に並ぶ機能たちは、すべてこの先の積み重ねの結果だ。

次回予告

第2回は、誕生の翌日から続く「音を整える」期間を追う。話者ごとの速度調整、BGMのクロスフェードとループ、セクションの切れ目に入れる無音、動的な音量調整……「とりあえず音が出る」から「聴けるものにする」へ。地味だが効く改良が並んだ数日間だ。


この記事は podcast-tool のコミット履歴を一次資料として書いています。引用したコミットハッシュ・時刻・コード構成は当時のリポジトリ状態に基づきます。

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