BFS Sampler (crop · mask · loop)
Swap a head in LTX video without the LoRA ever seeing a mask
- 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
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_videoandidentity_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 withcombined, 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-0samples 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_inpainton (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
decodetotiled.
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.
Inputs (37)
| Name | Type | Default | Description |
|---|---|---|---|
| model | MODEL | — | |
| vae | VAE | — | |
| noise | NOISE | — | |
| sampler | SAMPLER | — | |
| sigmas | SIGMAS | — | |
| guider | GUIDER | Provides CFG/STG settings; its conds are replaced per chunk. | |
| positive | CONDITIONING | — | |
| negative | CONDITIONING | — | |
| guide_video | IMAGE | Source clip: body, motion, camera, scene. Output geometry follows it. | |
| identity_imageopt | IMAGE | Reference 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. | |
| latentopt | LATENT | Empty 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_maskopt | MASK | Per-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_modeopt | COMBO | off | Sample 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_scaleopt | FLOAT | 1.501–4 | Box 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_byopt | INT | 328–128 | — |
| uncrop_featheropt | INT | 160–256 | 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_backopt | BOOLEAN | true | 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_maskopt | BOOLEAN | true | Composite 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_maskopt | BOOLEAN | true | Send 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_growopt | INT | 80–256 | Dilate the mask before use, in pixels. The new head can be bigger than the old one. |
| mask_bluropt | INT | 40–256 | Soften the mask edge, in pixels, to avoid a hard seam. |
| mask_hard_for_inpaintopt | BOOLEAN | true | Binarise 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_dilateopt | INT | 00–16 | Grow 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_framesopt | INT | 00–8 | Same, along time: grow by whole latent frames (one covers 8 video frames). |
| mask_strengthopt | FLOAT | 1.000–1 | How 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. |
| decodeopt | COMBO | full | How 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_sizeopt | INT | 76864–4096 | — |
| decode_overlapopt | INT | 640–1024 | — |
| decode_temporal_sizeopt | INT | 324–4096 | Frames decoded at once in tiled mode. |
| decode_temporal_overlapopt | INT | 40–256 | — |
| temporal_tile_sizeopt | INT | 00–1000 | Frames 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_overlapopt | INT | 160–256 | — |
| guide_source_idopt | FLOAT | 10–8 | — |
| identity_source_idopt | FLOAT | 20–8 | — |
| debug_logopt | BOOLEAN | false | — |
| auto_configopt | BOOLEAN | false | Measure 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_headroomopt | FLOAT | 1.151–2 | auto_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)
| Name | Type | Description |
|---|---|---|
| images | IMAGE | Final frames, with the crop pasted back when cropping is on. |
| mask_over_source | IMAGE | The 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_guide | IMAGE | Exactly 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_mask | MASK | The mask after grow/blur (and cropping), in the crop's pixel space. |
| latent_mask | MASK | 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. |
| latent | 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. |
| crop_bboxes | BOUNDING_BOX | The crop boxes, one per frame. Feed to Head Swap Paste Back after a second pass. |
| debug | STRING | What the node decided: crop mode and box, mask ops, tiling. |