Build a Video Handoff Validator With Python and ffprobe

Before handing a media folder to an editor, I want to check that the files inside match the delivery brief. A filename such as hero_final_1080p.mp4 gives me something to search for, but I still need to read the file's metadata.
I put the expected properties in a small JSON manifest and compare them with ffprobe output.
Write down what the package should contain
Save this as handoff.json at the root of the media package:
[
{
"file": "picture/hero.mp4",
"width": 1280,
"height": 720,
"audio": false
},
{
"file": "audio/narration.wav",
"audio": true
}
]
Paths are relative to that JSON file. Here, I expect a 1280 x 720 picture file without an audio stream, plus a separate narration file with audio. Omitting a property means it won't be checked.
Astorie's guide to what belongs in an NLE handoff package covers the broader brief, captions, and asset-history decisions. I keep these file checks alongside those editorial notes.
Compare the manifest with the files
This script uses Python's standard library. Install FFmpeg first and confirm that ffprobe -version works in your terminal.
Save the script as check_handoff.py:
import json
import shutil
import subprocess
import sys
from pathlib import Path
def validate(manifest_path):
entries = json.loads(manifest_path.read_text(encoding="utf-8"))
errors = []
for item in entries:
label = item["file"]
path = (manifest_path.parent / label).resolve()
if not path.is_file():
errors.append(f"{label}: missing")
continue
try:
result = subprocess.run(
[
"ffprobe", "-v", "error",
"-show_entries", "stream=codec_type,width,height",
"-of", "json", "-i", str(path),
],
check=True, capture_output=True, text=True, timeout=20,
)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
errors.append(f"{label}: probe failed")
continue
streams = json.loads(result.stdout).get("streams", [])
video = next(
(s for s in streams if s.get("codec_type") == "video"), {}
)
for key in ("width", "height"):
if key in item and video.get(key) != item[key]:
errors.append(
f"{label}: {key} expected {item[key]}, got {video.get(key)}"
)
has_audio = any(s.get("codec_type") == "audio" for s in streams)
if "audio" in item and has_audio != item["audio"]:
errors.append(
f"{label}: audio expected {item['audio']}, got {has_audio}"
)
print("\n".join(errors) if errors else f"PASS: {len(entries)} files match manifest")
return 1 if errors else 0
if __name__ == "__main__":
if len(sys.argv) != 2:
raise SystemExit("Usage: python check_handoff.py handoff.json")
if shutil.which("ffprobe") is None:
raise SystemExit("Install FFmpeg and put ffprobe on PATH.")
raise SystemExit(validate(Path(sys.argv[1]).resolve()))
Run it with the path to the manifest:
python check_handoff.py handoff.json
For a matching package:
PASS: 2 files match manifest
If I change the expected width to 1920 while the file remains 1280 pixels wide, the report becomes:
picture/hero.mp4: width expected 1920, got 1280
A missing narration file produces its own message:
audio/narration.wav: missing
The script continues through the list and exits with status 1 if anything fails. That makes the same check usable in a terminal or a build step.
I resolve file paths against the manifest's location so moving the whole package doesn't require rewriting the list. Width and height refer to the stored video dimensions. The audio check only detects a stream; listening to the narration is still a separate review task.
About the author
- Astorie is the agentic canvas for visual storytellers, with a creative AI partner that learns your taste, shapes your shots, and turns ideas into cinematic video.
Comments
Join the discussionNo comments yet. Be the first to comment.

