叠帧 · Numbered Stills → Cover Video
概要
フォルダをスキャンし、ファイル名の本体部分が純粋な数字であるファイル(01.jpg、200.jpg)のみを保持し、それらを数字順に再生してH.264 mp4にエンコードします。画像間のトランジションはハードカット(--transition none)またはクロスフェード(--transition fade、デフォルト)です。数字以外の画像(logo、discordなど)や既存の動画は無視されます。
手順
- まずフォルダを検査します。 ファイルを一覧表示し、ffprobeで1枚の画像の寸法を確認します。数字で命名された静止画がいくつ存在するか、アスペクト比が混在しているかを確認します。
- 既存の出力ファイルをバックアップします。
<folder>/cover.mp4がすでに存在する場合、レンダリング前にcover.prev-YYYYMMDD.mp4にリネームします。ユーザーの以前のレンダリングを警告なしに上書きすることは絶対にしません。 - バンドルされたスクリプトでレンダリングします:
python "<skill-dir>/scripts/imgseq2mp4.py" --dir "<folder>" --out cover.mp4
デフォルト: 画像あたり0.8秒、0.2秒のfadeクロスフェード、30 fps、1920x1080、レターボックスパディング、CRF 18、音声なし、+faststart。
- 確認します。想定しません。 継続時間/フレーム数をプローブし、黒フレームがないことを確認し、最後のフレームを抽出して最後のソース画像と比較します:
ffprobe -v error -show_entries format=duration -show_entries stream=nb_frames -of default=nw=1 cover.mp4
ffmpeg -i cover.mp4 -vf blackdetect=d=0.05:pix_th=0.10 -an -f null -
ffmpeg -y -sseof -0.05 -i cover.mp4 -frames:v 1 frame_last.png
トランジションがオンの場合、最初のトランジションウィンドウ内のフレームも取得し、それがブレンドであることを確認します(ハードカットよりも画像2に近いが、両方のソースから測定可能な違いがあること)。
- 報告します: 画像数、無視した画像、合計時間、出力パスとサイズ。
ペース
合計長さ = n × 秒 − (n−1) × トランジション秒。各画像の表示ステップは 秒 − トランジション秒 です。このステップが --seconds ではなく、視聴者が感じる値です。
| 画像あたりのステップ | コマンド | 20枚 | 44枚 | 200枚 |
|---|---|---|---|---|
| 0.35 秒 | --seconds 0.5 --transition-seconds 0.15 | ~8 秒 | ~18 秒 | ~75 秒 |
| 0.45 秒 | --seconds 0.65 --transition-seconds 0.2 | ~9 秒 | ~22 秒 | ~95 秒 |
| 0.60 秒 (デフォルト) | --seconds 0.8 --transition-seconds 0.2 | ~12 秒 | ~27 秒 | ~120 秒 |
| 0.80 秒 | --seconds 1.0 --transition-seconds 0.2 | ~16 秒 | ~34 秒 | ~150 秒 |
ハードカットの場合は --transition none --seconds <ステップ> を使用します。
ユーザーから速く/遅くのリクエストがあった場合、一度に1行ずつ調整します。--transition-seconds は0.15~0.2に保ってください。これより長くなると、クロスフェードというよりもスライドショーのように見え始めます。
ルール
- クロスフェードのみ。スライド、ワイプ、その他のプッシュトランジションは使用しないでください。これらは方向性として却下されました。
- 正規表現
^\d+\.(jpe?g|png|webp)$で選択します。ファイル名は数字のみでなければなりません。これによりlogo.jpg、discord.jpg、cover.jpgが除外されます。 - 辞書順ではなく整数値でソートします(
2.jpgは10.jpgの前、99.jpgは100.jpgの前)。番号に欠番があっても問題ありません。欠番はエラーではありません。 --fit pad(デフォルト)を使用して、フレーム全体が表示されるようにします。ユーザーが明示的にエッジツーエッジのトリミングを希望する場合にのみ--fit fillを使用します。- 出力は無音です。ユーザーがトラックを提供しない限り、音楽を追加しないでください。
- 色はそのままにします。JPEGはフルレンジです。タグなしのフルレンジH.264としてエンコードすると、ほとんどのプレーヤーはリミテッドレンジとしてデコードし、結果は明るく色あせて見えます(+10レベル)。スクリプトはリミテッドレンジのbt709に変換し、タグを付けます。このチェーンを「簡略化」しないでください。
- スクリプトは
--forceなしでは--outを上書きしません。盲目的に強制するのではなく、バックアップして解決します。
マシンに関する注意
- ffmpeg は PATH にありません。スクリプトは実際の
ffmpeg.exe(~/.dsh/tools/ffmpeg/**/ffmpeg.exe、次にC:/またはD:/ffmpegのコピー)を、.cmd ラッパーよりも優先します。そのラッパーは cmd.exe 経由でルーティングされ、コマンドラインが約8k文字に制限されるためです。FFMPEG=/path/to/ffmpegでオーバーライドします。 - このビルドには
-filter_complex_scriptがありません。xfade グラフを-filter_complex経由でインラインで渡します(スクリプトは既にそうしています)。 - トランジションは
--chunk(デフォルト6)のグループでレンダリングされ、その後マージされるため、同時に開かれるデコーダーは最大6つです。単一の44入力xfadeパスは16 GBマシン(空き約2.4 GB)でRAMを枯渇させ、Cannot allocate memoryで失敗しました。ここでは--chunkを約8以上に上げないでください。グループ化は再帰的に行われるため、200枚の画像はOOMなしで3段階でマージされます。 - 中間ステージファイルはCRF 14 / veryfastでエンコードされ、破棄されます。最終パスのみが
--crf/ mediumを使用します。 - 中間フレームには書き込み可能なディレクトリが必要です。
tempfile.mkdtemp()の結果は、制限されたサンドボックスではアクセスできない場合があります(Windows + DSHで確認:ffmpeg がPermission denied、os.listdirがWinError 5をスロー)。そのため、スクリプトはプレーンなディレクトリを作成し、書き込み可能であることを確認し、$IMGSEQ_WORKDIR→ システムテンポラリ →<out-dir>/.imgseq-workの順にフォールバックします。
リソース
scripts/imgseq2mp4.py— 選択、数値ソート、ハードカットまたはクロスフェードレンダリング、検証に適したサマリー。