Merges full history from art-dag/mono.git into the monorepo under the artdag/ directory. Contains: core (DAG engine), l1 (Celery rendering server), l2 (ActivityPub registry), common (shared templates/middleware), client (CLI), test (e2e). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> git-subtree-dir: artdag git-subtree-mainline:1a179de547git-subtree-split:4c2e716558
128 lines
3.6 KiB
Python
Executable File
128 lines
3.6 KiB
Python
Executable File
#!/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 <recipe.sexp> [output.mp4]")
|
|
print()
|
|
print("Commands:")
|
|
print(" run.py <recipe> - Plan and execute recipe")
|
|
print(" plan.py <recipe> - Generate plan only")
|
|
print(" execute.py <plan> - 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)
|