0

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_id identifies the shot even if its export filename changes.
  • prompt records the requested scene.
  • references records the source filenames for later review.
  • review_status keeps 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí