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.
This commit is contained in:
alisson-anjos
2026-08-31 21:00:40 +00:00
parent b440176096
commit 1ba2131b04
3 changed files with 89 additions and 10 deletions
+62 -8
View File
@@ -215,6 +215,12 @@ class BFSHeadSwapMaskedSampler:
"uncrop_feather": ("INT", {"default": 16, "min": 0, "max": 256, "tooltip":
"Blend width when pasting the crop back, in pixels. Sides sitting on the image "
"edge are never feathered -- there is nothing outside to blend into."}),
"paste_back": ("BOOLEAN", {"default": True, "tooltip":
"Composite the crop into the original frames before returning. Turn OFF for a "
"second pass: images/latent then stay in the crop's own space, so you can "
"upscale and refine the crop -- where the face actually has pixels -- and "
"composite at the end with the Head Swap Paste Back node, feeding it the "
"crop_bboxes output."}),
"paste_confine_to_mask": ("BOOLEAN", {"default": True, "tooltip":
"Paste only inside the mask instead of the whole crop rectangle, so anything the "
"model changed in the crop's background never reaches the frame. Off pastes the "
@@ -247,9 +253,9 @@ class BFSHeadSwapMaskedSampler:
},
}
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "MASK", "MASK", "LATENT", "STRING")
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "MASK", "MASK", "LATENT", "BOUNDING_BOX", "STRING")
RETURN_NAMES = ("images", "mask_over_source", "cropped_guide", "crop_mask",
"latent_mask", "latent", "debug")
"latent_mask", "latent", "crop_bboxes", "debug")
OUTPUT_TOOLTIPS = (
"Final frames, with the crop pasted back when cropping is on.",
"The mask painted over the ORIGINAL frames, plus the crop box outline. The "
@@ -261,7 +267,9 @@ class BFSHeadSwapMaskedSampler:
"The mask as the sampler actually sees it: reduced to the latent grid with "
"max and upsampled back for viewing. One frame per latent frame -- this is "
"the real resolution of the edit, and where a too-thin mask disappears.",
"Sampled latent.",
"Sampled latent. With cropping on this is the CROP's latent, which is what you "
"want to upscale and refine in a second pass.",
"The crop boxes, one per frame. Feed to Head Swap Paste Back after a second pass.",
"What the node decided: crop mode and box, mask ops, tiling.",
)
FUNCTION = "execute"
@@ -352,7 +360,7 @@ class BFSHeadSwapMaskedSampler:
guide_video, identity_image, latent=None, subject_mask=None,
crop_mode="off", crop_scale=1.5, crop_divisible_by=32, uncrop_feather=16,
inpaint_with_mask=True, mask_grow=8, mask_blur=4, mask_strength=1.0,
paste_confine_to_mask=True,
paste_back=True, paste_confine_to_mask=True,
temporal_tile_size=0, temporal_overlap=16,
guide_source_id=1.0, identity_source_id=2.0, debug_log=False):
from .ltx_multiple_controls import LTXMultipleControls
@@ -476,7 +484,11 @@ class BFSHeadSwapMaskedSampler:
images = images.reshape(-1, *images.shape[2:])
confine = masks if (paste_confine_to_mask and masks is not None) else None
final = self._paste_back(images, guide_video, crop_ctx, uncrop_feather, confine)
if paste_back:
final = self._paste_back(images, guide_video, crop_ctx, uncrop_feather, confine)
else:
final = images
notes.append("paste_back off: images/latent stay in crop space")
# --- inspection outputs ------------------------------------------
h_px, w_px = guide.shape[1], guide.shape[2]
@@ -518,11 +530,53 @@ class BFSHeadSwapMaskedSampler:
debug = " | ".join(notes)
if debug_log:
print("[BFS Head Swap Masked Sampler]", debug)
boxes_out = crop_ctx[1] if crop_ctx is not None else []
if crop_ctx is not None and crop_ctx[0] == "static":
x0, y0, w, h = crop_ctx[1]
boxes_out = [[{"x": x0, "y": y0, "width": w, "height": h}]] * guide_video.shape[0]
return (final, overlay, guide, crop_mask_out, latent_mask_out,
{"samples": samples}, debug)
{"samples": samples}, boxes_out, debug)
NODE_CLASS_MAPPINGS = {"BFSHeadSwapMaskedSampler": BFSHeadSwapMaskedSampler}
class BFSHeadSwapPasteBack:
"""Composite processed crops back into the original frames.
The other half of `paste_back: off`: run the sampler in crop space, upscale
and refine there, then bring the result home. Sides of a crop that sit on the
image edge are not feathered, and the paste can be confined to a mask so only
the subject travels back.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"cropped_images": ("IMAGE", {"tooltip": "Processed crops, at any resolution."}),
"original_images": ("IMAGE", {"tooltip": "The frames to paste into."}),
"crop_bboxes": ("BOUNDING_BOX", {"tooltip": "From the sampler's crop_bboxes output."}),
"feather": ("INT", {"default": 16, "min": 0, "max": 256}),
},
"optional": {
"confine_mask": ("MASK", {"tooltip":
"Confines the paste to the mask instead of the whole box, in the crop's space."}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("images",)
FUNCTION = "execute"
CATEGORY = CATEGORY
def execute(self, cropped_images, original_images, crop_bboxes, feather, confine_mask=None):
return (BFSHeadSwapMaskedSampler()._paste_back(
cropped_images, original_images, ("planned", crop_bboxes), feather, confine_mask),)
NODE_CLASS_MAPPINGS = {
"BFSHeadSwapMaskedSampler": BFSHeadSwapMaskedSampler,
"BFSHeadSwapPasteBack": BFSHeadSwapPasteBack,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"BFSHeadSwapMaskedSampler": "BFS Head Swap Sampler (crop · mask · loop)"
"BFSHeadSwapMaskedSampler": "BFS Head Swap Sampler (crop · mask · loop)",
"BFSHeadSwapPasteBack": "BFS Head Swap Paste Back",
}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "bfsnodes"
description = "Custom ComfyUI nodes including LTXV Edit Anything, Head-Swap/Anime2Real Bernini conditioning, the AMV Guide Builder, LTX Identity Transfer (IPT2V reference + source-phase RoPE), and MiniMax-H3 single-frame VAE loaders with tile control."
version = "1.33.1"
version = "1.34.0"
license = {file = "LICENSE"}
dependencies = [
"torch",
+26 -1
View File
@@ -105,6 +105,30 @@ slices the guide, the mask and the overlap together per chunk.
Chunked sampling with an AV latent is refused rather than silently corrupted —
slicing a nested latent in time is not a thing the node does yet.
## Second pass at higher resolution
With cropping on, `latent` is the **crop's** latent, not the frame's — which is
what you want to refine, because that is where the face has pixels. To chain a
second pass:
```
Head Swap Sampler (paste_back: off)
├─ latent → LTXV upscaler / second sampler → VAE Decode ─┐
└─ crop_bboxes ───────────────────────────────────────────────┤
guide_video (original frames) ────────────────────────────────────┤
▼
BFS Head Swap Paste Back → frames
```
With `paste_back: off` the node stops compositing, so `images` and `latent` stay
in the crop's own space. Refine there, then bring the result home with **BFS Head
Swap Paste Back**, which takes the crops, the original frames and `crop_bboxes`.
It does the same edge-aware feather and optional mask confinement the sampler
would have done.
Refining before compositing is the right order: upscaling the crop puts the
pixels where the identity is, and the frame around it never needed a second pass.
## Reading the outputs
| output | what it answers |
@@ -114,7 +138,8 @@ slicing a nested latent in time is not a thing the node does yet.
| `cropped_guide` | exactly what the model was fed. If this is wrong, nothing downstream can be right |
| `crop_mask` | the mask after grow/blur, in the crop's pixel space |
| `latent_mask` | the mask as the sampler sees it — where a thin mask disappears |
| `latent` | the sampled latent |
| `latent` | the sampled latent — the CROP's, when cropping is on |
| `crop_bboxes` | the boxes, for Head Swap Paste Back after a second pass |
| `debug` | crop mode and box, mask ops, tiling, the size to build the latent at |
## Suggested order when something looks wrong