$ cat node-template.py

V

Video Scene Detect

// Detects scene and shot boundaries in video. Supports detection of hard cuts and gradual transitions. Returns timecodes, durations, and keyframe timestamps.

Process
#video#scene-detection#shot-detection#pyscenedetect
template.py
1import os2import sys3import json4import traceback56from gais import Gais78INPUT_DIR = "/data/input"91011def main():12    try:13        input_json = sys.stdin.read()14        execution_input = json.loads(input_json)15        inputs = execution_input.get("inputs", {})1617        video = inputs.get("video", "")18        detector = inputs.get("detector", "content")19        # threshold unset → None, so the service applies the chosen detector's20        # default (content 27.0, adaptive 3.0)21        threshold_in = inputs.get("threshold")22        threshold = float(threshold_in) if threshold_in not in (None, "") else None23        # Nodes created before 1.5.4 saved the old schema default 27.0 into24        # their inputs. 27 is the content default and meaningless for adaptive25        # (useful range ~1-10), so treat it as unset there.26        if detector == "adaptive" and threshold == 27.0:27            print(28                "Adaptive detector with threshold 27 (the old content default): "29                "using the adaptive default instead",30                file=sys.stderr,31            )32            threshold = None33        min_scene_len = int(inputs.get("min_scene_len", 15) or 15)34        show_progress = inputs.get("show_progress", False)3536        if not video:37            raise ValueError("Video input is required")3839        if detector not in ("content", "adaptive"):40            raise ValueError(f"Invalid detector '{detector}'. Must be 'content' or 'adaptive'")4142        video_path = os.path.join(INPUT_DIR, video)43        if not os.path.exists(video_path):44            raise FileNotFoundError(f"Input video not found: {video_path}")4546        threshold_label = threshold if threshold is not None else "detector-default"47        print(48            f"Detecting scenes: detector={detector}, threshold={threshold_label}, "49            f"min_scene_len={min_scene_len}",50            file=sys.stderr,51        )5253        scene_args = {54            "detector": detector,55            "min_scene_len": min_scene_len,56            "show_progress": show_progress,57        }58        # Leave threshold out when unset: an executor SDK that predates the59        # optional threshold would otherwise crash on float(None).60        if threshold is not None:61            scene_args["threshold"] = threshold62        result = Gais.detect.scenes(video=video_path, **scene_args)6364        result_data = json.loads(result.content)6566        scenes = result_data.get("scenes", [])67        total_scenes = result_data.get("total_scenes", 0)68        video_duration = result_data.get("video_duration", 0.0)69        detector_used = result_data.get("detector_used", detector)70        processing_time = result_data.get("processing_time_ms", 0)7172        print(73            f"Detection complete: {total_scenes} scenes found, "74            f"duration={video_duration}s, detector={detector_used}, "75            f"processing_time={processing_time}ms",76            file=sys.stderr,77        )7879        # Flat output — keys match OUTPUT_SCHEMA80        # scenes is serialized as JSON Text for downstream Text consumers81        output = {82            "scenes": json.dumps(scenes),83            "total_scenes": total_scenes,84            "video_duration": video_duration,85            "detector_used": detector_used,86        }87        print(json.dumps(output, indent=2))8889    except Exception as e:90        error_output = {91            "error": str(e),92            "errorType": type(e).__name__,93            "traceback": traceback.format_exc(),94        }95        print(json.dumps(error_output), file=sys.stderr)96        sys.exit(1)979899if __name__ == "__main__":100    main()

$ git log --oneline

v1.5.4
HEAD
2026-10-09
v1.5.32026-08-18