$ cat node-template.py

P

Portrait Animation

// Generates a video by animating a portrait image driven by an audio clip. Supports configurable output resolution and reproducible seeds.

Process
Video
template.py
1import os2import sys3import json4import traceback56from gais import Gais78INPUT_DIR = "/data/input"9OUTPUT_DIR = "/data/output"101112def main():13    try:14        input_json = sys.stdin.read()15        execution_input = json.loads(input_json)16        inputs = execution_input.get("inputs", {})1718        image = inputs.get("image", "")19        audio = inputs.get("audio", "")20        resolution = inputs.get("resolution", "768x512")21        seed = inputs.get("seed", 72589)2223        if not image:24            raise ValueError("Input image is required")25        if not audio:26            raise ValueError("Input audio is required")2728        image_path = os.path.join(INPUT_DIR, image)29        audio_path = os.path.join(INPUT_DIR, audio)3031        if not os.path.exists(image_path):32            raise FileNotFoundError(f"Input image not found: {image_path}")33        if not os.path.exists(audio_path):34            raise FileNotFoundError(f"Input audio not found: {audio_path}")3536        os.makedirs(OUTPUT_DIR, exist_ok=True)3738        print(39            f"Requesting speech-driven video: resolution={resolution}, seed={seed}",40            file=sys.stderr,41        )4243        result = Gais.video.create_from_speech(44            image=image_path,45            audio=audio_path,46            resolution=resolution,47            seed=seed,48        )4950        # Save result51        out_filename = "speech_video.mp4"52        out_path = os.path.join(OUTPUT_DIR, out_filename)53        with open(out_path, "wb") as f:54            f.write(result.content)5556        inference_time = result.metadata.get("inference_time_ms", "unknown")57        print(f"Video generated: time={inference_time}ms", file=sys.stderr)5859        # Probe output dimensions and emit aspect_ratio + resolution so60        # downstream nodes can chain.61        try:62            import av63            with av.open(out_path) as _c:64                _s = next(s for s in _c.streams if s.type == "video")65                _w = int(_s.codec_context.width)66                _h = int(_s.codec_context.height)67        except Exception:68            _w, _h = 0, 06970        output = {71            "video": out_filename,72            "aspect_ratio": round(_w / _h, 4) if _h else 0.0,73            "resolution": f"{_w}x{_h}",74        }75        print(json.dumps(output, indent=2))7677    except Exception as e:78        error_output = {79            "error": str(e),80            "errorType": type(e).__name__,81            "traceback": traceback.format_exc(),82        }83        print(json.dumps(error_output), file=sys.stderr)84        sys.exit(1)858687if __name__ == "__main__":88    main()

$ git log --oneline

v3.9.0
HEAD
2026-05-07
v3.8.02026-04-23
v3.4.02026-03-29
v3.3.02026-03-20