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 <noreply@anthropic.com>
This commit is contained in:
@@ -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]}...")
|
logger.info(f"COMPOUND step {step.step_id}: {len(filter_chain)} effects -> {content_cid[:16]}...")
|
||||||
continue
|
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
|
# Get executor for this step type
|
||||||
executor = get_executor(step.node_type)
|
executor = get_executor(step.node_type)
|
||||||
if not executor:
|
if not executor:
|
||||||
|
|||||||
Reference in New Issue
Block a user