<?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%AD%97%E5%B9%95/</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, 08 Jul 2026 05:00:00 +0900</lastBuildDate><atom:link href="https://blog.fuga.jp/tags/%E5%AD%97%E5%B9%95/index.xml" rel="self" type="application/rss+xml"/><item><title>podcast-tool 開発日記 #5 — 同期と字幕の戦い</title><link>https://blog.fuga.jp/posts/2026-07-08-podcast-tool-devdiary-05-sync-and-subtitles/</link><pubDate>Wed, 08 Jul 2026 05:00:00 +0900</pubDate><guid>https://blog.fuga.jp/posts/2026-07-08-podcast-tool-devdiary-05-sync-and-subtitles/</guid><description>&lt;p>2025年9月前半。第4回で「音声+動画の基礎が整った」と書いたが、正確には「動いているように見えた」が正しかった。動画の長さが音声と合っていない。BGMのイントロが終わってから声が入るはずなのに、動画はその時間を無視して進む。9月9日から14日にかけての1週間は、その「見えていたズレ」を直す格闘だった。&lt;/p>
&lt;h2 id="タイムコードは既にあった">&lt;a href="#%e3%82%bf%e3%82%a4%e3%83%a0%e3%82%b3%e3%83%bc%e3%83%89%e3%81%af%e6%97%a2%e3%81%ab%e3%81%82%e3%81%a3%e3%81%9f" class="header-anchor">&lt;/a>タイムコードは既にあった
&lt;/h2>&lt;p>実は 9月14日以前から、動画にはタイムコードが焼き付けられていた。&lt;code>video.py&lt;/code> に FFmpeg の &lt;code>drawtext&lt;/code> フィルターが組み込まれており、左上に &lt;code>HH:MM:SS,mmm&lt;/code> 形式の現在時刻が表示される仕組みだ。デバッグ用として入れていたこれが、後に「本当の同期ズレがどのくらいか」を目で確認する手段になった。&lt;/p>
&lt;p>問題は数値として現れていた。動画 &lt;code>test.mp4&lt;/code> の再生時間が &lt;strong>26.0秒&lt;/strong>、期待値は &lt;strong>31.99秒&lt;/strong>——約6秒の不足。セクション間のギャップ1秒が3回分（計3秒）動画に反映されておらず、BGMのイントロ・アウトロ時間も計算から抜け落ちていた。&lt;/p>
&lt;h2 id="timing-memomd-と格闘の設計図">&lt;a href="#timing-memomd-%e3%81%a8%e6%a0%bc%e9%97%98%e3%81%ae%e8%a8%ad%e8%a8%88%e5%9b%b3" class="header-anchor">&lt;/a>timing-memo.md と格闘の設計図
&lt;/h2>&lt;p>9月9日、&lt;code>timing-memo.md&lt;/code> というファイルが復元された（&lt;code>14d292c&lt;/code>）。タイムライン計算の考え方を整理したメモで、こういう内容が書いてある。&lt;/p>
&lt;p>BGMには &lt;code>intro_volume_up_duration&lt;/code>（声が入る前のBGMのみ時間）と &lt;code>outro_volume_down_duration&lt;/code>（声が終わった後のBGMのみ時間）という2つのパラメータがある。台詞の長さに加えて、これら前後の「BGM単体時間」も動画の長さに含まれなければならない。セクション間のギャップも同様だ。&lt;/p>
&lt;pre tabindex="0">&lt;code>セクション長 = intro時間 + 台詞合計時間 + outro時間
全体長 = セクション1長 + ギャップ1秒 + セクション2長 + ...
&lt;/code>&lt;/pre>&lt;p>原理はシンプルだ。ただし、これを「音声の合成フェーズ」「BGMのミックスフェーズ」「動画の生成フェーズ」という複数の処理段階を経た後でも正確に維持するのが難しかった。9月10日のコミット &lt;code>70b7acd&lt;/code> では「階層的タイミング管理システム」の設計が完了し、実装開始前のスナップショットとして記録が残っている。コミットメッセージに書かれた問題分析がそのまま設計書になっていた。&lt;/p>
&lt;h2 id="9月11日の修正ループ">&lt;a href="#9%e6%9c%8811%e6%97%a5%e3%81%ae%e4%bf%ae%e6%ad%a3%e3%83%ab%e3%83%bc%e3%83%97" class="header-anchor">&lt;/a>9月11日の修正ループ
&lt;/h2>&lt;p>朝7時16分、&lt;code>d7c99ca&lt;/code>「動画生成処理の修正前（現状のコミット）」——これは「これから直す前の状態を保存した」という意味のコミットだ。&lt;/p>
&lt;p>そこから約1時間、修正が連続で入る。&lt;/p>
&lt;pre tabindex="0">&lt;code>07:19 eab0e14 セクション長計算を修正
07:37 0c84912 動画生成プロセスにデバッグログを追加
07:42 965c334 ギャップ動画の挿入を無効化、デバッグログを再追加
07:45 085da25 セクション動画の結合時に1秒のクロスフェードを適用
07:50 0da668d セクション動画生成時の不要な時間加算を削除
07:51 f706431 ログ出力の&amp;#39;crossfade_overlap&amp;#39;未定義エラーを修正
&lt;/code>&lt;/pre>&lt;p>ひとつ直すと別の問題が出る、という典型的なデバッグの連鎖だ。「ギャップ動画の挿入を無効化」というコミットがあることから、ギャップの扱い方自体を一度リセットして別アプローチに切り替えたのが読み取れる。&lt;/p>
&lt;p>そして午前11時26分、&lt;code>c80728e&lt;/code>「BGM intro/outro機能完成と完全同期達成」。コミットメッセージのテスト結果がそのまま残っていた。&lt;/p>
&lt;pre tabindex="0">&lt;code>- intro単体 (main4: 3s) → 9.023265s完全同期
- outro単体 (ending: 8s) → 13.491950s完全同期
- intro+outro (self_intro: 2s+2s) → 15.504489s完全同期
- 0.6ms精度の音声・動画同期を達成
&lt;/code>&lt;/pre>&lt;p>&lt;strong>0.6ミリ秒&lt;/strong>。この数字が出たとき、長かった午前中の格闘が終わった。&lt;/p>
&lt;h2 id="字幕という新しい軸">&lt;a href="#%e5%ad%97%e5%b9%95%e3%81%a8%e3%81%84%e3%81%86%e6%96%b0%e3%81%97%e3%81%84%e8%bb%b8" class="header-anchor">&lt;/a>字幕という新しい軸
&lt;/h2>&lt;p>同期が取れたその3日後、今度は字幕に手が伸びた。&lt;/p>
&lt;p>9月14日昼、&lt;code>54fac99&lt;/code>「字幕焼き込み機能の実装（WIP）」。実装メモを見ると、当日の調査結果として「タイムコード表示は &lt;code>video.py&lt;/code> に実装済み」「SRTファイル生成と焼き込みは未実装」と書いてある。あるものと無いものの棚卸しから始める、いつものスタイルだ。&lt;/p>
&lt;p>新しく &lt;code>subtitle_generator.py&lt;/code> が生まれた。中心は &lt;code>SubtitleGenerator&lt;/code> クラスと &lt;code>SubtitleEntry&lt;/code> データクラス——タイムラインの &lt;code>voice_segment&lt;/code> イベントを読み取り、SRT形式（&lt;code>HH:MM:SS,mmm --&amp;gt; HH:MM:SS,mmm&lt;/code>）に変換する。FFmpeg の &lt;code>subtitles&lt;/code> フィルターで動画に焼き込む部分は &lt;code>video.py&lt;/code> 側に追加された。既存の &lt;code>drawtext&lt;/code> タイムコードとの共存も考慮した実装になっている。&lt;/p>
&lt;p>夕方19時33分、&lt;code>e0f7ba5&lt;/code>「YouTube Shorts字幕焼き込み機能実装」でテスト完了が確認された。&lt;/p>
&lt;h2 id="bgmオフセットという落とし穴">&lt;a href="#bgm%e3%82%aa%e3%83%95%e3%82%bb%e3%83%83%e3%83%88%e3%81%a8%e3%81%84%e3%81%86%e8%90%bd%e3%81%a8%e3%81%97%e7%a9%b4" class="header-anchor">&lt;/a>BGMオフセットという落とし穴
&lt;/h2>&lt;p>ただし、そこで終わらなかった。&lt;/p>
&lt;p>動作確認してみると、字幕の表示タイミングが全体的に &lt;strong>3秒早い&lt;/strong>。「皆さん、こんにちは」という最初の台詞が &lt;code>00:00:03,592&lt;/code> から始まるはずが、&lt;code>00:00:00,592&lt;/code> 頃に出てしまう。&lt;/p>
&lt;p>原因はBGMのイントロだった。音声ファイルには先頭3秒のBGMイントロが含まれており、実際の台詞はその分遅れて始まる。ところが &lt;code>SubtitleGenerator&lt;/code> は &lt;code>Timeline&lt;/code> の &lt;code>voice_segment&lt;/code> の開始時刻をそのまま使っていた——BGMイントロのぶんがタイムライン上で考慮されていなかった。&lt;/p>
&lt;p>修正コミット &lt;code>f608294&lt;/code>「BGMイントロ時間を考慮した字幕タイミング修正」では、&lt;code>main.py&lt;/code> で &lt;code>bgm_config&lt;/code> から &lt;code>intro_volume_up_duration&lt;/code> を読み取り、&lt;code>TimelineGenerator.add_voice_segment&lt;/code> に &lt;code>bgm_intro_offset&lt;/code> パラメータとして渡す形に変えた。これで字幕開始が &lt;code>3.592秒 → 6.592秒&lt;/code> に修正され、音声と画面が一致した。&lt;/p>
&lt;p>BGMのintro/outroは、音声ミキシングだけでなく字幕のタイミングにも影響する。ひとつの設定値が複数の処理系にまたがって意味を持つ、という複雑さがここで顔を出した。&lt;/p>
&lt;h2 id="この週で積み上がったもの">&lt;a href="#%e3%81%93%e3%81%ae%e9%80%b1%e3%81%a7%e7%a9%8d%e3%81%bf%e4%b8%8a%e3%81%8c%e3%81%a3%e3%81%9f%e3%82%82%e3%81%ae" class="header-anchor">&lt;/a>この週で積み上がったもの
&lt;/h2>&lt;p>9月9日から15日の7日間で、3つのことが整った。BGMのイントロ・アウトロを含めた&lt;strong>動画の完全同期&lt;/strong>（0.6ms精度）、&lt;strong>SRT字幕ファイルの生成&lt;/strong>と動画への焼き込み、そして字幕タイミングへの&lt;strong>BGMオフセット反映&lt;/strong>。どれも「ある機能を追加したら別の場所がずれた」という連鎖の中で生まれている。&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>第6回は、同じ9月の中盤へ進む。字幕がSRTの単色テキストから &lt;strong>ASS形式&lt;/strong> へと進化し、話者ごとに色やフォントサイズを変えるスタイリング機能が登場する。セリフを読まなくても「誰が話しているか」が画面で分かる——そういう表現力が加わっていく。&lt;/p>
&lt;hr>
&lt;p>&lt;em>この記事は &lt;code>podcast-tool&lt;/code> のコミット履歴を一次資料として書いています。引用したコミットハッシュ・時刻・コード構成は当時のリポジトリ状態に基づきます。&lt;/em>&lt;/p></description></item></channel></rss>