From 164f1291ac7d7f68503b71ab1332efd3f3991dd7 Mon Sep 17 00:00:00 2001 From: gilesb Date: Tue, 13 Jan 2026 02:20:04 +0000 Subject: [PATCH] Add SEQUENCE node handling for concatenating clips Uses FFmpeg concat demuxer. Falls back to re-encoding if stream copy fails (different codecs/formats). Co-Authored-By: Claude Opus 4.5 --- legacy_tasks.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/legacy_tasks.py b/legacy_tasks.py index 60588a9..fe86a77 100644 --- a/legacy_tasks.py +++ b/legacy_tasks.py @@ -843,6 +843,75 @@ def execute_recipe(self, recipe_sexp: str, input_hashes: Dict[str, str], run_id: logger.info(f"COMPOUND step {step.step_id}: {len(filter_chain)} effects -> {content_cid[:16]}...") continue + # Handle SEQUENCE nodes (concatenate clips) + if step.node_type.upper() == "SEQUENCE": + import subprocess + import tempfile + + if len(input_paths) < 2: + raise ValueError(f"SEQUENCE requires at least 2 inputs, got {len(input_paths)}") + + # Create concat list file for FFmpeg + temp_dir = Path(tempfile.mkdtemp()) + concat_list = temp_dir / "concat.txt" + with open(concat_list, "w") as f: + for inp in input_paths: + f.write(f"file '{inp}'\n") + + output_dir = CACHE_DIR / "nodes" / step.cache_id + output_dir.mkdir(parents=True, exist_ok=True) + final_output = output_dir / "output.mkv" + + # FFmpeg concat demuxer + cmd = [ + "ffmpeg", "-y", + "-f", "concat", + "-safe", "0", + "-i", str(concat_list), + "-c", "copy", + str(final_output) + ] + + logger.info(f"SEQUENCE: Concatenating {len(input_paths)} clips") + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode != 0: + # Try with re-encoding if copy fails + cmd = [ + "ffmpeg", "-y", + "-f", "concat", + "-safe", "0", + "-i", str(concat_list), + "-c:v", "libx264", "-c:a", "aac", + str(final_output) + ] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError(f"FFmpeg concat failed: {result.stderr}") + + # Upload to IPFS + cached, content_cid = cache_manager.put( + final_output, + node_type="SEQUENCE", + node_id=step.cache_id, + ) + + # Cleanup + import shutil + shutil.rmtree(temp_dir, ignore_errors=True) + + step_results[step.step_id] = { + "status": "executed", + "path": str(final_output), + "cache_id": step.cache_id, + "cid": content_cid, + "input_count": len(input_paths), + } + cache_id_to_path[step.cache_id] = final_output + total_executed += 1 + logger.info(f"SEQUENCE step {step.step_id}: {len(input_paths)} clips -> {content_cid[:16]}...") + continue + # Get executor for this step type executor = get_executor(step.node_type) if not executor: