Nodes/ComfyUI-BFSNodes/BFS Sampler (crop · mask · loop)
ComfyUI Node

BFS Sampler (crop · mask · loop)

Swap a head in LTX video without the LoRA ever seeing a mask

By alisson-anjos·Created 6 months ago·Updated 18 days ago· 111
BFS Sampler (crop · mask · loop)
  • model
  • vae
  • noise
  • sampler
  • sigmas
  • guider
  • positive
  • negative
  • guide_video
  • identity_image
  • latent
  • subject_mask
  • images
  • mask_over_source
  • cropped_guide
  • crop_mask
  • latent_mask
  • latent
  • crop_bboxes
  • debug
crop_modeoff
crop_scale1.50
crop_divisible_by32
uncrop_feather16
paste_backtrue
paste_confine_to_masktrue
inpaint_with_masktrue
mask_grow8
mask_blur4
mask_hard_for_inpainttrue
latent_mask_dilate0
latent_mask_dilate_frames0
mask_strength1.00
decodefull
decode_tile_size768
decode_overlap64
decode_temporal_size32
decode_temporal_overlap4
temporal_tile_size0
temporal_overlap16
guide_source_id1
identity_source_id2
debug_logfalse
auto_configfalse
identity_headroom1.15

The BFS Head Swap Sampler (crop · mask · loop) is the node half of the BFS "Best Face Swap" LTX workflow, and the name undersells it. It's a whole sampling pipeline in one node: guide video and identity image in, and it crops, masks, samples in temporal chunks, and feathers the result back - all while the head-swap LoRA is never asked to understand a mask. This is the node the LoRA was trained around: guide video on source 1 (body, motion, camera), identity image on source 2, 73 frames per chunk.

LTX head swaps aren't free. LTX 2.3 gets you a fast, genuinely good video model, but a convincing swap still needs a LoRA trained on this reference-conditioning recipe (the pack README calls it video_to_video_ref_adaln). What this node adds on top is the boring production plumbing that makes swaps survive real footage: a stable crop when the face is small in frame, per-frame mask inpainting so only the head changes, and chunked sampling so long clips don't melt your VRAM.

How it works

