VACE Join Assemble
Stitch the seam back together — loop pass included
- start_images
- transition_images
- end_images
- images
VACE Join Prep builds the seam's control window; your VACE nodes generate the transition; VACE Join Assemble is where it all comes back together. It takes the three pieces - the kept start of clip A, the generated transition, and the kept start of clip B - and concatenates them into one continuous clip. And it has one extra trick: when is_final_loop_pass is on, it switches to the tail-to-head logic so the same node can also close a loop without a second, separate assembly path.
How it works
Normally it's a straightforward stitch:
output = start_images + transition_images + end_images
start_images is the body of clip A (with the replaced tail trimmed), transition_images is the crossfaded VACE output, end_images is clip B's body (with the replaced head trimmed). The node validates that all three match resolution before concatenating.
On the final loop pass, the behavior changes:
- It drops the trailing
context_framesof guidance from the transition (transition = transition[:-context_frames]) - those frames were just context anchoring the generation, not meant to be visible. - It does not append
end_images. On the loop pass, the "second clip" is really the first clip again, and appending its body would duplicate footage. So the output is juststart_images+ trimmed transition, and the loop closes into the first clip's body instead of repeating it.
The README phrases it as assembling "without duplicating or extending the first clip body," and that's the bit to remember: the final loop pass reuses the retained body rather than copying the whole clip a second time.
Inputs and outputs that matter
start_images,transition_images,end_images(IMAGE) - the three seam piecesis_final_loop_pass(BOOLEAN, forced input) - wire it fromVACE Clip Loop Start's output of the same name, so it's true only on the last passcontext_framesandreplace_frames(INT, forced inputs) - echoed through from prep so the node knows how many guidance frames to trim on the loop passdebug(boolean, defaultfalse) - prints frame counts, including how many transition frames were emitted after trimming
Output is a single images batch - the joined clip, ready for VAE decode.
Installing it
Part of the comfy-vace-automation pack, no extra pip dependencies:
cd ComfyUI/custom_nodes
git clone https://github.com/EnviralDesign/comfy-vace-automation
Restart ComfyUI, or install via ComfyUI Manager (search "comfy-vace-automation").
When it bites you
- Forced inputs.
is_final_loop_pass,context_frames, andreplace_framesare all forced-input, so they must be wired (usually from Loop Start and Join Prep), not typed. Missing links mean the node won't execute. - Resolution mismatch. All three pieces must match - this is the node that will tell you if your VACE decode came back at a different size than the source clips. Re-run with matching dimensions.
- Loop pass with a too-short transition. If the transition is shorter than
context_frames, the trim on the final pass errors out. Generate enough frames on the VACE side before assembling.
One way to think of the two-node split: Join Prep is the question ("what do I want the model to regenerate?"), Join Assemble is the answer ("here's the finished seam, in the right order"). Keep them wired as a matched pair and the loop logic just works.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| start_images | IMAGE | — | |
| transition_images | IMAGE | — | |
| end_images | IMAGE | — | |
| is_final_loop_pass | BOOLEAN | — | |
| context_frames | INT | — | |
| replace_frames | INT | — | |
| debug | BOOLEAN | false | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| images | IMAGE | — |