Check AI Video Exports with Python and FFprobe
A downloaded video file rarely contains enough information to explain how it was created. When preparing clips from Wan 3.0, keeping the prompt, reference filenames, and review status beside each export makes it easier to compare versions and prepare an editing handoff.
This tutorial builds a local validation step for that workflow. It checks downloaded files with Python and FFprobe. It does not connect to a generation API or assume that a successful download means the clip is ready to publish.
1. Record the shot and its requirements
Create a file named shot.json:
{
"shot_id": "product-001",
"file": "exports/product-shot.mp4",
"prompt": "A slow camera push toward a ceramic cup on a wooden table.",
"references": ["references/cup-front.png"],
"review_status": "pending",
"requirements": {
"min_width": 1280,
"min_height": 720,
"min_seconds": 4,
"max_seconds": 10,
"require_audio": false
}
}
The requirements belong to this particular project. They do not describe the limits of every generation model or mode.
The other fields serve a different purpose:
shot_ididentifies the shot even if its export filename changes.promptrecords the requested scene.referencesrecords the source filenames for later review.review_statuskeeps technical validation separate from approval.
For repeated generations, give each attempt its own manifest and exported file. Otherwise, overwriting an MP4 can leave an older prompt attached to a newer result.
2. Validate the downloaded file
You need Python 3 and FFmpeg, including ffprobe. Check the installation with:
python --version
ffprobe -version
Save this script as validate_video.py:
import json
import math
import subprocess
import sys
from pathlib import Path
def positive_number(value):
try:
number = float(value)
except (TypeError, ValueError, OverflowError):
return None
if not math.isfinite(number) or number <= 0:
return None
return number
def validate(manifest_path):
manifest_path = Path(manifest_path).resolve()
manifest = json.loads(
manifest_path.read_text(encoding="utf-8")
)
requirements = manifest["requirements"]
video_path = manifest_path.parent / manifest["file"]
if not video_path.is_file():
raise ValueError(f"File not found: {video_path}")
result = subprocess.run(
[
"ffprobe",
"-v", "error",
"-show_streams",
"-show_format",
"-of", "json",
str(video_path),
],
check=True,
capture_output=True,
text=True,
timeout=30,
)
metadata = json.loads(result.stdout)
streams = metadata.get("streams", [])
# Ignore cover art stored as an attached video stream.
video = next(
(
stream
for stream in streams
if stream.get("codec_type") == "video"
and not stream.get("disposition", {}).get("attached_pic")
),
None,
)
if video is None:
raise ValueError("No usable video stream found")
duration = positive_number(video.get("duration"))
duration_source = "video stream"
if duration is None:
duration = positive_number(
metadata.get("format", {}).get("duration")
)
duration_source = "container fallback"
if duration is None:
raise ValueError("No finite, positive duration available")
width = int(video.get("width", 0))
height = int(video.get("height", 0))
has_audio = any(
stream.get("codec_type") == "audio"
for stream in streams
)
failures = []
if width < requirements["min_width"]:
failures.append(f"width too small: {width}px")
if height < requirements["min_height"]:
failures.append(f"height too small: {height}px")
if not (
requirements["min_seconds"]
<= duration
<= requirements["max_seconds"]
):
failures.append(
f"duration outside allowed range: {duration:.3f}s"
)
if requirements.get("require_audio", False) and not has_audio:
failures.append("required audio stream is missing")
print(
f"{width}x{height}, {duration:.3f}s "
f"({duration_source}), audio={has_audio}"
)
if failures:
raise ValueError("; ".join(failures))
print("PASS: export metadata meets the delivery requirements")
print(
"Content review status:",
manifest.get("review_status", "pending"),
)
if __name__ == "__main__":
if len(sys.argv) != 2:
print(
"Usage: python validate_video.py shot.json",
file=sys.stderr,
)
sys.exit(2)
try:
validate(sys.argv[1])
except (
OSError,
ValueError,
KeyError,
TypeError,
OverflowError,
subprocess.SubprocessError,
) as exc:
print(f"FAIL: {exc}", file=sys.stderr)
sys.exit(1)
The script resolves the export path relative to the manifest. Running it from a different working directory therefore does not change which file gets checked.
It also rejects missing, nonnumeric, infinite, and NaN duration values. It prefers the video stream duration and uses the container duration only as a fallback. Container duration can include longer audio or other streams, so the output identifies when that fallback was necessary.
3. Reproduce a passing check
You can test the workflow without generating an AI clip. Create an exports directory, then generate a five-second black video:
mkdir -p exports
ffmpeg -hide_banner -loglevel error \
-f lavfi -i color=c=black:s=1280x720:r=25 \
-t 5 -c:v mpeg4 -y exports/product-shot.mp4
This command overwrites the named test file. Use a separate directory from your actual exports.
Run the validator:
python validate_video.py shot.json
The fixture contains a 1280 × 720 video stream with a five-second duration and no audio. With the example requirements, the expected result is:
1280x720, 5.000s (video stream), audio=False
PASS: export metadata meets the delivery requirements
Content review status: pending
The fixture only exercises the file-checking workflow. It does not demonstrate Wan 3.0 output quality.
4. Reproduce a failing check
Change max_seconds in the manifest to 4 and run the same command again.
The expected failure is:
FAIL: duration outside allowed range: 5.000s
The script exits with code 1, allowing a build system to stop the next step.
Restore max_seconds to 10, then set require_audio to true. The same silent fixture should fail because it has no audio stream.
These examples check two independent delivery requirements without relying on a model to generate a particular result.
5. Review what metadata cannot tell you
A metadata check cannot determine whether a product label changed, a character drifted between frames, or a camera move followed the prompt. An audio stream can exist while containing silence or unsuitable sound. FFprobe also does not perform a complete decode of every frame in this workflow.
For each actual export, compare the clip with its recorded prompt and references. Check the opening and closing frames, subject details, motion, and audio. Add prices, specifications, and other exact text in an editor rather than relying on generated lettering.
After review, update review_status to reflect the decision. The validator deliberately leaves that field unchanged: meeting technical requirements does not approve the content.
With a manifest beside each clip, the next person receives both a file and enough context to understand what still needs checking.
All rights reserved