叠帧 · Numbered Stills → Cover Video
Overview
Scan a folder, keep only files whose stem is purely a number (01.jpg, 200.jpg), play them in numeric order, and encode an H.264 mp4. Between images you either hard cut (--transition none) or crossfade (--transition fade, the default). Non-numbered images (logo, discord, …) and existing videos are ignored.
Procedure
- Inspect the folder first. List files and probe one image's dimensions with ffprobe. Note how many numbered stills exist and whether aspects are mixed.
- Back up any existing output. If
<folder>/cover.mp4already exists, rename it tocover.prev-YYYYMMDD.mp4before rendering. Never silently overwrite the user's earlier render. - Render with the bundled script:
python "<skill-dir>/scripts/imgseq2mp4.py" --dir "<folder>" --out cover.mp4
Defaults: 0.8 s per image with a 0.2 s fade crossfade, 30 fps, 1920x1080, letterbox pad, CRF 18, no audio, +faststart.
- Verify, don't assume. Probe duration/frames, confirm no black frames, and extract the last frame to compare against the last source image:
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
When transitions are on, also grab a frame inside the first transition window and check it is a blend (closer to image 2 than a hard cut would be, but still measurably different from both sources).
- Report image count, ignored images, total duration, output path and size.
Pace
Total length = n × seconds − (n−1) × transition_seconds. Each image's on-screen step is seconds − transition_seconds; that step, not --seconds, is what the viewer feels.
| Step per image | Command | 20 imgs | 44 imgs | 200 imgs |
|---|---|---|---|---|
| 0.35 s | --seconds 0.5 --transition-seconds 0.15 | ~8 s | ~18 s | ~75 s |
| 0.45 s | --seconds 0.65 --transition-seconds 0.2 | ~9 s | ~22 s | ~95 s |
| 0.60 s (default) | --seconds 0.8 --transition-seconds 0.2 | ~12 s | ~27 s | ~120 s |
| 0.80 s | --seconds 1.0 --transition-seconds 0.2 | ~16 s | ~34 s | ~150 s |
Hard cuts use --transition none --seconds <step>.
Move one row at a time when the user asks for faster or slower. Keep --transition-seconds at 0.15–0.2; longer starts to read as a slideshow rather than a crossfade.
Rules
- Crossfade only. Do not use slide, wipe, or other push transitions — they were rejected as a direction.
- Select with regex
^\d+\.(jpe?g|png|webp)$— filename must be only digits. This is what excludeslogo.jpg,discord.jpg,cover.jpg. - Sort by integer value, not lexicographically (
2.jpgbefore10.jpg,99.jpgbefore100.jpg). Gaps in numbering are fine; missing numbers are not errors. - Use
--fit pad(default) so the whole frame stays visible. Only use--fit fillwhen the user explicitly wants edge-to-edge cropping. - Output is silent. Do not add music unless the user supplies a track.
- Keep colors untouched. JPEGs are full-range; if you encode them as untagged full-range H.264, most players decode as limited and the result looks brighter and washed out (+10 levels). The script converts to limited-range bt709 and tags it — do not "simplify" that chain away.
- The script refuses to overwrite
--outwithout--force; resolve by backing up, not by forcing blindly.
Machine notes
- ffmpeg is not on PATH. The script prefers a real
ffmpeg.exe(~/.dsh/tools/ffmpeg/**/ffmpeg.exe, then copies underC:/orD:/ffmpeg) before any.cmdshim, because the shim routes through cmd.exe and caps the command line at ~8k chars. Override withFFMPEG=/path/to/ffmpeg. - This build lacks
-filter_complex_script; pass the xfade graph inline via-filter_complex(the script already does). - Transitions are rendered in groups of
--chunk(default 6) and then merged, so at most 6 decoders are open at once. A single 44-input xfade pass exhausted RAM on a 16 GB machine (~2.4 GB free) and failed withCannot allocate memory; do not raise--chunkabove ~8 here. Grouping recurses, so 200 images merge in three stages without OOM. - Intermediate stage files are encoded CRF 14 / veryfast and discarded; only the final pass uses
--crf/ medium. - Intermediate frames need a writable directory.
tempfile.mkdtemp()results can be inaccessible under restricted sandboxes (observed on Windows + DSH: ffmpeg gotPermission deniedandos.listdirraisedWinError 5). The script therefore creates a plain directory, proves it writable, and falls back through$IMGSEQ_WORKDIR→ system temp →<out-dir>/.imgseq-work.
Resources
scripts/imgseq2mp4.py— selection, numeric sort, hard-cut or crossfade render, verification-friendly summary.