0

Track Image and Audio Inputs for AI Video Experiments with Python

An image, a voice recording, and a motion prompt are enough to start many character-video experiments. Tracking those inputs becomes harder once you have several narration takes and similar-looking output files.

For workflows using services such as OmniHuman 2.0, a small local preparation script can help. Before uploading anything, it can inspect basic metadata, check the recording against a local duration policy, and save file hashes alongside the prompt.

This tutorial builds that script with Python’s standard library. Generation remains a manual step in the service’s interface; no generation API is required.

What the script checks

The example accepts PNG images and uncompressed PCM WAV audio. These are deliberately narrow script requirements, rather than a complete list of formats a platform might support.

It performs four tasks:

  • Reads PNG dimensions from the image header.
  • Checks WAV duration and detects truncated audio data.
  • Calculates SHA-256 hashes for both files.
  • Creates a JSON record with space for generation settings and review notes.

The PNG check does not decode the full image. Passing it does not prove that an image will render correctly. The audio checks also cannot determine whether speech is intelligible or whether the recording contains background noise.

Create the preparation script

Save this as prepare_inputs.py:

import argparse
import hashlib
import json
import struct
import wave
from pathlib import Path


def inspect_png(path):
    with path.open("rb") as source:
        header = source.read(24)

    if len(header) != 24 or header[:8] != b"\x89PNG\r\n\x1a\n":
        raise ValueError("Expected a PNG file")

    if header[12:16] != b"IHDR" or struct.unpack(">I", header[8:12])[0] != 13:
        raise ValueError("Invalid PNG dimension header")

    width, height = struct.unpack(">II", header[16:24])
    if not width or not height:
        raise ValueError("PNG dimensions must be positive")

    return {"width": width, "height": height}


def inspect_wav(path, limit):
    with wave.open(str(path), "rb") as audio:
        frames = audio.getnframes()
        rate = audio.getframerate()

        if audio.getcomptype() != "NONE" or rate <= 0:
            raise ValueError("Expected uncompressed PCM WAV audio")

        duration = frames / rate
        if not 0 < duration <= limit:
            raise ValueError("Audio exceeds the local duration policy or is empty")

        frame_bytes = audio.getnchannels() * audio.getsampwidth()
        remaining = frames

        while remaining:
            count = min(remaining, 65536)
            if len(audio.readframes(count)) != count * frame_bytes:
                raise ValueError("WAV audio data is truncated")
            remaining -= count

        return {
            "duration_seconds": duration,
            "sample_rate": rate,
            "channels": audio.getnchannels(),
        }


def describe(path, metadata):
    digest = hashlib.sha256()

    with path.open("rb") as source:
        for chunk in iter(lambda: source.read(1024 * 1024), b""):
            digest.update(chunk)

    return {
        "filename": path.name,
        "sha256": digest.hexdigest(),
        **metadata,
    }


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--image", required=True, type=Path)
    parser.add_argument("--audio", required=True, type=Path)
    parser.add_argument("--prompt", default="")
    parser.add_argument("--max-seconds", type=int, default=30)
    parser.add_argument("--out", required=True, type=Path)
    args = parser.parse_args()

    try:
        if args.max_seconds <= 0:
            raise ValueError("--max-seconds must be positive")

        for path in (args.image, args.audio):
            if path.resolve() == args.out.resolve():
                raise ValueError("Output must differ from input files")

        manifest = {
            "image": describe(args.image, inspect_png(args.image)),
            "audio": describe(
                args.audio,
                inspect_wav(args.audio, args.max_seconds),
            ),
            "motion_prompt": args.prompt,
            "local_max_seconds": args.max_seconds,
            "generation": {
                "model": None,
                "settings": {},
                "generated_at": None,
                "task_id": None,
                "output_file": None,
            },
            "review": {
                "status": "pending",
                "notes": [],
            },
        }

        with args.out.open("x", encoding="utf-8") as target:
            json.dump(manifest, target, indent=2)
            target.write("\n")

    except (OSError, ValueError, wave.Error, EOFError) as exc:
        parser.exit(1, f"Preflight failed: {exc}\n")

    print(f"Manifest written to {args.out}")


