「本当に動いてます?」と3回聞いた17枚の画像を1本の動画に繋ぐ夜
歴史解説の動画を、脚本から音声・画像・動画まで全部自動で作るパイプラインを個人で回しています。 ある日まで、背景は1枚の絵をズームし続けるだけでした。 2分半、同じ絵。さすがに単調です。
そこで「1文ごとに別の絵に切り替える」に作り替えました。 17文の原稿なら、17枚の絵。 言うのは一瞬ですが、ここから地味な落とし穴を2つ踏みます。 どちらも「一発で終わると思ってた処理が、全然終わらない」系です。
16GBのMacは、17枚の絵を一気に描けない
画像はローカルのFLUX(オープンな画像生成モデル)で作ります。 1枚1024x576、4ステップ、量子化3bit。 これを16GBのMacで動かすと、1枚でメモリをごっそり持っていく。 だから絶対に並列で回してはいけない。1枚ずつ、前のプロセスが完全に死んでから次、が鉄則です。
生成スクリプトには「空きメモリが50%未満なら実行を拒否する」門番が付いています。 最初、これに弾かれました。
事前チェック: 空きメモリ 35%(必要 50%以上)
✋ 空きが少なすぎ。重いブラウザ等を1つ閉じてから再実行してください。
ブラウザを閉じて64%。門番を通過。 17枚の逐次生成が始まります。1枚およそ1分。全部で15〜20分コース。
「全然始まらないですけど、動いてます?」
14枚目まで順調に出ました。 そこで一度止めて、残り3枚をあとで再開することにします。 ここで1つ目の落とし穴。
再開のつもりで生成を投げたら、進捗ログがうんともすんとも言わない。 私はClaude Codeに聞きました。「全然始まらないですけど、動いてます?」 プロセスは生きている、と言う。 少し待って、もう一度。「本当に動いてます?」
疑って正解でした。再開になっていなかったのです。
原因はこうです。
このFLUXラッパーは、出力ファイル名を「seed番号」で決めてキャッシュします。
「同じseedなら作り直さず再利用」という前提でした。
ところが実際は、同名ファイルがあると上書きせず_1という別名で作り直していた。
img_201_1024x576.png ← 14枚目までで作った本物
img_201_1024x576_1.png ← 再開時に無駄に作り直された変種
つまり「全部やり直せ」と乱暴なフラグ(--force)を付けたせいで、
出来ている14枚を横目に、最初の1枚から律儀に描き直していた。
そりゃ終わらないわけです。3回聞かれても仕方ない。
学び:「最初から」じゃなく「足りない分だけ」
直し方は単純でした。 生成ループの頭に、たった数行の門番を足すだけ。
# その絵がもう存在するなら、丸ごとスキップして次へ
if [ -s "$WORKDIR/scene_${i}.png" ]; then
echo "⏭ scene $i already done — skip"
continue
fi
これで再開は一瞬になりました。 14枚は即スキップ、残り2枚だけ約4分で生成。 外部モデル側のキャッシュを信じきらず、自分の側で冪等(何度流しても同じ結果)な再開を持つ。 これが正解でした。
長い処理は「最初からやり直す」のではなく「足りない分だけ」作らせる。
当たり前に聞こえて、--force一発で全消しした自分が言うと重みが違います。
ついでに、途中で綺麗に止めるための小細工も足しました。
work/.stopという空ファイルがあったら、今描いている1枚を描き切ってから次のループに入る前に止まる。
Ctrl-Cで作りかけを殺すより、シーンの切れ目で止まるほうが後始末が楽です。
「止めて」と言われて即座に止まりつつ、成果物は無傷。人にやさしい。
2つ目の落とし穴:動画の尺が、音声と1フレームずれる
絵が17枚そろったら、今度は繋いで動画にします。 各絵にゆっくりズーム(いわゆるKen Burns)をかけ、17本の短いクリップにして、連結。 その上に字幕を重ね、ナレーション音声を合わせる。
ここで動画の長さは音声の長さにピッタリ一致していないといけない。 ずれると末尾で絵が固まったり、字幕が後半ずるずる遅れたりします。
各シーンの長さは「音声の実測タイミング」から割り出します。 素朴にやると、各クリップのフレーム数を個別に四捨五入したくなる。 でもこれをやると、17回ぶんの端数がぜんぶ積み重なって、合計が音声とずれる。 1フレーム、2フレームの誤差でも、繋ぐと効いてきます。
効いた対処は「個別に丸めず、累積で丸める」でした。
# 各シーンの開始位置を「累積秒 × fps」で先に丸めてから、隣との差をフレーム数にする
開始フレーム_k = round(そのシーンの開始秒 × fps)
フレーム数_k = 開始フレーム_(k+1) − 開始フレーム_k # 最後のシーンだけ全体尺で締める
こうすると端数が繰り上がり式に相殺され、全クリップの合計は必ず「音声の総フレーム数」に一致します。 結果、動画も音声もキッチリ121.40秒。1フレームも余りません。
締め
作り替え自体は「絵を17枚にして繋ぐ」だけの、地味な話です。 でも詰まったのは派手なアルゴリズムではなく、 「途中まで出来た成果物を捨てないこと」と「端数の丸め方」という、しょうもない現場仕事でした。
外部ツールのキャッシュを信じるな。自分で冪等な再開を持て。 そして端数は、個別に丸めると必ず裏切る。累積で丸めろ。
「本当に動いてます?」と3回聞かせた処理は、 たいてい、動いてるか分からない設計をした自分のせいです。