_paste_back gave up when there was no crop box and returned the sampler's frame
whole. Nothing outside the mask *changed*, but the entire frame had been through
the VAE's encode/decode, so every pixel the edit never touched came back softer.
For a pointwise edit, or anything sharpness-adjacent, that is the opposite of
what the mask was for.
With no crop and a mask, the full frame is now composited into the ORIGINAL
frames through that mask, the way the crop path already did: outside the mask
the output is the source verbatim, never a round trip of it. The mask's own blur
supplies the soft edge -- there is no box border to feather here.
This makes crop_mode a resolution decision again. Before it, preserving the
untouched pixels required turning the crop on even when the region already had
all the resolution it needed.
52 tests; the new ones pin that outside is the source byte for byte, inside is
the result, no mask still composites nothing, and a result of another size is
brought back to the frame first.
Three things.
`identity_image` moves from required to optional. The node was never head-swap
specific below the surface -- the reference slot is simply not packed when it is
empty, and everything else (crop, mask, inpainting, chunking, paste-back) is
indifferent to which LoRA is running. An instruction-edit, restyle or sharpening
IC-LoRA now runs on the guide alone. IMAGE is a link, not a widget, so this
costs no widget position.
Renamed to "BFS Sampler (crop · mask · loop)" and "BFS Paste Back". The
registration keys are deliberately unchanged: saved workflows reference those,
and only the display name is free to move.
And the fix for the reason this was noticed: 1.37.0 inserted auto_config and
identity_headroom BEFORE crop_mode. ComfyUI stores widgets_values positionally,
so every later value in every saved workflow shifted two slots -- crop_mode's
string landed in auto_config, crop_scale's float in identity_headroom, and the
graph failed validation with "input out of range" on crop_scale and
crop_divisible_by. Both widgets are appended at the end now, with a comment
saying why, and a test pins the historical order so the next addition cannot
repeat it. ltx_multiple_controls.py already carried that rule in a comment;
this file did not follow it.
48 tests.
Two compounding mistakes in 1.37.0's derivation, reported as a hole much larger
than the head with the new head generated floating inside it:
- the upward slack was scaled by the mask's HEIGHT (hh * delta / tilt). Height
swings with how much neck the mask took, and a portrait-shaped identity image
(tilt < 1) multiplied it further: a 300x400 head with a 0.6 reference asked
for 136 px above. Every slack is now a fraction of the head's WIDTH, the
stable dimension, capped at 25% sideways and 30% up.
- latent_mask_dilate was derived from mask_grow and forced to at least one
cell, which added a whole 32 px on EVERY side -- including down, which the
pixel grow deliberately holds at the base -- on top of a grow that already
carried the slack. It is 0 now: _mask_to_latent reduces with MAX, so any cell
the mask touches is fully editable already. The same head asked for 4 cells,
128 px, before this.
Also undo the guider injections in reverse: with several chunks each injection
captured the state the previous one left, so undoing forwards restored chunk
1's specs onto the guider instead of clearing them.
Tests pin both: growth stays under 45% of the head's width across every
reference aspect and headroom, the latent dilation stays 0, and the crop box
always holds the grow plus the feather.
auto_config measures the subject in subject_mask -- width, height, travel, size
change, per-frame step -- and derives crop mode, crop scale, mask grow/blur,
paste feather and latent dilation from it. Every amount in this node is in
pixels, and a pixel only means something relative to how big the head is in
frame: the same 8 px is generous on a distant head and invisible on a close
one, and that mismatch is what leaves a seam.
The mask is the OLD head, so growing it by a fixed fraction only moves the
clip when the new head is wider or has more hair. The reference's proportions
are measured (identity_image's aspect against the mask's) and decide WHERE the
slack goes; its absolute size cannot be measured at all -- a cropped head
carries no scale -- so that part is identity_headroom, default 1.15. Growth is
anisotropic: up and sideways, never down into the neck and collar, which stay
the guide's own pixels. The margin between mask and crop border is a budget:
the grow takes what it needs, the feather gets half of what is left, floored --
a ramp wider than its share fades the head itself.
Two fixes for state that outlived a run:
- the transformer_options written into the guider are now undone in a finally.
The guider belongs to another node and survives in the execution cache, so
the reference latents stayed reachable (VRAM never freed) and a later run
that did not overwrite the same keys sampled with the previous clip's specs.
- chunked clips move each sampled chunk to the offload device as it comes out,
instead of holding all of them on the sampling device.
40 tests, including that each side grows by exactly its own amount (the pad is
mirrored: padding the top grows the mask downward) and that the feather never
reaches past the margin it has.
The node was feeding one mask to two jobs with opposite needs. A soft edge
hides the seam when compositing, but in the denoise mask it means partial
denoising: the original latent, and with it the original identity, blends
back in exactly at the edge of the head. mask_hard_for_inpaint (default on)
binarises the mask for the sampler and leaves the soft one for the paste.
latent_mask_dilate grows the mask by whole latent cells after the reduction,
which is the scale that actually decides whether the head sits inside
editable blocks -- 32 px per cell, 8 video frames per latent frame. On a test
mask one cell went from 11% to 25% of the grid. latent_mask_dilate_frames
does the same along time. The latent_mask output reflects both.
Internal decoding exists only because the paste-back needs pixels. For
anything past a single pass the graph's own VAE Decode (Tiled) is the better
tool -- tunable, visible, already there -- so the docs now say so and point
at decode: none plus the paste-back node.
With cropping on, the sampled latent is the crop's, which is exactly what a
refinement pass wants: the face has pixels there and the surrounding frame
never needed a second pass. But the node composited before returning, so
there was no way to get between the two.
paste_back: off now keeps images and latent in crop space, crop_bboxes comes
out alongside them, and the new BFS Head Swap Paste Back node composites at
the end with the same edge-aware feather and mask confinement.
Inpainting preserves the INITIAL latent outside the mask. The node was
handing it the empty latent, so everything outside the mask came back grey
instead of the source video -- the encode that a normal inpaint graph does
before Set Latent Noise Mask was simply missing.
The guide chunk is now VAE-encoded and becomes the sampling latent whenever
a mask is in play. On AV latents the audio stream from the connected latent
is kept and only the video stream is replaced.
The frontend fetches node help from /extensions/<module>/docs/<NodeID>.md,
which is served out of WEB_DIRECTORY -- a docs/ folder at the pack root is
never reached. Moved to web/docs/.
mask_strength controls how completely the masked region is replaced. It is
NOT how you get expressions: those arrive through the aligned guide, which
carries the whole source performance regardless of the mask. Below 1.0 the
original latent blends back, keeping the original geometry and dragging the
original identity along with it -- an average of both faces. Documented as
such so it is not reached for by mistake.