#!/usr/bin/env python """ orbit_poc.py — 2.5D actor orbit preview proof-of-concept. Usage: python orbit_poc.py --input img1.png img2.png ... --output ./output python orbit_poc.py --input ./filmstrip_images/ --output ./output --mode swing --frames 36 Output: ./output/orbit_frames/frame_NNN.png ./output/orbit_preview.mp4 ./output/debug/selected_frame.png ./output/debug/actor_rgba.png ./output/debug/mask.png ./output/debug/depth.png ./output/debug/depth_colorized.png """ import argparse import glob import os import sys import time # Ensure tour-comfy is on the path when running from project root _here = os.path.dirname(os.path.abspath(__file__)) if _here not in sys.path: sys.path.insert(0, _here) from orbit_module import run_orbit_pipeline _IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tiff", ".tif"} def _collect_inputs(raw_inputs: list) -> list: """Expand dirs and glob patterns; return sorted list of image paths.""" paths = [] for item in raw_inputs: if os.path.isdir(item): for fname in sorted(os.listdir(item)): if os.path.splitext(fname)[1].lower() in _IMAGE_EXTS: paths.append(os.path.join(item, fname)) elif "*" in item or "?" in item: paths.extend(sorted(glob.glob(item))) elif os.path.isfile(item): paths.append(item) else: print(f"[warn] not found: {item}", file=sys.stderr) return paths def main(): parser = argparse.ArgumentParser( description="Generate a 2.5D orbit preview (depth-card parallax) from actor images." ) parser.add_argument( "--input", "-i", nargs="+", required=True, metavar="PATH", help="Input image paths, glob patterns, or directories", ) parser.add_argument( "--output", "-o", default="./output", metavar="DIR", help="Output directory (default: ./output)", ) parser.add_argument( "--frames", "-f", type=int, default=36, help="Number of animation frames (default: 36)", ) parser.add_argument( "--fps", type=int, default=24, help="Output video framerate (default: 24)", ) parser.add_argument( "--parallax", "-p", type=float, default=0.08, help="Parallax strength 0–1, fraction of image width (default: 0.08)", ) parser.add_argument( "--angle", "-a", type=float, default=35.0, help="Maximum orbit angle in degrees for swing mode (default: 35)", ) parser.add_argument( "--mode", "-m", choices=["swing", "orbit"], default="swing", help="'swing' = left↔right loop (default), 'orbit' = full 360°", ) parser.add_argument( "--no-debug", action="store_true", help="Skip writing debug output files", ) args = parser.parse_args() image_paths = _collect_inputs(args.input) if not image_paths: print("[error] No input images found.", file=sys.stderr) sys.exit(1) print(f"[orbit] {len(image_paths)} input image(s)") for p in image_paths[:5]: print(f" {p}") if len(image_paths) > 5: print(f" ... ({len(image_paths) - 5} more)") print(f"[orbit] output dir : {os.path.abspath(args.output)}") print(f"[orbit] frames={args.frames} fps={args.fps} mode={args.mode} " f"parallax={args.parallax} angle±{args.angle}°") # Use the first image as the primary input (CLI can pass multiple; first = best choice) primary = image_paths[0] if len(image_paths) > 1: print(f"[orbit] using first image as primary: {primary}") print(f" (pass a single image or the specific frame you want)") t0 = time.perf_counter() result = run_orbit_pipeline( image_path=primary, output_dir=args.output, n_frames=args.frames, parallax_strength=args.parallax, mode=args.mode, fps=args.fps, max_angle_deg=args.angle, debug=not args.no_debug, ) elapsed = time.perf_counter() - t0 print(f"\n[orbit] done in {elapsed:.1f}s") print(f" actor : {result['actor_path']}") print(f" has alpha : {result['has_alpha']}") print(f" has bg plate : {result['has_bg']}") print(f" frames dir : {result['frames_dir']} ({result['n_frames']} PNGs)") print(f" video : {result['video_path']}") if not args.no_debug: print(f" debug dir : {result['debug_dir']}") if not result['has_alpha']: print(f"\n TIP: Use 'No BG' on this image first for a much better orbit effect.") if __name__ == "__main__": main()