$ 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-09v1.5.32026-08-18