$ cat node-template.py

V

Video Image Overlay

// Overlays a static image onto a video at a specified position and size.

Process
Video
template.py
1import os2import sys3import json4import subprocess5import traceback67INPUT_DIR = "/data/input"8OUTPUT_DIR = "/data/output"910FFMPEG_IMAGE = f"emblema/ffmpeg:{os.getenv('EMBLEMA_VERSION', 'dev')}"11SVG_CONVERTER_IMAGE = "emblema/svg-converter:latest"121314def main():15    execution_input = json.loads(sys.stdin.read())16    inputs = execution_input.get("inputs", {})1718    x = inputs.get("x", 10)19    y = inputs.get("y", 10)20    z = inputs.get("z", 100)21    _opacity_in = inputs.get("opacity")22    opacity_pct = 100 if _opacity_in in (None, "") else int(_opacity_in)23    opacity = max(0.0, min(1.0, opacity_pct / 100.0))2425    os.makedirs(OUTPUT_DIR, exist_ok=True)2627    filter_complex = (28        f"[1:v]scale={z}:-1,format=rgba,colorchannelmixer=aa={opacity}[ovrl];"29        f"[0:v][ovrl]overlay={x}:{y}"30    )3132    # Use host staging paths (set by executor unconditionally)33    # so the sibling ffmpeg container mounts the same host directories.34    host_input = os.environ.get("HOST_STAGING_INPUT", INPUT_DIR)35    host_output = os.environ.get("HOST_STAGING_OUTPUT", OUTPUT_DIR)3637    image_file = inputs['image']38    image_input_path = f"/data/input/{image_file}"3940    if image_file.lower().endswith('.svg'):41        # Container 1: Convert SVG to transparent PNG via rsvg-convert42        svg_cmd = [43            "docker", "run", "--rm",44            "--network", "none",45            "--memory", "512m",46            "--cpus", "1.0",47            "-v", f"{host_input}:/data/input:ro",48            "-v", f"{host_output}:/data/output:rw",49            SVG_CONVERTER_IMAGE,50            "-w", str(z),51            "-a",52            f"/data/input/{image_file}",53            "-o", "/data/output/_overlay.png",54        ]55        result = subprocess.run(svg_cmd, capture_output=True, text=True)56        if result.returncode != 0:57            raise RuntimeError(f"rsvg-convert failed: {result.stderr}")58        image_input_path = "/data/output/_overlay.png"5960    video_path = f"/data/input/{inputs['video']}"61    shell_script = (62        "set -e\n"63        f"HAS_AUDIO=$(ffprobe -v quiet -select_streams a -show_entries stream=codec_type -of csv=p=0 '{video_path}' | head -1)\n"64        f"if [ -n \"$HAS_AUDIO\" ]; then\n"65        f"  ffmpeg -y -i '{video_path}' -i '{image_input_path}' -filter_complex '{filter_complex}' -codec:a copy /data/output/output.mp4\n"66        f"else\n"67        f"  ffmpeg -y -i '{video_path}' -i '{image_input_path}' -filter_complex '{filter_complex}' /data/output/output.mp4\n"68        f"fi\n"69    )7071    cmd = [72        "docker", "run", "--rm",73        "--network", "none",74        "--memory", "2g",75        "--cpus", "2.0",76        "-v", f"{host_input}:/data/input:ro",77        "-v", f"{host_output}:/data/output:rw",78        "--entrypoint", "sh",79        FFMPEG_IMAGE,80        "-c", shell_script,81    ]8283    result = subprocess.run(cmd, capture_output=True, text=True, timeout=1800)84    if result.returncode != 0:85        raise RuntimeError(f"ffmpeg failed (exit {result.returncode}): {result.stderr[-2000:]}")8687    print(json.dumps({"video": "output.mp4"}, indent=2))888990if __name__ == "__main__":91    try:92        main()93    except Exception as e:94        print(json.dumps({95            "error": str(e),96            "errorType": type(e).__name__,97            "traceback": traceback.format_exc(),98        }), file=sys.stderr)99        sys.exit(1)

$ git log --oneline

v1.2.1
HEAD
2026-08-18
v1.1.22026-05-07
v1.1.12026-04-23
v1.1.02026-04-22
v1.0.02026-04-09