Add Bidirectional Extend, Frame Interpolation, and Replace/Inpaint modes
Three new modes for the VACE Mask Generator node, bringing the total to 8. Bidirectional generates before and after the clip, Frame Interpolation inserts frames between each source pair, and Replace/Inpaint regenerates a region in-place. All reuse existing inputs with mode-specific semantics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
71
README.md
71
README.md
@@ -18,10 +18,10 @@ Restart ComfyUI. The node appears under the **VACE Tools** category.
|
||||
| Input | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `source_clip` | IMAGE | — | Source video frames (B, H, W, C tensor) |
|
||||
| `mode` | ENUM | `End Extend` | Generation mode (see below) |
|
||||
| `target_frames` | INT | `81` | Total output frame count for mask and control_frames (1–10000) |
|
||||
| `split_index` | INT | `0` | Where to split the source. Meaning varies by mode. Unused by Edge/Join. |
|
||||
| `edge_frames` | INT | `8` | Number of edge frames for Edge and Join modes. Unused by End/Pre/Middle. |
|
||||
| `mode` | ENUM | `End Extend` | Generation mode (see below). 8 modes available. |
|
||||
| `target_frames` | INT | `81` | Total output frame count for mask and control_frames (1–10000). Unused by Frame Interpolation and Replace/Inpaint. |
|
||||
| `split_index` | INT | `0` | Where to split the source. Meaning varies by mode. Unused by Edge/Join. Bidirectional: frames before clip (0 = even split). Frame Interpolation: new frames per gap. Replace/Inpaint: start index of replace region. |
|
||||
| `edge_frames` | INT | `8` | Number of edge frames for Edge and Join modes. Replace/Inpaint: number of frames to replace. Unused by End/Pre/Middle/Bidirectional/Frame Interpolation. |
|
||||
|
||||
### Outputs
|
||||
|
||||
@@ -143,6 +143,69 @@ control_frames: [ part_2 ][ GREY × generated ][ part_3 ]
|
||||
| `segment_3` | Part 3 — leading edge of second half |
|
||||
| `segment_4` | Part 4 — second half minus its leading edge |
|
||||
|
||||
---
|
||||
|
||||
### Bidirectional Extend
|
||||
|
||||
Generate new frames **both before and after** the source clip.
|
||||
|
||||
- **`split_index`** — number of generated frames to place before the clip. `0` = even split (half before, half after).
|
||||
- **`target_frames`** — total output frame count.
|
||||
- **`frames_to_generate`** = `target_frames − source_frames`
|
||||
|
||||
```
|
||||
mask: [ WHITE × pre ][ BLACK × source ][ WHITE × post ]
|
||||
control_frames: [ GREY × pre ][ source clip ][ GREY × post ]
|
||||
```
|
||||
|
||||
| Segment | Content |
|
||||
|---|---|
|
||||
| `segment_1` | Full source clip |
|
||||
| `segment_2`–`4` | Placeholder |
|
||||
|
||||
---
|
||||
|
||||
### Frame Interpolation
|
||||
|
||||
Insert generated frames **between each consecutive pair** of source frames.
|
||||
|
||||
- **`split_index`** — number of new frames to insert per gap (min 1). `target_frames` is unused.
|
||||
- **`frames_to_generate`** = `(source_frames − 1) × split_index`
|
||||
- **Total output** = `source_frames + frames_to_generate`
|
||||
|
||||
```
|
||||
mask: [ B ][ W×step ][ B ][ W×step ][ B ] ...
|
||||
control_frames: [ f0][ GREY ][ f1][ GREY ][ f2] ...
|
||||
```
|
||||
|
||||
| Segment | Content |
|
||||
|---|---|
|
||||
| `segment_1` | Full source clip |
|
||||
| `segment_2`–`4` | Placeholder |
|
||||
|
||||
---
|
||||
|
||||
### Replace/Inpaint
|
||||
|
||||
Regenerate a range of frames **in-place** within the source clip.
|
||||
|
||||
- **`split_index`** — start index of the region to replace (clamped to source length).
|
||||
- **`edge_frames`** — number of frames to replace (clamped to remaining frames after start).
|
||||
- **`frames_to_generate`** = `edge_frames` (after clamping). `target_frames` is unused.
|
||||
- **Total output** = `source_frames` (same length — in-place replacement).
|
||||
|
||||
```
|
||||
mask: [ BLACK × before ][ WHITE × replace ][ BLACK × after ]
|
||||
control_frames: [ before frames ][ GREY × replace ][ after frames ]
|
||||
```
|
||||
|
||||
| Segment | Content |
|
||||
|---|---|
|
||||
| `segment_1` | Before — source[:start] |
|
||||
| `segment_2` | Original replaced frames — source[start:start+length] |
|
||||
| `segment_3` | After — source[start+length:] |
|
||||
| `segment_4` | Placeholder |
|
||||
|
||||
## Dependencies
|
||||
|
||||
None beyond PyTorch, which is bundled with ComfyUI.
|
||||
|
||||
48
nodes.py
48
nodes.py
@@ -55,10 +55,13 @@ class VACEMaskGenerator:
|
||||
"Middle Extend",
|
||||
"Edge Extend",
|
||||
"Join Extend",
|
||||
"Bidirectional Extend",
|
||||
"Frame Interpolation",
|
||||
"Replace/Inpaint",
|
||||
],
|
||||
{
|
||||
"default": "End Extend",
|
||||
"description": "End: generate after clip. Pre: generate before clip. Middle: generate at split point. Edge: generate between reversed edges (looping). Join: generate to heal two halves.",
|
||||
"description": "End: generate after clip. Pre: generate before clip. Middle: generate at split point. Edge: generate between reversed edges (looping). Join: generate to heal two halves. Bidirectional: generate before AND after clip. Frame Interpolation: insert generated frames between each source pair. Replace/Inpaint: regenerate a range of frames in-place.",
|
||||
},
|
||||
),
|
||||
"target_frames": (
|
||||
@@ -67,7 +70,7 @@ class VACEMaskGenerator:
|
||||
"default": 81,
|
||||
"min": 1,
|
||||
"max": 10000,
|
||||
"description": "Total output frame count for mask and control_frames.",
|
||||
"description": "Total output frame count for mask and control_frames. Unused by Frame Interpolation and Replace/Inpaint.",
|
||||
},
|
||||
),
|
||||
"split_index": (
|
||||
@@ -76,7 +79,7 @@ class VACEMaskGenerator:
|
||||
"default": 0,
|
||||
"min": -10000,
|
||||
"max": 10000,
|
||||
"description": "Where to split the source. End: trim from end (e.g. -16). Pre: reference frames from start (e.g. 24). Middle: split frame index. Unused by Edge/Join.",
|
||||
"description": "Where to split the source. End: trim from end (e.g. -16). Pre: reference frames from start (e.g. 24). Middle: split frame index. Unused by Edge/Join. Bidirectional: frames before clip (0 = even split). Frame Interpolation: new frames per gap. Replace/Inpaint: start index of replace region.",
|
||||
},
|
||||
),
|
||||
"edge_frames": (
|
||||
@@ -85,7 +88,7 @@ class VACEMaskGenerator:
|
||||
"default": 8,
|
||||
"min": 1,
|
||||
"max": 10000,
|
||||
"description": "Number of edge frames to use for Edge and Join modes. Unused by End/Pre/Middle.",
|
||||
"description": "Number of edge frames to use for Edge and Join modes. Unused by End/Pre/Middle. Replace/Inpaint: number of frames to replace.",
|
||||
},
|
||||
),
|
||||
}
|
||||
@@ -159,6 +162,43 @@ class VACEMaskGenerator:
|
||||
control_frames = torch.cat([part_2, solid(frames_to_generate, GREY), part_3], dim=0)
|
||||
return (mask, control_frames, safe(part_1), safe(part_2), safe(part_3), safe(part_4), frames_to_generate)
|
||||
|
||||
elif mode == "Bidirectional Extend":
|
||||
frames_to_generate = max(0, target_frames - B)
|
||||
if split_index > 0:
|
||||
pre_count = min(split_index, frames_to_generate)
|
||||
else:
|
||||
pre_count = frames_to_generate // 2
|
||||
post_count = frames_to_generate - pre_count
|
||||
mask = torch.cat([solid(pre_count, WHITE), solid(B, BLACK), solid(post_count, WHITE)], dim=0)
|
||||
control_frames = torch.cat([solid(pre_count, GREY), source_clip, solid(post_count, GREY)], dim=0)
|
||||
return (mask, control_frames, source_clip, ph(), ph(), ph(), frames_to_generate)
|
||||
|
||||
elif mode == "Frame Interpolation":
|
||||
step = max(split_index, 1)
|
||||
frames_to_generate = (B - 1) * step
|
||||
mask_parts = []
|
||||
ctrl_parts = []
|
||||
for i in range(B):
|
||||
mask_parts.append(solid(1, BLACK))
|
||||
ctrl_parts.append(source_clip[i:i+1])
|
||||
if i < B - 1:
|
||||
mask_parts.append(solid(step, WHITE))
|
||||
ctrl_parts.append(solid(step, GREY))
|
||||
mask = torch.cat(mask_parts, dim=0)
|
||||
control_frames = torch.cat(ctrl_parts, dim=0)
|
||||
return (mask, control_frames, source_clip, ph(), ph(), ph(), frames_to_generate)
|
||||
|
||||
elif mode == "Replace/Inpaint":
|
||||
start = max(0, min(split_index, B))
|
||||
length = max(0, min(edge_frames, B - start))
|
||||
end = start + length
|
||||
frames_to_generate = length
|
||||
before = source_clip[:start]
|
||||
after = source_clip[end:]
|
||||
mask = torch.cat([solid(before.shape[0], BLACK), solid(length, WHITE), solid(after.shape[0], BLACK)], dim=0)
|
||||
control_frames = torch.cat([before, solid(length, GREY), after], dim=0)
|
||||
return (mask, control_frames, safe(before), safe(source_clip[start:end]), safe(after), ph(), frames_to_generate)
|
||||
|
||||
raise ValueError(f"Unknown mode: {mode}")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user