# 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:

```json
[
  {
    "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](https://astorie.ai/blog/video-production-pipeline) 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`:

```python
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:

```bash
python check_handoff.py handoff.json
```

For a matching package:

```text
PASS: 2 files match manifest
```

If I change the expected width to 1920 while the file remains 1280 pixels wide, the report becomes:

```text
picture/hero.mp4: width expected 1920, got 1280
```

A missing narration file produces its own message:

```text
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.
