$ 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)