Add COMPOUND node execution and S-expression API
- Execute COMPOUND nodes with combined FFmpeg filter chain - Handle TRANSFORM, RESIZE, SEGMENT filters in chain - Migrate orchestrator to S-expression recipes (remove YAML) - Update API endpoints to use recipe_sexp parameter - Extract analysis nodes from recipe for dynamic analysis Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
103
tasks/execute.py
103
tasks/execute.py
@@ -178,6 +178,109 @@ def execute_step(
|
||||
"item_paths": item_paths,
|
||||
}
|
||||
|
||||
# Handle COMPOUND nodes (collapsed effect chains)
|
||||
if step.node_type == "COMPOUND":
|
||||
filter_chain = step.config.get("filter_chain", [])
|
||||
if not filter_chain:
|
||||
raise ValueError("COMPOUND step has empty filter_chain")
|
||||
|
||||
# Resolve input paths
|
||||
input_paths = []
|
||||
for input_step_id in step.input_steps:
|
||||
input_cache_id = input_cache_ids.get(input_step_id)
|
||||
if not input_cache_id:
|
||||
raise ValueError(f"No cache_id for input step: {input_step_id}")
|
||||
path = cache_mgr.get_by_content_hash(input_cache_id)
|
||||
if not path:
|
||||
raise ValueError(f"Input not in cache: {input_cache_id[:16]}...")
|
||||
input_paths.append(Path(path))
|
||||
|
||||
if not input_paths:
|
||||
raise ValueError("COMPOUND step has no inputs")
|
||||
|
||||
# Build FFmpeg filter graph from chain
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
filters = []
|
||||
for filter_item in filter_chain:
|
||||
filter_type = filter_item.get("type", "")
|
||||
filter_config = filter_item.get("config", {})
|
||||
|
||||
if filter_type == "TRANSFORM":
|
||||
effects = filter_config.get("effects", {})
|
||||
for eff_name, eff_value in effects.items():
|
||||
if eff_name == "saturation":
|
||||
filters.append(f"eq=saturation={eff_value}")
|
||||
elif eff_name == "brightness":
|
||||
filters.append(f"eq=brightness={eff_value}")
|
||||
elif eff_name == "contrast":
|
||||
filters.append(f"eq=contrast={eff_value}")
|
||||
elif eff_name == "hue":
|
||||
filters.append(f"hue=h={eff_value}")
|
||||
|
||||
elif filter_type == "RESIZE":
|
||||
width = filter_config.get("width", -1)
|
||||
height = filter_config.get("height", -1)
|
||||
mode = filter_config.get("mode", "fit")
|
||||
if mode == "fit":
|
||||
filters.append(f"scale={width}:{height}:force_original_aspect_ratio=decrease")
|
||||
elif mode == "fill":
|
||||
filters.append(f"scale={width}:{height}:force_original_aspect_ratio=increase,crop={width}:{height}")
|
||||
else:
|
||||
filters.append(f"scale={width}:{height}")
|
||||
|
||||
output_dir = Path(tempfile.mkdtemp())
|
||||
output_path = output_dir / f"compound_{step.cache_id[:16]}.mp4"
|
||||
|
||||
cmd = ["ffmpeg", "-y", "-i", str(input_paths[0])]
|
||||
|
||||
# Handle segment timing
|
||||
for filter_item in filter_chain:
|
||||
if filter_item.get("type") == "SEGMENT":
|
||||
seg_config = filter_item.get("config", {})
|
||||
if "start" in seg_config:
|
||||
cmd.extend(["-ss", str(seg_config["start"])])
|
||||
if "end" in seg_config:
|
||||
duration = seg_config["end"] - seg_config.get("start", 0)
|
||||
cmd.extend(["-t", str(duration)])
|
||||
elif "duration" in seg_config:
|
||||
cmd.extend(["-t", str(seg_config["duration"])])
|
||||
|
||||
if filters:
|
||||
cmd.extend(["-vf", ",".join(filters)])
|
||||
|
||||
cmd.extend(["-c:v", "libx264", "-c:a", "aac", str(output_path)])
|
||||
|
||||
logger.info(f"Running COMPOUND FFmpeg: {' '.join(cmd)}")
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"FFmpeg failed: {result.stderr}")
|
||||
|
||||
cached_file, ipfs_cid = cache_mgr.put(
|
||||
source_path=output_path,
|
||||
node_type="COMPOUND",
|
||||
node_id=step.cache_id,
|
||||
)
|
||||
|
||||
logger.info(f"COMPOUND step {step.step_id} completed with {len(filter_chain)} filters")
|
||||
complete_task(step.cache_id, worker_id, str(cached_file.path))
|
||||
|
||||
import shutil
|
||||
if output_dir.exists():
|
||||
shutil.rmtree(output_dir, ignore_errors=True)
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"step_id": step.step_id,
|
||||
"cache_id": step.cache_id,
|
||||
"output_path": str(cached_file.path),
|
||||
"content_hash": cached_file.content_hash,
|
||||
"ipfs_cid": ipfs_cid,
|
||||
"filter_count": len(filter_chain),
|
||||
}
|
||||
|
||||
# Get executor for this node type
|
||||
try:
|
||||
node_type = NodeType[step.node_type]
|
||||
|
||||
Reference in New Issue
Block a user