$ cat node-template.py

V

Video Lip Sync

// Synchronizes lip movements in a video to match a provided audio track. Lip Expression controls how pronounced the mouth motion is (LatentSync guidance scale). Handles audio/video length mismatches with loop modes and configurable silent padding. Cloud/hybrid: fal-ai/sync-lipsync/v3 (seed, silent_padding, lip expression and inference steps silently ignored). Local: LatentSync 1.6.

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        video = inputs.get("video", "")19        audio = inputs.get("audio", "")2021        if not video:22            raise ValueError("Video input is required")23        if not audio:24            raise ValueError("Audio input is required")2526        video_path = os.path.join(INPUT_DIR, video)27        if not os.path.exists(video_path):28            raise FileNotFoundError(f"Input video not found: {video_path}")2930        audio_path = os.path.join(INPUT_DIR, audio)31        if not os.path.exists(audio_path):32            raise FileNotFoundError(f"Input audio not found: {audio_path}")3334        os.makedirs(OUTPUT_DIR, exist_ok=True)3536        # Extract optional parameters with defaults37        seed_in = inputs.get("seed", 266)38        seed = int(seed_in) if seed_in not in (None, "") else 26639        loop_mode = str(inputs.get("loop_mode", "pingpong"))40        silent_padding_sec = float(inputs.get("silent_padding_sec", 0.5) or 0.5)4142        # Clamp to the ranges the service enforces — an out-of-range value43        # would come back as an opaque 422 instead of a usable result.44        lips_expression = float(inputs.get("lips_expression", 2.0) or 2.0)45        lips_expression = max(1.0, min(3.0, lips_expression))46        inference_steps = int(inputs.get("inference_steps", 20) or 20)47        inference_steps = max(10, min(50, inference_steps))4849        print(50            f"Requesting lipsync: seed={seed}, loop={loop_mode}, "51            f"padding={silent_padding_sec}s, expression={lips_expression}, "52            f"steps={inference_steps}",53            file=sys.stderr,54        )5556        result = Gais.video.lipsync(57            video=video_path,58            audio=audio_path,59            seed=seed,60            loop_mode=loop_mode,61            silent_padding_sec=silent_padding_sec,62            lips_expression=lips_expression,63            inference_steps=inference_steps,64        )6566        # Save result67        out_filename = "lipsync_result.mp4"68        out_path = os.path.join(OUTPUT_DIR, out_filename)69        with open(out_path, "wb") as f:70            f.write(result.content)7172        inference_time = result.metadata.get("inference_time_ms", "unknown")73        print(74            f"Lipsync complete: time={inference_time}ms, "75            f"seed={seed}, loop={loop_mode}, expression={lips_expression}",76            file=sys.stderr,77        )7879        output = {80            "video": out_filename,81        }82        print(json.dumps(output, indent=2))8384    except Exception as e:85        error_output = {86            "error": str(e),87            "errorType": type(e).__name__,88            "traceback": traceback.format_exc(),89        }90        print(json.dumps(error_output), file=sys.stderr)91        sys.exit(1)929394if __name__ == "__main__":95    main()

$ git log --oneline

v1.8.0
HEAD
2026-08-18
v1.7.02026-05-22
v1.6.02026-05-07
v1.3.02026-03-29
v1.2.02026-03-20