Refactor to VACE_PIPE: bundle merge metadata into single pipe connection

Replace 3 separate forceInput wires (mode, trim_start, trim_end) plus
mask and blend_frames with a single VACE_PIPE dict carrying mode, trim
bounds, and context frame counts. SourcePrep outputs reduced from 12 to
7 (segments removed), MergeBack inputs reduced from 9 to 5. Blending
now auto-derives from context counts instead of manual blend_frames.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 23:08:21 +01:00
parent 89fa3405cb
commit 605279d88e
4 changed files with 58 additions and 100 deletions

View File

@@ -12,27 +12,6 @@ OPTICAL_FLOW_PRESETS = {
PASS_THROUGH_MODES = {"Edge Extend", "Frame Interpolation", "Keyframe", "Video Inpaint"}
def _count_leading_black(mask):
"""Count consecutive black (context) frames at the start of mask."""
count = 0
for i in range(mask.shape[0]):
if mask[i].max().item() < 0.01:
count += 1
else:
break
return count
def _count_trailing_black(mask):
"""Count consecutive black (context) frames at the end of mask."""
count = 0
for i in range(mask.shape[0] - 1, -1, -1):
if mask[i].max().item() < 0.01:
count += 1
else:
break
return count
def _alpha_blend(frame_a, frame_b, alpha):
"""Simple linear crossfade between two frames (H,W,3 tensors)."""
@@ -102,16 +81,15 @@ class VACEMergeBack:
)
DESCRIPTION = """VACE Merge Back — splices VACE sampler output back into the original full-length video.
Connect the original (untrimmed) clip, the VACE sampler output, the mask from VACE Mask Generator,
and the mode/trim_start/trim_end from VACE Source Prep. The node detects context zones from the mask
and blends at the seams where context meets generated frames.
Connect the original (untrimmed) clip, the VACE sampler output, and the vace_pipe from VACE Source Prep.
The pipe carries mode, trim bounds, and context frame counts for automatic blending.
Pass-through modes (Edge Extend, Frame Interpolation, Keyframe, Video Inpaint):
Returns vace_output as-is — the VACE output IS the final result.
Splice modes (End, Pre, Middle, Join, Bidirectional, Replace):
Reconstructs original[:trim_start] + vace_output + original[trim_end:]
with optional blending at the seams.
with automatic blending across the full context zones.
Blend methods:
none — Hard cut at seams (fastest)
@@ -124,17 +102,19 @@ Blend methods:
"required": {
"original_clip": ("IMAGE", {"description": "Full original video (before any trimming)."}),
"vace_output": ("IMAGE", {"description": "VACE sampler output."}),
"mask": ("IMAGE", {"description": "Mask from VACE Mask Generator — BLACK=context, WHITE=generated."}),
"mode": ("STRING", {"forceInput": True, "description": "Mode from VACE Source Prep."}),
"trim_start": ("INT", {"forceInput": True, "default": 0, "description": "Start of trimmed region in original."}),
"trim_end": ("INT", {"forceInput": True, "default": 0, "description": "End of trimmed region in original."}),
"blend_frames": ("INT", {"default": 4, "min": 0, "max": 100, "description": "Context frames to blend at each seam (0 = hard cut)."}),
"vace_pipe": ("VACE_PIPE", {"description": "Pipe from VACE Source Prep carrying mode, trim bounds, and context counts."}),
"blend_method": (["optical_flow", "alpha", "none"], {"default": "optical_flow", "description": "Blending method at seams."}),
"of_preset": (["fast", "balanced", "quality", "max"], {"default": "balanced", "description": "Optical flow quality preset."}),
},
}
def merge(self, original_clip, vace_output, mask, mode, trim_start, trim_end, blend_frames, blend_method, of_preset):
def merge(self, original_clip, vace_output, vace_pipe, blend_method, of_preset):
mode = vace_pipe["mode"]
trim_start = vace_pipe["trim_start"]
trim_end = vace_pipe["trim_end"]
left_ctx = vace_pipe["left_ctx"]
right_ctx = vace_pipe["right_ctx"]
# Pass-through modes: VACE output IS the final result
if mode in PASS_THROUGH_MODES:
return (vace_output,)
@@ -145,34 +125,24 @@ Blend methods:
tail = original_clip[trim_end:]
result = torch.cat([head, vace_output, tail], dim=0)
if blend_method == "none" or blend_frames <= 0:
if blend_method == "none" or (left_ctx == 0 and right_ctx == 0):
return (result,)
# Detect context zones from mask
left_ctx_len = _count_leading_black(mask)
right_ctx_len = _count_trailing_black(mask)
def blend_frame(orig, vace, alpha):
if blend_method == "optical_flow":
return _optical_flow_blend(orig, vace, alpha, of_preset)
return _alpha_blend(orig, vace, alpha)
# Blend at LEFT seam (context → generated transition)
bf_left = min(blend_frames, left_ctx_len)
for j in range(bf_left):
alpha = (j + 1) / (bf_left + 1)
orig_frame = original_clip[trim_start + j]
vace_frame = vace_output[j]
result[trim_start + j] = blend_frame(orig_frame, vace_frame, alpha)
# Blend across full left context zone
for j in range(left_ctx):
alpha = (j + 1) / (left_ctx + 1)
result[trim_start + j] = blend_frame(original_clip[trim_start + j], vace_output[j], alpha)
# Blend at RIGHT seam (generated → context transition)
bf_right = min(blend_frames, right_ctx_len)
for j in range(bf_right):
alpha = 1.0 - (j + 1) / (bf_right + 1)
frame_idx = V - bf_right + j
orig_frame = original_clip[trim_end - bf_right + j]
vace_frame = vace_output[frame_idx]
result[trim_start + frame_idx] = blend_frame(orig_frame, vace_frame, alpha)
# Blend across full right context zone
for j in range(right_ctx):
alpha = 1.0 - (j + 1) / (right_ctx + 1)
frame_idx = V - right_ctx + j
result[trim_start + frame_idx] = blend_frame(original_clip[trim_end - right_ctx + j], vace_output[frame_idx], alpha)
return (result,)