Write talking-head scripts and produce Instagram reels and YouTube shorts
90
97%
Does it follow best practices?
Impact
64%
1.77xAverage score across 3 eval scenarios
Low
Low-risk findings worth noting
#!/usr/bin/env python3
"""Check an exported file against the platform it is bound for.
export_variants.py encodes to a spec; nothing until now read the result back
and asked whether it actually meets one. This does. Each row is one property,
measured from the file, against the platform's requirement, with the fix.
Rows come in two kinds, and the split is the point:
format codec, pixel format, colour tags, HDR, true peak, VFR. Wrong is
wrong; the fix is mechanical and changes nothing the viewer sees.
judgement duration, aspect, fps, loudness. Fixing these changes the
content — a cut loses material, a crop loses edges, a gain
boosts ambience — so a FAIL here is a decision to put to the
human, not a thing to correct silently.
Platform caps are a SNAPSHOT (see PLATFORMS and SPECS_REVIEWED). They move;
when a cap is older than the reel, check the platform's own help page before
trusting a FAIL on duration.
Adapted from kajisho5/ffmpeg-skill's check.py (MIT, see CREDITS.md).
Usage:
check_export.py exports/ig_reels.mp4 --platform ig_reels
check_export.py exports/*.mp4 # platform from the file name
check_export.py exports/tiktok.mp4 --json --no-loudness
"""
import argparse, json, subprocess, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from ffsafe import measure_loudness # noqa: E402
SPECS_REVIEWED = "2026-09-10" # bump when PLATFORMS is re-checked
# max_s: None = no cap this check enforces. aspects: (w/h) ratios accepted.
PLATFORMS = {
"tiktok": {"max_s": 600, "aspects": (9 / 16, 1.0), "min_h": 1080, "fps_max": 60, "lufs": -14, "tol": 2.0, "tp": -1.0, "sdr_only": True},
"ig_reels": {"max_s": 180, "aspects": (9 / 16, 4 / 5, 1.0), "min_h": 1080, "fps_max": 60, "lufs": -14, "tol": 2.0, "tp": -1.0, "sdr_only": True},
"yt_shorts": {"max_s": 180, "aspects": (9 / 16, 1.0), "min_h": 1080, "fps_max": 60, "lufs": -14, "tol": 2.0, "tp": -1.0, "sdr_only": False},
"youtube": {"max_s": None, "aspects": (16 / 9, 9 / 16, 1.0, 4 / 3), "min_h": 720, "fps_max": 60, "lufs": -14, "tol": 2.0, "tp": -1.0, "sdr_only": False},
}
CODECS = ("h264", "hevc")
JUDGEMENT = ("duration", "aspect", "fps", "loudness")
HDR_TRANSFERS = ("smpte2084", "arib-std-b67")
ASPECT_TOL = 0.02
def probe(path):
"""The fields this check reads, from one ffprobe call. Exit on failure."""
r = subprocess.run(
["ffprobe", "-v", "error", "-show_entries",
"stream=codec_type,codec_name,width,height,pix_fmt,r_frame_rate,"
"avg_frame_rate,color_transfer,color_primaries,color_space"
":format=duration", "-of", "json", str(path)],
capture_output=True, text=True)
if r.returncode != 0:
sys.exit(f"ffprobe could not read {path}: {r.stderr.strip()[-300:]}")
d = json.loads(r.stdout)
v = next((s for s in d.get("streams", []) if s.get("codec_type") == "video"), None)
if v is None:
sys.exit(f"{path} has no video stream — this checks video deliverables.")
return v, float(d["format"]["duration"])
def frac(s):
try:
n, d = s.split("/")
return float(n) / float(d) if float(d) else 0.0
except (ValueError, AttributeError):
return 0.0
def rows_for(v, duration, spec, loud=None):
"""Every check row for one file. Pure: no ffmpeg here, so it is testable."""
out = []
def row(name, status, got, want, fix=""):
out.append({"check": name, "status": status, "measured": got,
"expected": want, "fix": fix,
"kind": "judgement" if name in JUDGEMENT else "format"})
if spec["max_s"]:
row("duration", "PASS" if duration <= spec["max_s"] else "FAIL",
f"{duration:.2f}s", f"<= {spec['max_s']}s",
"decide with the human: a cut drops material, a speed-up changes motion")
w, h = int(v.get("width", 0)), int(v.get("height", 0))
ar = w / h if h else 0.0
ok = any(abs(ar - a) <= ASPECT_TOL for a in spec["aspects"])
row("aspect", "PASS" if ok else "FAIL", f"{w}x{h} ({ar:.3f})",
" / ".join(f"{a:.3f}" for a in spec["aspects"]),
"re-export from a master of the right shape; a crop here loses edges")
row("height", "PASS" if h >= spec["min_h"] else "WARN", f"{h}",
f">= {spec['min_h']}", "the platform will upscale; re-render at the mezzanine size")
fps = frac(v.get("avg_frame_rate", "0/1")) or frac(v.get("r_frame_rate", "0/1"))
row("fps", "PASS" if 0 < fps <= spec["fps_max"] else "FAIL",
f"{fps:.3f}", f"<= {spec['fps_max']}", "render_reel.py conforms to 30fps")
vfr = frac(v.get("r_frame_rate", "0/1")) - frac(v.get("avg_frame_rate", "0/1"))
row("vfr", "PASS" if abs(vfr) < 0.5 else "WARN",
"variable" if abs(vfr) >= 0.5 else "constant", "constant",
"re-encode through render_reel.py, which forces a constant rate")
codec = v.get("codec_name", "")
row("video codec", "PASS" if codec in CODECS else "FAIL", codec, "/".join(CODECS),
"export_variants.py writes h264")
pf = v.get("pix_fmt", "")
row("pixel format", "PASS" if pf == "yuv420p" else "FAIL", pf, "yuv420p",
"a 4:2:2 or 10-bit file plays wrong or not at all on phones; re-export")
trc = v.get("color_transfer", "")
if trc in HDR_TRANSFERS:
row("colour", "FAIL" if spec["sdr_only"] else "WARN", f"HDR ({trc})",
"SDR bt709" if spec["sdr_only"] else "SDR bt709, or HDR the platform accepts",
"normalize_clips.py tone-maps HDR to bt709; re-run from the mezzanine")
else:
tags = (v.get("color_primaries", ""), v.get("color_transfer", ""), v.get("color_space", ""))
if all(t == "bt709" for t in tags):
row("colour", "PASS", "bt709 tagged", "bt709 tagged")
elif not any(tags):
row("colour", "WARN", "untagged", "bt709 tagged",
"players assume bt709; export_variants.py tags explicitly")
else:
row("colour", "WARN", "/".join(t or "-" for t in tags), "bt709 tagged",
"tags disagree with bt709; re-export to retag")
if loud is not None and spec["lufs"] is not None:
lufs, tp = float(loud["input_i"]), float(loud["input_tp"])
diff = abs(lufs - spec["lufs"])
row("loudness", "PASS" if diff <= spec["tol"] else "FAIL",
f"{lufs:.1f} LUFS", f"{spec['lufs']} ± {spec['tol']} LUFS",
"export_variants.py normalises speech and music; leave near-silent ambience alone and say so")
row("true peak", "PASS" if tp <= spec["tp"] else "FAIL",
f"{tp:.1f} dBTP", f"<= {spec['tp']} dBTP",
"re-export; the two-pass loudnorm caps true peak")
return out
def platform_from_name(path):
stem = Path(path).stem
return stem if stem in PLATFORMS else None
def main():
ap = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("files", nargs="+")
ap.add_argument("--platform", choices=sorted(PLATFORMS),
help="Default: the file's stem, when it names a platform")
ap.add_argument("--no-loudness", action="store_true",
help="Skip the loudness analysis pass (it decodes the whole file)")
ap.add_argument("--json", action="store_true")
a = ap.parse_args()
reports, any_fail = [], False
for f in a.files:
plat = a.platform or platform_from_name(f)
if plat is None:
sys.exit(f"{f}: cannot tell the platform from the name — pass --platform.")
v, dur = probe(f)
loud = None if a.no_loudness else measure_loudness(f)
if not a.no_loudness and loud is None:
print(f"WARNING: {f}: loudness could not be measured — rows omitted",
file=sys.stderr)
rows = rows_for(v, dur, PLATFORMS[plat], loud)
fails = [r for r in rows if r["status"] == "FAIL"]
any_fail |= bool(fails)
reports.append({"file": f, "platform": plat, "specs_reviewed": SPECS_REVIEWED,
"rows": rows,
"defects": [r for r in fails if r["kind"] == "format"],
"decisions": [r for r in fails if r["kind"] == "judgement"],
"ok": not fails})
if a.json:
print(json.dumps(reports, indent=2))
else:
for rep in reports:
print(f"{rep['file']} -> {rep['platform']} (caps reviewed {SPECS_REVIEWED})")
for r in rep["rows"]:
tag = "" if r["status"] == "PASS" else f" [{r['kind']}]"
print(f" {r['status']:4} {r['check']:13} {r['measured']:<22} want {r['expected']}{tag}")
if r["status"] != "PASS" and r["fix"]:
print(f" -> {r['fix']}")
if rep["decisions"]:
print(" A judgement FAIL changes the content to fix. Put it to the "
"human; do not correct it silently.")
sys.exit(1 if any_fail else 0)
if __name__ == "__main__":
main().tessl-plugin
skills
reel-builder
assets
references
scripts
yap-writer