$ cat node-template.py

Video Uniform

// Normalizes a single video to a target resolution, framerate, and codec. Resize with crop (fill frame) or pad (letterbox). Outputs H.264 video with AAC audio.

Process
Video
template.py
1import os2import sys3import json4import subprocess5import traceback67INPUT_DIR = "/data/input"8OUTPUT_DIR = "/data/output"9FFMPEG_IMAGE = "jrottenberg/ffmpeg:7-ubuntu"101112def parse_resolution(res_str):13    """Parse 'WxH' string to (int, int)."""14    w, h = res_str.split("x")15    return int(w), int(h)161718def build_vf_filter(w, h, fit_mode):19    """Return the -vf filter string for the given resolution and fit mode."""20    if fit_mode == "pad":21        return (22            f"scale={w}:{h}:force_original_aspect_ratio=decrease,"23            f"pad={w}:{h}:(ow-iw)/2:(oh-ih)/2:black,setsar=1"24        )25    # Default: crop26    return (27        f"scale={w}:{h}:force_original_aspect_ratio=increase,"28        f"crop={w}:{h},setsar=1"29    )303132def build_shell_script(video, w, h, fps, fit_mode):33    """Generate the sh script to normalize a single video."""34    vf = build_vf_filter(w, h, fit_mode)35    src = f'/data/input/{video}'36    out = '/data/output/uniform.mp4'3738    lines = [39        "set -e",40        "",41        f"HAS_AUDIO=$(ffprobe -v quiet -select_streams a -show_entries stream=codec_type -of csv=p=0 '{src}' | head -1)",42        'if [ -n "$HAS_AUDIO" ]; then',43        f"  ffmpeg -i '{src}' -vf '{vf}' -r {fps} "44        f"-c:v libx264 -preset medium -crf 23 -pix_fmt yuv420p "45        f"-c:a aac -ar 44100 -ac 2 -b:a 192k "46        f"-movflags +faststart -y '{out}'",47        "else",48        f"  ffmpeg -i '{src}' -f lavfi -i anullsrc=r=44100:cl=stereo "49        f"-vf '{vf}' -r {fps} "50        f"-c:v libx264 -preset medium -crf 23 -pix_fmt yuv420p "51        f"-c:a aac -ar 44100 -ac 2 -b:a 192k "52        f"-map 0:v:0 -map 1:a:0 -shortest "53        f"-movflags +faststart -y '{out}'",54        "fi",55    ]5657    return "\n".join(lines)585960def main():61    execution_input = json.loads(sys.stdin.read())62    inputs = execution_input.get("inputs", {})6364    video = inputs.get("video", "")65    if not video:66        raise ValueError("Video input is required")6768    resolution = inputs.get("resolution", "1920x1080")69    fit_mode = inputs.get("fit_mode", "crop")70    fps = int(inputs.get("framerate", 25))7172    w, h = parse_resolution(resolution)7374    input_path = os.path.join(INPUT_DIR, video)75    if not os.path.exists(input_path):76        raise FileNotFoundError(f"Input video not found: {input_path}")7778    os.makedirs(OUTPUT_DIR, exist_ok=True)7980    shell_script = build_shell_script(video, w, h, fps, fit_mode)8182    host_input = os.environ.get("HOST_STAGING_INPUT", INPUT_DIR)83    host_output = os.environ.get("HOST_STAGING_OUTPUT", OUTPUT_DIR)8485    cmd = [86        "docker", "run", "--rm",87        "--network", "none",88        "--memory", "2g",89        "--cpus", "2.0",90        "-v", f"{host_input}:/data/input:ro",91        "-v", f"{host_output}:/data/output:rw",92        "--entrypoint", "sh",93        FFMPEG_IMAGE,94        "-c", shell_script,95    ]9697    print(f"Launching ffmpeg container: resolution={w}x{h}, fps={fps}, fit={fit_mode}", file=sys.stderr)98    result = subprocess.run(cmd, capture_output=True, text=True, timeout=1800)99    if result.returncode != 0:100        raise RuntimeError(f"ffmpeg failed (exit {result.returncode}): {result.stderr[-2000:]}")101102    print(json.dumps({"video": "uniform.mp4"}, indent=2))103104105if __name__ == "__main__":106    try:107        main()108    except Exception as e:109        print(json.dumps({110            "error": str(e),111            "errorType": type(e).__name__,112            "traceback": traceback.format_exc(),113        }), file=sys.stderr)114        sys.exit(1)