<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>開発日記 on 思いつきそうで思いつかなくていたときに</title><link>https://blog.fuga.jp/tags/%E9%96%8B%E7%99%BA%E6%97%A5%E8%A8%98/</link><description>Recent content in 開発日記 on 思いつきそうで思いつかなくていたときに</description><generator>Hugo -- gohugo.io</generator><language>ja</language><copyright>Copyright(c) 2022-2025 SATO Daisuke. All rights reserved.</copyright><lastBuildDate>Wed, 17 Jun 2026 06:00:00 +0900</lastBuildDate><atom:link href="https://blog.fuga.jp/tags/%E9%96%8B%E7%99%BA%E6%97%A5%E8%A8%98/index.xml" rel="self" type="application/rss+xml"/><item><title>podcast-tool 開発日記 #1 誕生 — 音が出た日</title><link>https://blog.fuga.jp/posts/2026-06-17-podcast-tool-devdiary-01-birth/</link><pubDate>Wed, 17 Jun 2026 06:00:00 +0900</pubDate><guid>https://blog.fuga.jp/posts/2026-06-17-podcast-tool-devdiary-01-birth/</guid><description>&lt;h2 id="このシリーズについて">&lt;a href="#%e3%81%93%e3%81%ae%e3%82%b7%e3%83%aa%e3%83%bc%e3%82%ba%e3%81%ab%e3%81%a4%e3%81%84%e3%81%a6" class="header-anchor">&lt;/a>このシリーズについて
&lt;/h2>&lt;p>&lt;code>podcast-tool&lt;/code> という自作ツールがある。台本と話者定義、BGM設定を放り込むと、音声合成エンジンで読み上げて、BGMを混ぜて、最終的に動画付きの一本に仕上げてくれる——そういうものだ。気づけば1年近く、1000以上のコミットを積み重ねていた。&lt;/p>
&lt;p>この連載は、その積み重ねを &lt;strong>コミット履歴という一次資料&lt;/strong> から振り返る開発日記だ。「この頃、何を考えて、どう作っていたのか」を、当時のソースコードと差分を引きながら掘り起こしていく。扱うのはあくまでシステム・コードの話。ポッドキャストそのものの中身には踏み込まない。&lt;/p>
&lt;p>進め方の建て付けも書いておく。ネタを決めて一次資料を当たり、初稿を起こすところまでは自分の手で書く。そこから先のファクトチェックや校正は、ワークフロー化して AI に手伝ってもらっている。コミットハッシュや関数名がコードと食い違っていないか、日付の前後関係が正しいか——その手の照合は機械の得意分野だ。&lt;/p>
&lt;p>第1回は、すべての始まり。&lt;strong>最初のコミットが置かれた日&lt;/strong>を追う。&lt;/p>
&lt;h2 id="ゼロコミットの景色">&lt;a href="#%e3%82%bc%e3%83%ad%e3%82%b3%e3%83%9f%e3%83%83%e3%83%88%e3%81%ae%e6%99%af%e8%89%b2" class="header-anchor">&lt;/a>ゼロコミットの景色
&lt;/h2>&lt;p>リポジトリの最初のコミットは、2025年7月11日の朝10時18分。中身は &lt;code>pyproject.toml&lt;/code> がたった一つあるだけだった。&lt;code>src/podcast_tool&lt;/code> を &lt;code>uv&lt;/code> のパッケージとして登録する、それだけのコミットだ。&lt;/p>
&lt;div class="highlight">&lt;div style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;">&lt;tr>&lt;td style="vertical-align:top;padding:0;margin:0;border:0;">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>856373a 2025-07-11 10:18 [feat] pyproject.toml: Add src/podcast_tool to uv.packages
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>ツールでもなんでもない。まだ「箱」を用意しただけ。だがこの日の午後、この箱は一気に中身で満たされることになる。&lt;/p>
&lt;h2 id="26分の怒涛">&lt;a href="#26%e5%88%86%e3%81%ae%e6%80%92%e6%b6%9b" class="header-anchor">&lt;/a>26分の怒涛
&lt;/h2>&lt;p>午後4時25分、手が動き出す。ここからのコミットログを時刻つきで並べてみる。&lt;/p>
&lt;div class="highlight">&lt;div style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;">&lt;tr>&lt;td style="vertical-align:top;padding:0;margin:0;border:0;">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>16:25 548b080 プロジェクトの初期セットアップとCLIの基本構造を構築
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>16:29 28efd5a コマンドライン引数の追加と typer の動作確認
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>16:30 b5094d2 uv run時の typer 引数解析問題の調査と回避策の確認
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>16:31 c50f32d speakers.json と replace.csv の読み込み機能を追加
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>16:32 219ece2 script.txt と bgm_config.json の読み込み機能を追加
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>16:33 7dce293 Voicevoxエンジンとの連携と音声生成の並列処理を実装
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>16:46 e737f1b 音声ミキシングとffmpegによるm4a出力機能を追加
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>16:51 273f021 typer→argparse へ移行し、uv runでの引数処理を修正
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>26分&lt;/strong>だ。CLIの骨格を組み、設定ファイルを読み込めるようにし、VOICEVOX で音声を合成し、それを混ぜて &lt;code>m4a&lt;/code> で書き出す——「ツールとして動く」最小形が、この短い時間で立ち上がっている。16時46分のコミット、&lt;code>音声ミキシングとffmpegによるm4a出力機能を追加&lt;/code> が、事実上の「&lt;strong>音が出た瞬間&lt;/strong>」だった。&lt;/p>
&lt;p>勢いがそのままログに残っているのが面白い。1分刻みでコミットが積まれ、設計を頭の中で組み上げながら手を動かしていた様子がうかがえる。&lt;/p>
&lt;h2 id="typer-をやめて-argparse-にした話">&lt;a href="#typer-%e3%82%92%e3%82%84%e3%82%81%e3%81%a6-argparse-%e3%81%ab%e3%81%97%e3%81%9f%e8%a9%b1" class="header-anchor">&lt;/a>typer をやめて argparse にした話
&lt;/h2>&lt;p>この日いちばん「生々しい」のは、16:29 から16:51 にかけての出来事だ。&lt;/p>
&lt;p>最初はCLIフレームワークに &lt;a class="link" href="https://typer.tiangolo.com/" target="_blank" rel="noopener"
>typer&lt;/a> を使おうとしていた（16:29）。ところが直後の16:30、&lt;code>uv run&lt;/code> で実行したときに引数解析がうまくいかない問題にぶつかる。コミットメッセージにはっきり「調査と回避策の確認」と書いてある。そして16:51、最終的に標準ライブラリの &lt;code>argparse&lt;/code> へ乗り換えた。&lt;/p>
&lt;p>華やかなライブラリより、&lt;code>uv run&lt;/code> という実行環境と素直に噛み合うほうを選ぶ。30分足らずで下したこの判断は、その後のツール全体の地に足のついた作りを象徴しているように思う。&lt;/p>
&lt;h2 id="最初からデータ駆動だった">&lt;a href="#%e6%9c%80%e5%88%9d%e3%81%8b%e3%82%89%e3%83%87%e3%83%bc%e3%82%bf%e9%a7%86%e5%8b%95%e3%81%a0%e3%81%a3%e3%81%9f" class="header-anchor">&lt;/a>最初から「データ駆動」だった
&lt;/h2>&lt;p>誕生時点（16:33 のコミット）のファイル構成を見ると、設計の骨格がもう見える。&lt;/p>
&lt;div class="highlight">&lt;div style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;">&lt;tr>&lt;td style="vertical-align:top;padding:0;margin:0;border:0;">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>script.txt # 台本
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>speakers.json # 話者定義
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>replace.csv # 読み替え辞書
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>bgm_config.json # BGM設定
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>src/podcast_tool/main.py
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>入力は &lt;strong>4つの設定ファイル&lt;/strong>、出力は一本の音声。ロジックではなくデータで振る舞いを決める、という発想が初日から入っていた。台本・話者・読み替え・BGM をそれぞれ独立したファイルに分けたこの形は、このあと長く土台であり続ける。&lt;/p>
&lt;h2 id="295行ぜんぶ入りの-mainpy">&lt;a href="#295%e8%a1%8c%e3%81%9c%e3%82%93%e3%81%b6%e5%85%a5%e3%82%8a%e3%81%ae-mainpy" class="header-anchor">&lt;/a>295行、ぜんぶ入りの &lt;code>main.py&lt;/code>
&lt;/h2>&lt;p>中身の &lt;code>main.py&lt;/code> は、この時点で &lt;strong>単一ファイル・295行&lt;/strong>。関数を並べてみるとこうなっている。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>関数&lt;/th>
&lt;th>役割&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>load_speakers()&lt;/code> / &lt;code>load_replace_dict()&lt;/code> / &lt;code>load_script()&lt;/code> / &lt;code>load_bgm_config()&lt;/code>&lt;/td>
&lt;td>4つの設定ファイルの読み込み&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>generate_voice(text, speaker_id, replace_dict)&lt;/code>&lt;/td>
&lt;td>1セリフ → 音声（VOICEVOX へHTTP）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>mix_audio(voice_paths, bgm_config, output_file)&lt;/code>&lt;/td>
&lt;td>音声群とBGMをミックス&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>main(...)&lt;/code>&lt;/td>
&lt;td>並列で合成 → ミックス → ffmpeg で m4a 化&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>依存ライブラリは &lt;code>httpx&lt;/code>（エンジンへのHTTP）、&lt;code>concurrent.futures&lt;/code>（並列合成）、&lt;code>pedalboard&lt;/code> と &lt;code>numpy&lt;/code>（音響処理）、そして &lt;code>subprocess&lt;/code> 経由の &lt;code>ffmpeg&lt;/code>。注目したいのは、&lt;strong>初日から &lt;code>ThreadPoolExecutor&lt;/code> で音声合成を並列化している&lt;/strong>ことだ。セリフが増えても待たされにくいように、という速度への意識が出発点からあった。&lt;/p>
&lt;p>いわゆる「全部入りの一枚岩」。今読むと分割したくなる作りだが、まずは動くものを一本のファイルで通しきる、というのは立ち上げ期の正しい姿だと思う。この295行が、のちの大規模なリファクタリング（core / models / audio / video への分割）の出発点になる——その話はずっと先の回で。&lt;/p>
&lt;h2 id="この日できたことできなかったこと">&lt;a href="#%e3%81%93%e3%81%ae%e6%97%a5%e3%81%a7%e3%81%8d%e3%81%9f%e3%81%93%e3%81%a8%e3%81%a7%e3%81%8d%e3%81%aa%e3%81%8b%e3%81%a3%e3%81%9f%e3%81%93%e3%81%a8" class="header-anchor">&lt;/a>この日、できたこと・できなかったこと
&lt;/h2>&lt;p>誕生時点の README は、自分のツールをこう紹介している。&lt;/p>
&lt;blockquote>
&lt;p>台本、話者定義、BGM設定などから、VOICEVOXを利用してポッドキャストを自動生成するPythonツール&lt;/p>&lt;/blockquote>
&lt;p>前提は Python 3.12 以上、&lt;code>uv&lt;/code>、ローカルで動く VOICEVOX Engine。出力は当初「1つのWAVファイル」とあり、&lt;code>m4a&lt;/code> 対応はこの直後に入ったのが分かる。&lt;/p>
&lt;p>できたこと: 台本から音声を合成し、BGMと混ぜ、ファイルに書き出す。一連の流れがこの日のうちに通った。&lt;/p>
&lt;p>できなかったこと: まだ単一エンジン専用で、キャッシュもなく、当然ながら動画も字幕もない。複数エンジン対応も、超解像も、YouTube Shorts も、AIによる抑揚付けも——いま README に並ぶ機能たちは、すべてこの先の積み重ねの結果だ。&lt;/p>
&lt;h2 id="次回予告">&lt;a href="#%e6%ac%a1%e5%9b%9e%e4%ba%88%e5%91%8a" class="header-anchor">&lt;/a>次回予告
&lt;/h2>&lt;p>第2回は、誕生の翌日から続く「&lt;strong>音を整える&lt;/strong>」期間を追う。話者ごとの速度調整、BGMのクロスフェードとループ、セクションの切れ目に入れる無音、動的な音量調整……「とりあえず音が出る」から「聴けるものにする」へ。地味だが効く改良が並んだ数日間だ。&lt;/p>
&lt;hr>
&lt;p>&lt;em>この記事は &lt;code>podcast-tool&lt;/code> のコミット履歴を一次資料として書いています。引用したコミットハッシュ・時刻・コード構成は当時のリポジトリ状態に基づきます。&lt;/em>&lt;/p></description></item></channel></rss>