Under the hood it's a sampler-shaped node - that's why it takes noise, sampler, sigmas, and guider inputs straight out of a normal LTX sampling chain. The guide and identity frames get VAE-encoded into reference latents and injected into the model through transformer_options patches (via the pack's LTXMultipleControls), then each chunk is denoised through your guider. Three extras sit on top:

  • Mask inpainting rides ComfyUI's native path. The mask becomes a latent noise_mask, so outside the mask nothing changes and the original face stays visible to the model. No LoRA training needed for this.
  • Cropping uses a planner vendored from drozbay's MaskVidExperiments (GPL-3.0, same as the pack). Its boxes hold still through mask noise and occlusion - a jittering per-frame crop reads to a video model as camera motion, and it can't carry identity.
  • Paste-back is edge-aware. The feather only fades inward; sides of a crop sitting on the image edge aren't feathered, because there's nothing outside to blend into.

Why crop at all? At 512×288 a person filling a fifth of the frame leaves a face ~25 px tall. No LoRA recovers identity from that. Sampling the head region full-frame gives it 200–300 px, then pastes the result back.

The inputs that matter

  • guide_video and identity_image - the two things that make it a swap. Crop the identity to the head; the guide carries the whole source performance.
  • subject_mask - per-frame mask of the region to edit (head, with a margin). Leave it unconnected for a plain swap. Include the hair if you want it retained; people trip on this constantly.
  • crop_mode - off / combined / tracked / zoomed. Off is fine when the face is big in frame; the others sample inside a box around the subject instead. Start with combined, one static box for the clip.
  • latent - connect it. Feed an empty latent from EmptyLTXVLatentVideo sized to the crop. LTX-2.5 latents are AV (video+audio); without one the node builds a plain video latent and the model may ignore the guide entirely.
  • temporal_tile_size - 0 samples the whole clip in one pass. For clips longer than 73 frames (the training length of the BFS head-swap recipe), set it to 73. One catch: chunked sampling with an AV nested latent throws an error - set this back to 0 or feed a video-only latent.

Outputs worth knowing: images (final frames), latent (the crop's latent when cropping is on - this is what you upscale in a second pass), crop_bboxes (feed to the BFS Head Swap Paste Back node), plus inspection views cropped_guide, mask_over_source, and latent_mask that show you exactly what the model was fed.

Install and what it needs

Install once, like any pack:

cd ComfyUI/custom_nodes
git clone https://github.com/alisson-anjos/ComfyUI-BFSNodes.git
cd ComfyUI-BFSNodes
pip install -r requirements.txt

Restart ComfyUI, then add a BFS head-swap LoRA to ComfyUI/models/loras/. You also need an LTX model and VAE. One dependency gotcha: the crop planner imports scipy, which is listed in the pack's pyproject.toml but not in requirements.txt. If a crop mode dies on import, pip install scipy.

Where people get burned

  • Identity drift or a face that ignores the reference is almost always the face being tiny in frame (turn cropping on) or a reference that isn't a tight frontal head crop. The reference image matters more than any knob here.
  • A soft seam at the head's edge means softness reached the denoise mask. Keep mask_hard_for_inpaint on (default): blur belongs to the paste-back, not the sampling, or the original identity bleeds back in at the boundary.
  • Second-pass decode hangs on a big upscaled latent - switch decode to tiled.

The sampler rewards the "connect only what you need" philosophy: guide + identity is a plain swap, add a mask to restrict it, add a crop when the face is small. Start there and you'll be surprised how often the defaults just work.

CategoryBFS/video

Inputs (37)

NameTypeDefaultDescription
modelMODEL
vaeVAE
noiseNOISE
samplerSAMPLER
sigmasSIGMAS
guiderGUIDERProvides CFG/STG settings; its conds are replaced per chunk.
positiveCONDITIONING
negativeCONDITIONING
guide_videoIMAGESource clip: body, motion, camera, scene. Output geometry follows it.
identity_imageoptIMAGEReference image for the LoRA that wants one — a head crop for a head swap, a subject for identity transfer. Leave it unconnected for any IC-LoRA that works from the guide alone (an instruction edit, a sharpener, a restyler): the slot is simply not packed and the guide is the only reference.
latentoptLATENTEmpty latent from EmptyLTXVLatentVideo, sized to the CROP when cropping is on. Strongly recommended: LTX-2.5 latents are AV (video+audio) and this node cannot fabricate that structure -- without it a plain video latent is built and the model may ignore the guide entirely.
subject_maskoptMASKPer-frame mask of the region to edit (head, with margin). Drives the crop box and, with inpaint_with_mask on, restricts denoising to it. Leave unconnected for a plain swap.
crop_modeoptCOMBOoffSample inside a box around the subject instead of the whole frame -- the fix for faces that are too small to carry identity. combined: one static box for the clip. tracked: a constant-size box that stays still until the subject would leave it. zoomed: the box follows the subject's size too, planned over the whole clip.
crop_scaleoptFLOAT1.501–4Box size as a multiple of the subject. 1.5 leaves a third as margin. Keep neck and shoulders in: a face-tight crop is a framing the LoRA never saw in training.
crop_divisible_byoptINT328–128
uncrop_featheroptINT160–256Blend 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_backoptBOOLEANtrueComposite 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_maskoptBOOLEANtrueComposite only inside the mask, so anything the model changed outside it never reaches the frame. With a crop this confines the paste to the mask instead of the whole box; WITHOUT a crop it is what keeps the untouched pixels the source's own — otherwise the whole frame is the VAE's round trip of it, softer everywhere the edit never went. Off pastes the full frame or box.
inpaint_with_maskoptBOOLEANtrueSend the mask to the sampler as a denoise mask, so only the masked region changes and everything else stays the guide's own pixels. Native ComfyUI inpainting -- the LoRA never sees the mask and the original face stays visible to the model.
mask_growoptINT80–256Dilate the mask before use, in pixels. The new head can be bigger than the old one.
mask_bluroptINT40–256Soften the mask edge, in pixels, to avoid a hard seam.
mask_hard_for_inpaintoptBOOLEANtrueBinarise the mask before it becomes the denoise mask. Blur belongs to the paste-back, where a soft edge hides the seam; in the DENOISE mask a soft edge means partial denoising, which blends the original latent -- and the original identity -- back in exactly at the edge of the head. Off passes the soft mask through to the sampler as well.
latent_mask_dilateoptINT00–16Grow the mask by whole LATENT cells after the reduction. One cell is 32 px, so this is much coarser than mask_grow -- and it is what guarantees the head sits inside editable blocks instead of clipping at a cell boundary. Check the latent_mask output to see the effect.
latent_mask_dilate_framesoptINT00–8Same, along time: grow by whole latent frames (one covers 8 video frames).
mask_strengthoptFLOAT1.000–1How completely the masked region is replaced. 1.0 = fully regenerated. Below that the original latent is blended back, which keeps the original geometry and expression in pixels -- and drags the original identity back with them, so the result becomes an average of both faces. Expression does NOT need this: it reaches the model through the aligned guide, which carries the whole source performance regardless of the mask.
decodeoptCOMBOfullHow to turn the sampled latent into frames. full: one shot, fine at the size the sampler ran at. tiled: for a big latent (a second pass after a 2x upscaler), where a full decode thrashes VRAM and looks like a hang. none: skip decoding and return the latent only -- use your own VAE Decode (Tiled) downstream. With none there is nothing to paste back, so images comes out empty.
decode_tile_sizeoptINT76864–4096
decode_overlapoptINT640–1024
decode_temporal_sizeoptINT324–4096Frames decoded at once in tiled mode.
decode_temporal_overlapoptINT40–256
temporal_tile_sizeoptINT00–1000Frames per chunk. 0 samples the whole clip in one pass. Use the length the LoRA trained at (73 for the LTX head-swap recipe) for clips longer than that.
temporal_overlapoptINT160–256
guide_source_idoptFLOAT10–8
identity_source_idoptFLOAT20–8
debug_logoptBOOLEANfalse
auto_configoptBOOLEANfalseMeasure the subject in the mask and set crop mode, crop scale, mask grow and blur, paste feather and latent dilation from its size -- ignoring those widgets. Needs subject_mask. Every amount below is in pixels, which 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 debug output prints what it chose.
identity_headroomoptFLOAT1.151–2auto_config only. How much bigger the reference head may be than the head in the guide. The mask is the OLD head, so a wider face or more hair lands outside it and gets clipped -- the seam. The reference's PROPORTIONS are measured from identity_image against the mask; its absolute size cannot be, because a cropped head carries no scale. Raise it for big hair or a visibly larger head; 1.0 assumes the two heads match.

Outputs (8)

NameTypeDescription
imagesIMAGEFinal frames, with the crop pasted back when cropping is on.
mask_over_sourceIMAGEThe mask painted over the ORIGINAL frames, plus the crop box outline. The quickest way to see whether the mask is where you think it is, at the scale you think it is, on the frames it belongs to.
cropped_guideIMAGEExactly what the model was fed as the guide: cropped and resized. If this does not look like the region you meant to edit, nothing downstream will.
crop_maskMASKThe mask after grow/blur (and cropping), in the crop's pixel space.
latent_maskMASKThe 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.
latentLATENTSampled latent. With cropping on this is the CROP's latent, which is what you want to upscale and refine in a second pass.
crop_bboxesBOUNDING_BOXThe crop boxes, one per frame. Feed to Head Swap Paste Back after a second pass.
debugSTRINGWhat the node decided: crop mode and box, mask ops, tiling.