GPU LayerStyles TwoBatchLoop
Two clips in, one seamless loop out, both seams blended
- images_1
- images_2
- images
Seamless loops used to be the one job you kept AnimateDiff around for. That era is over - you generate the loop with a modern video model and assemble it, and the assembly is where it goes wrong. It's easy to blend one junction and forget the other, and a loop with one soft seam and one hard cut looks worse than two hard cuts. GPULayerStyles_TwoBatchLoop blends both boundaries in one node.
How it works
Give it two batches and it produces a cycle in this order:
- The tail of
images_2crossfades into the head ofimages_1. - The middle of
images_1, untouched. - The tail of
images_1crossfades into the head ofimages_2. - The middle of
images_2, untouched.
So you get A → B → back to A, with both joins dissolved. Each transition is exactly blend_target frames long including both endpoints: the first frame of the fade is entirely the outgoing batch, the last is entirely the incoming one, linear in between. The blends run in float32 and touch every channel including alpha, without clamping - a soft-edged RGBA loop stays soft through the fade.
The maths on length is worth internalising before you set the slider. blend_target frames get consumed at each end of each batch, so the output is len(images_1) + len(images_2) - 2 × blend_target frames. Fifteen frames of blend costs you thirty frames total. On two 81-frame clips that's an 81-frame loop - fine. On two 30-frame clips, the node refuses outright: each input needs at least 2 × blend_target + 1 frames so that a middle survives, which for blend_target: 15 means 31 frames minimum per clip.
Inputs
images_1,images_2- the two halves of the cycle. Lengths may differ; height, width and channel count must match (RGB or RGBA).blend_target- default 15, minimum 2. The exact number of frames in each transition, endpoints included.append_first_frame- boolean, default off. On, it appends an exact copy of the completed loop's first frame. That sounds redundant until you know why: it's there so a frame-interpolation pass can generate the frames from the loop's last frame back to its first, closing the final seam smoothly. The node doesn't interpolate - that's your RIFE/FILM node's job - and after interpolating you trim the redundant endpoint with aSliceImageBatchset to0:-1. The two nodes are designed to be used as a pair.output_deviceandbatch_size- the pack's standard execution controls: float32 on the compute device, output on CPU by default (gpuavoids a round trip if the next node is on-device), and chunked processing where0starts at 64 frames and the chunk halves itself on an allocation failure.
One output, images.
The workflow it's for
Generate two clips that are actually the same continuous action, then assemble. Or generate one clip, split it with a slice, and bounce it - a shorter window with a longer blend reads as a slow shutter drag rather than a loop, which is often what you wanted. The honest caveat is the one the KB already records about loop-morphing generally: crossfading hides a discontinuity, it doesn't resolve one. Frames from both sides are ghosting through each other across the fade, so if the two batches contain genuinely different motion, motion blur or a beat-synced cut is the better answer. Blend windows of 8–20 frames are where a loop reads as continuous without the double-image being legible.
Install
ComfyUI Manager → search GPU LayerStyles and restart. Or:
cd /path/to/ComfyUI/custom_nodes
git clone https://github.com/lany42/gpu-layerstyles
# restart ComfyUI
Nothing to pip-install (the runtime dependency list is empty), no models, no API key. Python 3.13+ and a ComfyUI recent enough for the comfy_api node schema are the real requirements. AGPL-3.0-only. Repo canonical at git.colorized.life, mirrored on GitHub.
Getting burned
The frame-count floor is the one that bites. Short clips can't take a big blend_target - the node errors and prints how many frames each batch actually has, which makes it easy to fix, but if you're generating 25-frame test clips, set blend_target to 5 or so and expect the loop to be short.
Watch the memory. The full output is reserved up front, so two 500-frame 1080p clips means an ~970-frame float32 tensor plus chunk buffers. Leave output_device on cpu unless you have a reason to keep it in VRAM.
Don't forget the trim. With append_first_frame on and no interpolation step, you get a loop whose last frame equals its first - one duplicated frame per cycle, which is visible as a hitch on repeat.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| images_1 | IMAGE | — | |
| images_2 | IMAGE | — | |
| blend_target | INT | 152–2147483647 | Exact number of frames in each transition, including both endpoints. At least 2; both batches must retain a nonempty middle after removing this many frames from each end. |
| append_first_frame | BOOLEAN | false | Append an exact copy of the completed loop's first frame to enable interpolation from the last frame to the first. After interpolation, remove the redundant endpoint with an external SliceImageBatch node using 0:-1. TwoBatchLoop does not interpolate or trim frames. |
| output_device | COMBO | cpu | Return output on CPU (default) after float32 processing on ComfyUI's selected compute device. GPU keeps output on that device to avoid transfers between compatible nodes. ComfyUI CPU mode returns CPU output. |
| batch_size | INT | 00–2147483647 | Frames per processing chunk. 0 starts at up to 64 frames. Positive values can exceed 64, capped by the output length. Allocation failures halve the chunk size for this run; each run starts fresh. Complete inputs and outputs still need memory. Pass smaller batches through the entire workflow to reduce complete-batch memory. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| images | IMAGE | — |