if __name__ == "__main__":
    main()

The "x" file mode prevents accidental replacement of an existing manifest. Give each experiment its own output filename.

Keep source files unchanged while the script runs. Metadata inspection and hashing happen in separate reads, so this example assumes the inputs are stable.

Generate reproducible test files

You can check the script without preparing a character illustration or recording narration.

Save the following as make_fixtures.py. It creates a gray PNG and a two-second silent WAV:

import struct
import wave
import zlib
from pathlib import Path


def png_chunk(kind, data):
    checksum = zlib.crc32(kind + data) & 0xFFFFFFFF
    return (
        struct.pack(">I", len(data))
        + kind
        + data
        + struct.pack(">I", checksum)
    )


width = height = 320
pixels = (b"\x00" + b"\x80\x80\x80" * width) * height

png = (
    b"\x89PNG\r\n\x1a\n"
    + png_chunk(
        b"IHDR",
        struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0),
    )
    + png_chunk(b"IDAT", zlib.compress(pixels))
    + png_chunk(b"IEND", b"")
)

Path("fixture.png").write_bytes(png)

with wave.open("fixture.wav", "wb") as audio:
    audio.setnchannels(1)
    audio.setsampwidth(2)
    audio.setframerate(16000)
    audio.writeframes(b"\x00\x00" * 32000)

print("Created fixture.png and fixture.wav")

Run:

python make_fixtures.py
python prepare_inputs.py --image fixture.png --audio fixture.wav --out fixture-v1.json

Expected output:

Manifest written to fixture-v1.json

The manifest should contain a width and height of 320, a sample rate of 16000, and a duration of 2.0 seconds.

These fixtures test file handling. A blank image and silent audio are not useful generation inputs.

Check the failure behavior

Set a one-second local limit:

python prepare_inputs.py --image fixture.png --audio fixture.wav --max-seconds 1 --out rejected.json

Expected output:

Preflight failed: Audio exceeds the local duration policy or is empty

The process exits with status 1 and does not create the manifest.

Running the successful command again with the same output filename also fails, protecting the existing experiment record. The operating system’s exact error wording may vary.

Use the record with a real generation

Replace the fixtures with a permitted character image and a clean narration recording:

python prepare_inputs.py --image presenter.png --audio introduction.wav --prompt "Keep a steady medium shot. Use one small welcoming gesture." --out introduction-v1.json

The default 30-second limit is a local policy for short experiments. Check the service’s current requirements and displayed generation cost separately.

Upload the same files through the platform’s interface. After generation, fill in the manifest’s generation fields using information actually available in the interface:

{
  "model": "OmniHuman 2.0",
  "settings": {},
  "generated_at": null,
  "task_id": null,
  "output_file": "introduction-v1.mp4"
}

Record displayed settings in settings. Add the generation time and task ID if available; leave unknown values empty rather than guessing.

The linked service identifies itself as an independent third-party platform. Use the model label and options shown in your actual generation session.

Understand what the hashes prove

A SHA-256 hash identifies the file’s bytes. If someone replaces a recording while retaining its filename, the hash lets you detect the change.

It does not measure visual or audio similarity. Re-exporting an image can change its hash even when the picture looks identical.

Matching hashes also do not guarantee matching generated videos. Model updates, settings, and variation between runs can affect the result.

Review before publishing

The manifest makes inputs traceable, but the generated performance still needs review. Check mouth movement, facial consistency, gestures, object contact, and caption timing.

Use media you own or have permission to use. Obtain consent before animating an identifiable person, and label generated performances when viewers could otherwise mistake them for real recordings.

For each attempt, keep the inputs, manifest, and reviewed output together. That gives the next revision a concrete starting point: you can see what changed without relying on filenames or memory.


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í