#!/usr/bin/env python3 """ Run a recipe: plan then execute. This is a convenience wrapper that: 1. Generates a plan (runs analyzers, expands SLICE_ON) 2. Executes the plan (produces video output) """ import json import sys import tempfile from pathlib import Path # Add artdag to path sys.path.insert(0, str(Path(__file__).parent.parent / "artdag")) from artdag.sexp import compile_string from artdag.sexp.planner import create_plan from artdag.sexp.parser import Binding # Import execute functionality from execute import execute_plan class PlanEncoder(json.JSONEncoder): """JSON encoder that handles Binding objects.""" def default(self, obj): if isinstance(obj, Binding): return { "_bind": obj.analysis_ref, "range_min": obj.range_min, "range_max": obj.range_max, } return super().default(obj) def run_recipe(recipe_path: Path, output_path: Path = None): """Run a recipe file: plan then execute.""" recipe_text = recipe_path.read_text() recipe_dir = recipe_path.parent print(f"=== COMPILE ===") print(f"Recipe: {recipe_path}") compiled = compile_string(recipe_text) print(f"Name: {compiled.name} v{compiled.version}") print(f"Nodes: {len(compiled.nodes)}") # Track analysis results analysis_data = {} def on_analysis(node_id, results): analysis_data[node_id] = results times = results.get("times", []) print(f" Analysis: {len(times)} beat times @ {results.get('tempo', 0):.1f} BPM") # Generate plan print(f"\n=== PLAN ===") plan = create_plan( compiled, inputs={}, recipe_dir=recipe_dir, on_analysis=on_analysis, ) print(f"Plan ID: {plan.plan_id[:16]}...") print(f"Steps: {len(plan.steps)}") # Write plan to temp file for execute plan_dict = { "plan_id": plan.plan_id, "recipe_id": compiled.name, "recipe_hash": plan.recipe_hash, "encoding": compiled.encoding, "output_step_id": plan.output_step_id, "steps": [], } for step in plan.steps: step_dict = { "step_id": step.step_id, "node_type": step.node_type, "config": step.config, "inputs": step.inputs, "level": step.level, "cache_id": step.cache_id, } if step.node_type == "ANALYZE" and step.step_id in analysis_data: step_dict["config"]["analysis_results"] = analysis_data[step.step_id] plan_dict["steps"].append(step_dict) # Save plan work_dir = Path(tempfile.mkdtemp(prefix="artdag_run_")) plan_file = work_dir / "plan.json" with open(plan_file, "w") as f: json.dump(plan_dict, f, indent=2, cls=PlanEncoder) print(f"Plan saved: {plan_file}") # Execute plan print(f"\n=== EXECUTE ===") result = execute_plan(plan_file, output_path, recipe_dir) print(f"\n=== DONE ===") print(f"Output: {result}") return result if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: run.py [output.mp4]") print() print("Commands:") print(" run.py - Plan and execute recipe") print(" plan.py - Generate plan only") print(" execute.py - Execute pre-generated plan") sys.exit(1) recipe_path = Path(sys.argv[1]) output_path = Path(sys.argv[2]) if len(sys.argv) > 2 else None if not recipe_path.exists(): print(f"Recipe not found: {recipe_path}") sys.exit(1) run_recipe(recipe_path, output_path)