<?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/%E5%8B%95%E7%94%BB%E7%94%9F%E6%88%90/</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, 01 Jul 2026 05:31:24 +0900</lastBuildDate><atom:link href="https://blog.fuga.jp/tags/%E5%8B%95%E7%94%BB%E7%94%9F%E6%88%90/index.xml" rel="self" type="application/rss+xml"/><item><title>podcast-tool 開発日記 #4 — 動画対応の幕開け</title><link>https://blog.fuga.jp/posts/2026-07-01-podcast-tool-devdiary-04-video-dawn/</link><pubDate>Wed, 01 Jul 2026 05:31:24 +0900</pubDate><guid>https://blog.fuga.jp/posts/2026-07-01-podcast-tool-devdiary-04-video-dawn/</guid><description>&lt;p>2025年8月後半。前回（第3回）でキャッシュ基盤と複数エンジン対応が固まったその翌週、&lt;code>podcast-tool&lt;/code> に静かな転換点が訪れた。コミットハッシュ &lt;code>43fe3a1&lt;/code>——そこに &lt;code>video.py&lt;/code> というファイルが初めて登場する。220行のそのモジュールが、「音声を作るツール」を「音と映像を作るツール」へと変えていく起点になった。&lt;/p>
&lt;h2 id="動画生成機能の初日8月16日">&lt;a href="#%e5%8b%95%e7%94%bb%e7%94%9f%e6%88%90%e6%a9%9f%e8%83%bd%e3%81%ae%e5%88%9d%e6%97%a58%e6%9c%8816%e6%97%a5" class="header-anchor">&lt;/a>動画生成機能の初日（8月16日）
&lt;/h2>&lt;p>8月16日土曜日の午後1時、最初の動画生成機能が main に入った（&lt;code>43fe3a1&lt;/code>）。コミットメッセージには「プロトタイプ版（1セクション1画像）」という但し書きがある。&lt;/p>
&lt;p>この時点での仕様はシンプルだ。台本の各セクション（INTRO・MAIN1〜5・ENDING）ごとに1枚の静止画を割り当て、そのセクションの音声時間ぶんだけ画像を引き伸ばして動画化する。セクション動画を順に結合すれば、音声と同期したMP4が完成する——という「1セクション1画像」方式だ。&lt;/p>
&lt;p>技術的な選択はこうだった。&lt;/p>
&lt;ul>
&lt;li>解像度: 1080p&lt;/li>
&lt;li>フレームレート: 30fps&lt;/li>
&lt;li>コーデック: H.264&lt;/li>
&lt;li>画像処理: Pillow でリサイズ、アスペクト比をレターボックスで吸収&lt;/li>
&lt;li>動画生成: ffmpeg を subprocess 経由で呼び出す&lt;/li>
&lt;li>エントリポイント: &lt;code>--video&lt;/code> オプション追加（音声生成と同時実行）&lt;/li>
&lt;/ul>
&lt;p>新たに生まれた &lt;code>video.py&lt;/code> は、&lt;code>VideoConfig&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;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-python" data-lang="python">&lt;span style="display:flex;">&lt;span>@dataclass
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b6a0ff">class&lt;/span> VideoConfig:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#79a8ff">&amp;#34;&amp;#34;&amp;#34;動画生成設定&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> enabled: &lt;span style="color:#f78fe7">bool&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> resolution: &lt;span style="color:#f78fe7">str&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> framerate: &lt;span style="color:#f78fe7">int&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> codec: &lt;span style="color:#f78fe7">str&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> output_format: &lt;span style="color:#f78fe7">str&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>設定を型で束ねるのは、前回（第3回）から続く流儀だ。&lt;code>cache_utils.py&lt;/code> でも同じアプローチを取っていた。「動くコードを書く」だけでなく「あとで変えやすいコードを書く」という意識が、ここでも形になっている。&lt;/p>
&lt;p>同じコミットにはテストも276行分ついてくる（&lt;code>tests/test_video.py&lt;/code> + &lt;code>tests/test_main_video.py&lt;/code>）。7月に根づいた「TDDでいく」という習慣が、新領域にも最初から持ち込まれている。&lt;/p>
&lt;h2 id="同日14時台本からトランジションを読む35798c8">&lt;a href="#%e5%90%8c%e6%97%a514%e6%99%82%e5%8f%b0%e6%9c%ac%e3%81%8b%e3%82%89%e3%83%88%e3%83%a9%e3%83%b3%e3%82%b8%e3%82%b7%e3%83%a7%e3%83%b3%e3%82%92%e8%aa%ad%e3%82%8035798c8" class="header-anchor">&lt;/a>同日14時、台本からトランジションを読む（&lt;code>35798c8&lt;/code>）
&lt;/h2>&lt;p>最初の動画実装から約2時間後、もう一本のコミットが続く。&lt;code>35798c8&lt;/code>「mediaタグのtransition属性による切り替え効果機能を実装」——プロトタイプが入った当日に、早くも「複数mediaタグ対応は今後の拡張予定」を実装している。&lt;/p>
&lt;p>追加されたのは3つのトランジション効果だ。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>transition 値&lt;/th>
&lt;th>効果&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>fade&lt;/code>&lt;/td>
&lt;td>前の画像から次の画像へフェード&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>extend&lt;/code>&lt;/td>
&lt;td>前の画像を指定時間延長表示&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>cut&lt;/code>&lt;/td>
&lt;td>即座に切り替え（デフォルト）&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>台本の &lt;code>media&lt;/code> タグにこう書く。&lt;/p>
&lt;pre tabindex="0">&lt;code>[media:image.jpg transition=fade transition_duration=1.0]
&lt;/code>&lt;/pre>&lt;p>これを &lt;code>config_utils.py&lt;/code> の新関数 &lt;code>parse_media_tag()&lt;/code> が解析し、&lt;code>video.py&lt;/code> の &lt;code>create_fade_transition()&lt;/code> が実際のフェード映像を作る。台本の記述が画面上の動きに直結する——「書いた通りに動く」という感触がここで生まれた。&lt;/p>
&lt;p>同日には仕様書も整備された（&lt;code>5b9cbe5&lt;/code>）。&lt;code>SPECIFICATION.md&lt;/code> に「E7トランジション効果」として仕様が追記され、&lt;code>README.md&lt;/code> には使い方が説明される。機能とドキュメントを同日にそろえる癖がついている。&lt;/p>
&lt;h2 id="翌週の試行錯誤8月17日23日">&lt;a href="#%e7%bf%8c%e9%80%b1%e3%81%ae%e8%a9%a6%e8%a1%8c%e9%8c%af%e8%aa%a48%e6%9c%8817%e6%97%a523%e6%97%a5" class="header-anchor">&lt;/a>翌週の試行錯誤（8月17日〜23日）
&lt;/h2>&lt;p>初日に「動く」状態まで持ち込んだあとは、細部との戦いが続いた。&lt;/p>
&lt;p>&lt;strong>8/17&lt;/strong> には動画ファイルを背景に使う機能が WIP として入る（&lt;code>9756087&lt;/code>）。静止画だけでなく、動画クリップをループさせてセクション背景にするアイデアだ。&lt;/p>
&lt;p>&lt;strong>8/18&lt;/strong> はビルドシステムの整備回になる（&lt;code>1829bba&lt;/code>）。&lt;code>make/just&lt;/code> 両対応のテンプレートベースビルドシステムが実装され、&lt;code>justfile&lt;/code> に動画生成ターゲット &lt;code>video-m4a&lt;/code> が追加された（&lt;code>6a1d2bf&lt;/code>）。「コマンドひとつで音声+動画を両方作れる」という使い勝手が整い始める。同日 Linter 機能が一度追加されてすぐ Revert されるという一幕もあった（&lt;code>af00053&lt;/code> → &lt;code>e12767f&lt;/code>）。まだ準備が整っていなかったのだろう。&lt;/p>
&lt;p>&lt;strong>8/23&lt;/strong> にはフェード切り替えの画像サイズ問題が直される（&lt;code>0930043&lt;/code>）。解像度が異なる画像間でフェードをかけると映像が崩れる問題で、&lt;code>generate_section_video&lt;/code> 関数に前処理を統合して対応した（&lt;code>b4e8c27&lt;/code>）。「実際に動かして見つかるバグ」の典型だ。同日、超解像対応とH.264エンコーダーの修正も入っている（&lt;code>ea26c23&lt;/code>）——プロトタイプとしての動画から、品質への意識が芽生えてきた。&lt;/p>
&lt;h2 id="大きな整理8月25日">&lt;a href="#%e5%a4%a7%e3%81%8d%e3%81%aa%e6%95%b4%e7%90%868%e6%9c%8825%e6%97%a5" class="header-anchor">&lt;/a>大きな整理（8月25日）
&lt;/h2>&lt;p>8月25日は「整理の日」だった。&lt;/p>
&lt;p>午前中に &lt;code>PodcastApplicationクラス&lt;/code> が追加される（&lt;code>3643199&lt;/code>）。それまでグローバル変数として散らばっていた設定を、クラスに束ねて一元管理しようという大方針転換だ。&lt;/p>
&lt;pre tabindex="0">&lt;code>[refactor] PodcastApplicationクラスを追加してグローバル状態管理を改善
- PodcastApplicationクラスで設定管理を一元化
- 依存性注入パターンの基盤を構築
- グローバルAPP_CONFIG変数の段階的移行を開始
&lt;/code>&lt;/pre>&lt;p>「段階的移行」という言葉が示す通り、これは一度には終わらない。続く4本のコミットで &lt;code>main()&lt;/code> の書き換え、テストの修正、コマンドパス解決の移行が順番に実行された（&lt;code>33fb037&lt;/code>, &lt;code>9dcfb91&lt;/code>, &lt;code>badaee8&lt;/code>, &lt;code>959cea5&lt;/code>）。&lt;/p>
&lt;p>さらに同日、&lt;code>大規模関数分割&lt;/code> のリファクタリング（&lt;code>9d2397f&lt;/code>, &lt;code>3fa5a14&lt;/code>, &lt;code>03865c0&lt;/code>）が行われる。動画生成まわりの関数が肥大化していたのだろう。「Phase 2達成」という言葉が示す計画的な分割だ。&lt;/p>
&lt;p>一時 Revert されていた Linter 機能もここで改めて入る（&lt;code>a8500ba&lt;/code>）。プロジェクトデータの検証——config.json や台本の構造的な整合性を事前チェックする仕組みだ。動画が絡むと「設定ミスで動かない」パターンが増えるため、早期検出の価値が高くなる。&lt;/p>
&lt;p>午後には統一エラーハンドリングシステムも追加された（&lt;code>4d2c219&lt;/code>）。機能が増えれば、失敗時の情報も整理が要る。&lt;/p>
&lt;h2 id="背景動画の登場8月27日28日">&lt;a href="#%e8%83%8c%e6%99%af%e5%8b%95%e7%94%bb%e3%81%ae%e7%99%bb%e5%a0%b48%e6%9c%8827%e6%97%a528%e6%97%a5" class="header-anchor">&lt;/a>背景動画の登場（8月27日〜28日）
&lt;/h2>&lt;p>8月27日、静止画の下で動画をループさせる「背景動画」機能が形になる（&lt;code>df74d87&lt;/code>、&lt;code>a36cb6b&lt;/code>）。frame-based transitions というキーワードがコミットメッセージに現れる——セクションごとに画像を切り替えていた初期設計から、フレーム単位で映像を制御する方向へ進化している。&lt;/p>
&lt;p>同日、並列音声処理のデッドロック問題も修正された（&lt;code>af3483a&lt;/code>）。動画生成を組み込む前から複数スレッドで音声を処理していたが、そこに新しいコードが絡んで問題が顕在化したものと思われる。&lt;/p>
&lt;p>8月28日には背景動画のクロスフェード機能が WIP として追加される（&lt;code>6b9dc43&lt;/code>, &lt;code>6eb0076&lt;/code>）。「静止画を表示し続ける」から「動画が背景に流れる」へ——出力の質感が変わる一歩だ。&lt;/p>
&lt;h2 id="月末のセキュリティ対応8月31日9月1日">&lt;a href="#%e6%9c%88%e6%9c%ab%e3%81%ae%e3%82%bb%e3%82%ad%e3%83%a5%e3%83%aa%e3%83%86%e3%82%a3%e5%af%be%e5%bf%9c8%e6%9c%8831%e6%97%a59%e6%9c%881%e6%97%a5" class="header-anchor">&lt;/a>月末のセキュリティ対応（8月31日〜9月1日）
&lt;/h2>&lt;p>8月最終日には、GitHub Copilot 経由でセキュリティ改善 PR が入る（&lt;code>7553374&lt;/code>, PR #2）。URL バリデーションの修正（&lt;code>aa6c837&lt;/code>）——VOICEVOX エンジンの localhost 接続が弾かれていた問題だ。機能開発が進む中で見落とされがちなセキュリティ角度を、外部レビューが拾っている。&lt;/p>
&lt;h2 id="この期間でできたこと">&lt;a href="#%e3%81%93%e3%81%ae%e6%9c%9f%e9%96%93%e3%81%a7%e3%81%a7%e3%81%8d%e3%81%9f%e3%81%93%e3%81%a8" class="header-anchor">&lt;/a>この期間でできたこと
&lt;/h2>&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>video.py&lt;/code> 新設・&lt;code>--video&lt;/code> オプション追加（1セクション1画像）&lt;/td>
&lt;td>&lt;code>43fe3a1&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>media&lt;/code> タグの &lt;code>transition&lt;/code> 属性（fade/extend/cut）&lt;/td>
&lt;td>&lt;code>35798c8&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>justfile&lt;/code> への動画生成ターゲット追加&lt;/td>
&lt;td>&lt;code>6a1d2bf&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>テンプレートベースビルドシステム&lt;/td>
&lt;td>&lt;code>1829bba&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>PodcastApplication&lt;/code> クラスによる状態管理一元化&lt;/td>
&lt;td>&lt;code>3643199&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>大規模関数分割（Phase 2）とテスト整備&lt;/td>
&lt;td>&lt;code>03865c0&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Linter（プロジェクトデータ検証）本実装&lt;/td>
&lt;td>&lt;code>a8500ba&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>統一エラーハンドリング&lt;/td>
&lt;td>&lt;code>4d2c219&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>背景動画（静止画下でのループ再生）&lt;/td>
&lt;td>&lt;code>df74d87&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>背景動画クロスフェード（WIP）&lt;/td>
&lt;td>&lt;code>6b9dc43&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>セキュリティ改善 PR（Copilot）&lt;/td>
&lt;td>PR #2&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="転回点を振り返って">&lt;a href="#%e8%bb%a2%e5%9b%9e%e7%82%b9%e3%82%92%e6%8c%af%e3%82%8a%e8%bf%94%e3%81%a3%e3%81%a6" class="header-anchor">&lt;/a>転回点を振り返って
&lt;/h2>&lt;p>第3回まで &lt;code>podcast-tool&lt;/code> は「音声を作るツール」だった。8月16日を境に、それは「音と映像を作るツール」へと舵を切った。&lt;/p>
&lt;p>最初の &lt;code>video.py&lt;/code> は220行のシンプルなプロトタイプだ。それが2週間で、トランジション効果・背景動画・クロスフェード・Linter・大規模リファクタリングまで進んだ。速い。しかしそれは無理に詰め込んだ速さではなく、「動かしながら見えてきた問題を直す」という積み重ねの速さだ。&lt;/p>
&lt;p>&lt;code>PodcastApplication&lt;/code> クラスの導入は、その勢いの中でも「後で困らないよう整理する」という判断だった。機能が増えるほど、設計の負債が後で響く。それを感じ取って、追加と整理を交互に繰り返している。&lt;/p>
&lt;p>8月28日のコミット &lt;code>6eb0076&lt;/code> のタイトルは「WIP: 背景動画クロスフェード機能の文書化」——まだ作りかけだ。次回は9月以降、この WIP がどこへ向かうかを追う。&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>第5回は9月前半を追う予定だ。背景動画クロスフェードの完成、BGMイントロ・アウトロとの時間整合、そして YouTube Shorts 対応の字幕焼き込みへと展開していく。「音と映像」が揃ってきたあとの次の課題——&lt;strong>届け方の多様化&lt;/strong>——が見えてくる時期だ。&lt;/p>
&lt;hr>
&lt;p>&lt;em>この記事は &lt;code>podcast-tool&lt;/code> のコミット履歴を一次資料として書いています。引用したコミットハッシュ・時刻・コード構成は当時のリポジトリ状態に基づきます（時刻は JST 表記）。&lt;/em>&lt;/p></description></item></channel></rss>