Commit Graph
12 Commits
Author SHA1 Message Date
alisson-anjos 12bd7a73b1 Shape the crop to the latent's aspect, not the latent to the crop (1.41.1)
1.41.0 fixed the wrong half of this. The deformation came from the crop and the
latent having different ASPECTS -- a mask around a face gives a portrait box,
sampled in a landscape latent it is stretched on the way in and squeezed back on
the way out -- and rebuilding the latent at the crop's size did remove the
mismatch, but by throwing away the resolution that cropping exists to buy. A
face 200 px tall in the source was then sampled at 200 px, which is the problem
the crop was there to solve.

The planner already takes an aspect_ratio and we were passing 0.0, letting the
box follow the subject. It now gets the connected latent's aspect, and
_static_box grows its short side to match when the planner is not in play. The
box still contains the subject and stays divisible by the grid.

So the latent's size is yours again -- it is the sampling resolution, choose it
for the quality you want -- and its shape no longer has to be guessed.

68 tests; the new ones pin that a landscape latent gives a landscape box, that
the box still contains the subject, and that a free aspect still follows it.
2026-09-03 17:08:17 +00:00
alisson-anjos dab4233e48 Rebuild the latent to the crop instead of asking for it (1.41.0)
The crop's size is derived from the mask, so an EmptyLTXVLatentVideo built
upstream cannot know it -- and a latent that disagreed was not an error
anywhere. The model sampled at the latent's shape and the paste-back squeezed
the result into the box: a face crop is portrait, sampled into a landscape
latent it came back visibly deformed, and under crop_mode zoomed the deformation
changed frame to frame. The only cure was to run once, read the size off the
debug string, rebuild the latent and run again, after every change to the mask.

The sampler now rebuilds the latent's video stream at the crop's shape when they
disagree, keeps the audio stream as it is, and reports it in debug. Nothing is
lost: at denoise 1.0 the initial latent contributes nothing, and the inpainting
path overwrites the video stream with the encoded guide regardless.

BFS Crop Size stays useful for building the latent at the right size up front
(skipping the rebuild) and for taking the per-frame boxes straight to BFS Paste
Back, but it is no longer the price of a correct result.

68 tests.
2026-09-03 16:52:09 +00:00
alisson-anjos c145b091d7 BFS Crop Size: the crop's size before anything is sampled (1.40.0)
The sampler derives its box from the mask, so the size to build the empty latent
at was only knowable after a run: sample a whole clip, read one number off the
debug string, rebuild the latent, sample again -- and repeat after every change
to the mask or the crop widgets.

Getting it wrong does not look like a sizing problem. A mask around a face in a
1920x1080 frame gives a portrait box, 512x672 say; sampled into a 768x512
landscape latent the crop is stretched horizontally, and the paste-back then
squeezes the result back into the portrait box. The face comes out deformed, and
under crop_mode: zoomed the deformation changes frame to frame.

The new node runs the same planner on the same mask and returns width, height,
the per-frame boxes and a note, so the latent can be built up front. The planner
is deterministic, so identical crop widgets give the sampler the identical box.

Also factored the per-frame box normalisation the sampler did inline into
_boxes_per_frame, which both nodes now share.

63 tests.
2026-09-03 16:50:39 +00:00
alisson-anjos c79629dc80 Confine the paste to the mask even with no crop (1.39.0)
_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.
2026-09-03 14:27:51 +00:00
alisson-anjos df32bdc0fe BFS Sampler: optional identity, and put the new widgets back at the end (1.38.0)
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.
2026-09-03 14:07:39 +00:00
alisson-anjos a2b499ce3e Stop the auto config from blowing the mask hole open (1.37.1)
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.
2026-09-01 14:29:22 +00:00
alisson-anjos f4af1cae69 Auto config from the mask, and give the guider back its options (1.37.0)
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.
2026-09-01 12:40:30 +00:00
alisson-anjos 9cdd19ca2d Hard denoise mask, and dilation in latent cells (1.36.0)
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.
2026-08-31 22:30:00 +00:00
alisson-anjos d6f3714e98 Recommend the external decode in the help page (1.35.1)
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.
2026-08-31 21:44:02 +00:00
alisson-anjos 1ba2131b04 Allow deferring the paste-back for a second pass (1.34.0)
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.
2026-08-31 21:00:40 +00:00
alisson-anjos d96761df1f Seed the latent from the guide when inpainting (1.33.0)
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.
2026-08-31 20:44:58 +00:00
alisson-anjos 7433c8457e Serve the help page from web/docs, and add mask_strength (1.32.1)
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.
2026-08-31 20:39:16 +00:00