Batch-optimize a folder of mixed footage into edit-ready, server-friendly HD MP4s.
Point it at 400 clips of assorted resolutions, ratios, codecs and frame rates. Get back one consistent set that scrubs properly in DaVinci Resolve and Premiere, previews in a browser, and is small enough to live on a shared drive.
Built at Starting Frame to put a 181 GB stock-footage library onto a 1 TB shared drive. It came out at 10.5 GB — 94% smaller — across 889 files, with no visible quality loss on the footage that mattered.
Never upscale. Fit the long edge inside 1920 and the short edge inside 1080 — whichever runs out first.
That second half is the part other tools get wrong.
| Source | Output | |
|---|---|---|
| 3840×2160 | 1920×1080 | UHD, the easy case |
| 4096×2160 | 1920×1012 | DCI 4K — not 1080, and the free dimension is forced even |
| 4096×3072 | 1440×1080 | 4:3 — the short edge binds first |
| 5760×2160 | 1920×720 | ultrawide |
| 1080×1920 | untouched | vertical HD — a plain 1920×1080 box crushes this to 608×1080 |
| 2160×3840 | 1080×1920 | vertical 4K — the box turns with the footage |
| 1920×1080 | untouched | never upscales, never re-encodes for nothing |
--cap 2160 | 1440 | 1080 | 720 | 540 moves the box. The rule is the same at every size.
rules.json decides what happens to each file. Read top to bottom, first match wins. No AI, no network, no API key — just if <measured facts> then <action>.
{
"name": "already a lean HD MP4",
"when": { "ext": ["mp4"], "codec": ["h264"],
"long_edge_lte": 1920, "short_edge_lte": 1080, "bitrate_mbps_lte": 14 },
"do": "ignore",
"why": "already the deliverable — don't spend a re-encode on it"
}Conditions: alpha · hdr · ext · codec · long_edge_lte/gt · short_edge_lte/gt · bitrate_mbps_lte/gt · size_gb_lte/gt · duration_s_lte/gt · path_contains
Actions: ignore · compress · copy (repackage, no re-encode) · keep-alpha (ProRes 4444 .mov)
Per-rule encode overrides: quality · crf · maxrate_mbps · flat_areas · aq_strength · grain
A malformed rules file is rejected at startup, naming the offending rule and reason.
Live action and motion graphics fail differently — a gradient bands at 8-bit long before a face does.
{
"name": "motion graphics / gradients",
"when": { "path_contains": ["Backgrounds", "Textures", "Elements"] },
"do": "compress",
"quality": "high", "maxrate_mbps": 24,
"flat_areas": true, "aq_strength": 1.3, "grain": 2.5
}Measured on a midtone gradient encoded both ways: the longest run of identical pixels along a row went 794 px → 23 px. The grain does most of that work — it gives the encoder something to dither against.
Small and smooth aren't in tension at 1080p. What breaks NLE playback is the encode parameters, not the codec.
| Setting | Value | Why |
|---|---|---|
| GOP | closed, keyframe every 1 s | Scrubbing needs anchors. Open GOP is the biggest cause of sluggish seeking. |
| Rate control | capped CRF (CRF 20 + a ceiling) | Quality-driven, but a noisy clip can't blow up the drive. |
| Pixel format | yuv420p 8-bit |
10-bit 4:2:2 is what makes an NLE choke on camera footage. |
| B-frames | 2, no pyramid | Less reordering to unwind on a seek. |
| Colour | Rec.709 written explicitly | An untagged file makes Resolve guess. |
| Range | limited (tv), converted not relabelled |
Mixed full/limited range in one library means blacks jump between clips. |
| Frame rate | left alone | Mixed 24/25/30/60 is fine. CFR is forced only for genuinely variable sources. |
| Audio | AAC 192k, surround downmixed | A batch tool that mangles audio is dead on arrival. |
| Container | MP4 + faststart |
Streams without downloading the whole file. |
Codec choice does more than shrink things: H.264 MP4 previews natively in most browsers, SharePoint and Teams. ProRes and MJPEG .mov don't. In the library above, 115 files couldn't be previewed at all before conversion.
- Real transparency. H.264/MP4 can't carry alpha. But an alpha-capable pixel format isn't proof of transparency — the alpha plane is sampled, and a fully opaque one converts as normal video. Set the rule to
keep-alphato downscale as ProRes 4444.movinstead. - HDR / wide gamut, unless your ffmpeg has
zimgorlibplacebo. Without a correct tonemap, a naive convert comes out washed-out grey. Better to skip and say so. - Anything already within spec. A
copyrule repackages to MP4 with-c copy— verified pixel-identical to the source.
Needs Python 3.9+ and ffmpeg / ffprobe on your PATH.
brew install ffmpeg # macOS
git clone https://github.com/startingframe/rightsize.git# see the plan first — encodes nothing, writes an HTML table of every decision
python3 rightsize.py ~/Footage -o ~/Footage-HD --dry-run
# do it
python3 rightsize.py ~/Footage -o ~/Footage-HD
python3 rightsize.py ~/Footage -o ~/out --cap 720 --quality small
python3 rightsize.py ~/Footage -o ~/out --mirror # keep the folder tree
python3 rightsize.py ~/Footage -o ~/out --hw # Apple VideoToolbox, much faster
python3 rightsize.py ~/Footage -o ~/out --rules my.jsonEvery run writes a dated HTML report showing what happened to each file, which rule fired, and why anything was skipped.
verify.py |
check the output against the source — box, dimensions, pixel format, colour tags, closed GOP, keyframe spacing, faststart, duration, and exact frame counts on a decoded sample |
organize.py |
sort a flat output back into the source folder structure; --dedupe sets aside the same clip delivered twice |
recover.py |
if you change the rules or the code mid-run, list exactly which files need another pass — pipe it straight back into rightsize.py --overwrite |
Things that cost real time, in case they save you some:
- ffmpeg 8.1 drops
-color_primaries/-color_trcthrough a filter chain. Only the matrix and range survive as output options. The tags have to come from asetparamsfilter. setparamsmust run before anything that touches pixels. Some ProRes files carry frame-level primaries ofreserved, an invalid enum that swscale refuses to convert — the encode dies with "Nothing was written into output file." Bothscaleandformatare swscale, so puttingsetparamsafter either one only fixes half your files.ffprobedoesn't expose this at stream level, so it can't be detected up front.- An alpha-capable pixel format isn't proof of alpha. Sample the plane.
- Duplicate filenames across folders will silently eat footage in any flat output. Three camera cards all hold
C0001.MP4. - Never reconstruct identity from a formatted string when you still have the original. A dedupe keyed on stripping a trailing
-<digits>collapsed everyClip-676847382toClip— because the names end in digits.
MIT licensed. rightsize shells out to ffmpeg rather than linking it, so it isn't a derivative work.
Made by Starting Frame — production tools for people making media.