BFS Video Compare plays two videos in one player (wipe, overlay or side by
side, any of guide / target / difference on top) and measures a per-pixel
difference map on the real frames, with the difference per frame as a
clickable strip. Shot Join gains a 'guide' output: the original frames under
each output frame (the comparison's original column), so the two connect
straight into the compare node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Planner: Mode "One shot (whole range)" (the range as a single shot, Max seconds / shot not applied) and
"Range length, seconds" next to it, so a whole shot or pieces of at most X are set in the Video tab alone.
- BFS Shot Seed: one seed per shot from a base seed (same / increment / random per shot) with per-shot overrides.
- BFS Shot Noise (experimental, optional): starting noise for SamplerCustomAdvanced on H3's video + audio latent;
every amount at 0 is exactly RandomNoise. shared_base (the coarse pattern from a base seed, the same across shots,
resolutions and lengths), static_share, distribution, spectrum_tilt, pyramid, guide_structure, ref_tint.
- BFS Sigmas Preview: chart of a sigma schedule (steps on layout / shapes / detail), pass-through.
Tested: split logic, seed logic, noise statistics (unit variance, identity at 0) and the chart, in Python.
Not tested yet inside ComfyUI / the panel in a browser.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A video model only follows a guide inside the clip length it was trained on, so long
sources are split into shots that fit and run once per shot.
- BFS Shot Planner: interactive Vue timeline (thumbnails, cut-detector curve, draggable
boundaries, split/merge, per-shot reference gallery with upload, per-shot prompt).
Splits at camera cuts via PySceneDetect (adaptive/content, built-in fallback), fixed
length, or by hand; lengths snap to the model grid (H3 17n+5, LTX/Wan 8n+1, 4n+1).
Outputs the shots as a ComfyUI list so everything downstream runs once per shot.
- BFS Shot Unpack: guide frames, references, prompt, length, audio of one shot.
- BFS Shot H3 Conditioning: native MiniMax H3 conditioning per shot (aligned guide,
native reference video, or both).
- BFS Shot Join: concatenates in order, trims each shot to its true length, cross-fades
soft joins with the real overlap frames, returns the soundtrack.
- Queue loop mode: one shot per run stored on disk, auto re-queue, nodes after the join
blocked until the last shot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A defect a LoRA learned — smoothed skin, identity drifting at distance, a pose it
refuses to copy — usually lives in one module family and a range of blocks, not in the
whole adapter. This node makes that testable without retraining: pick a family, pick
blocks, set a scale, queue. Nothing is written to disk, so iterating is a slider away.
Model-agnostic by construction. The structure is discovered from key names rather than
hardcoded, covering the diffusers (lora_A/lora_B) and kohya (lora_down/lora_up + alpha)
conventions. Verified against three architectures with different naming: Qwen-Image-2.1
(transformer_blocks, 32), Krea 2 (blocks 28 + txtfusion) and MiniMax-H3 (blocks 50 +
token_refiner).
Scaling multiplies only the up/B factor, since (sB)A = s(BA) — exact under either alpha
convention, and negative scales stay usable.
The panel is a Vue app in a DOM widget, with Vue vendored so it works offline. It lists
each family's module types sorted by ||dW||, which reads as a map of where training
invested. Selections convert to a regex on demand for cases the tree cannot express.
Rules are ordered and later ones win, so the UI says so and lets you reorder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
The LoRA keeps doing only its job -- guide on source_id 1, identity on
source_id 2. The mask never reaches it: it becomes latent["noise_mask"],
which the guider consumes as denoise_mask, so only the masked region is
denoised and the original face stays visible to the model.
Masks are reduced to the latent grid with MAX rather than ComfyUI's
trilinear resize, which blurs a mask across frames and lets the original
content bleed through the edit.
Crop exists for the case a LoRA cannot fix: a face too small to carry
identity. tracked/zoomed borrow drozbay/MaskVidExperiments' planner when
that pack is installed -- its boxes hold still through mask noise and
occlusion, and a jittering crop reads as camera motion to a video model.
Without it, one static box around the subject's whole travel.
Helpers are unit-tested; the sampling path still needs a GPU run.
Two formatting rules the VLM breaks consistently, and that no amount of prompt
work fixed — five rounds of rewriting plus temperature 0 still produced the same
violations:
no reference -> the marker sentence must not exist, and a swap is a plain
Replace. The model emits Add + marker + Remove instead, which
is the reference form applied where it does not belong.
reference -> the marker may only follow a sentence starting with Add. When
it lands on a Replace, that pair appears in zero training
samples.
Both are decidable in code: the caller already knows whether it attached a
reference image, so the node takes that boolean rather than asking the model to
infer it. Add+Remove is recomposed into Replace, and Replace+marker into
Add+Remove, preserving the object description the VLM did get right.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The stock SaveLatent calls .contiguous() straight on samples["samples"], which
fails on H3 because the latent is a NestedTensor holding the video and audio
components. This unbinds them first and writes each as its own tensor, so an H3
latent can be taken out of the graph and decoded or inspected offline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Decoding a DiT-generated H3 latent through the stock VAEDecode produces a visible
grid. The reference path does not use vae.decode() at all: both the checkpoint
card and the multimodalart/MiniMax-H3-images Space call the decoder directly.
This node is that recipe, with the four details its author flags as silently
corrupting if changed:
1. vae.decoder(vae.post_quant_conv(z)) directly, never vae.decode(), whose
temporal chunker cannot form a chunk from the few latent slices an image
generation produces;
2. decoded[:, :, -1] -- the last output slice, not index 0;
3. latent denormalisation, z * latents_std + latents_mean;
4. ImageNet pixel denormalisation, without which the frame comes out dark and
over-contrasted like a tone-mapping bug.
Reconstruction check against a real image: 22.43 dB, block ratio 1.30 at the
32px patch boundary -- matching the raw decoder path measured separately, which
is what confirms the node reproduces the reference rather than approximating it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ports the sharpest-frame selection from scanpe_experiments/h3_image.py, which
takes it from IF-Edit (arXiv:2511.19435): decoded frames carry different amounts
of residual motion blur, so score by Laplacian variance and keep the crispest
instead of a fixed index.
Pairs with running MiniMax-H3 as an image generator: the VAE's minimum clip is 5
pixel frames (2 latents), decode yields frames of uneven sharpness, and the best
one is not always at the same index.
The node documents a limitation found while testing it, because it is easy to
misuse: Laplacian variance is a BLUR heuristic, not a quality metric -- artifacts
are high-frequency too. On a real H3 decode it picked the frame with the WORST
PSNR of its block (19.72 dB against 23.19 for its neighbour) because that frame
was the most artifacted. It is sound when the candidates are the same content
differing only in blur, which is the IF-Edit setting; it is meaningless for
choosing among frames from different moments.
Also relevant and not obvious: the "last frame of each decoded block is the valid
one" rule (decoded[:, :, -1] in the upstream example) held for the first block
here (34.49 dB against 26.80 for its neighbour) but inverted on the second.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The published single-frame decoder (iamkaikai/MiniMax-H3-Single-Frame-VAE-500K)
cannot be loaded by the stock VAELoader: detection in comfy/sd.py requires an
encoder key, and the file is decoder-only in diffusers naming, so it falls back
to Stable Diffusion geometry (4 latent channels, 8x upscale) and decodes blocks.
Two nodes:
- MiniMaxH3SingleFrameVAELoader: combines the official H3 VAE with the author's
decoder-only checkpoint, translating the naming in memory. Validated against
the author's own diffusers path (load_decoder.py) on the same latent: 72.92 dB,
i.e. fp16 noise.
- MiniMaxH3VAELoaderTiled: loads an already complete H3 VAE and exposes tile_size,
which the stock loader does not.
Two key conversions are not derivable from names, and getting either wrong
produces wrong output with no load error. Both were settled by correlating
against the official VAE:
- to_qkv is interleaved per head (heads, 3, dim_head), not stacked [q;k;v]
(see qkv.view(B, S, -1, 3*dim_head) in comfy/ldm/minimax/vae.py); corr 0.998.
- ff.w1 halves are swapped relative to diffusers, since ComfyUI reads gate first;
correlation: straight -0.013, swapped +2.00.
tile_size defaults to 512 rather than the constructor's 256. Measured on a
1056x640 image: 512 gives 26.00 dB with a seam ratio of 0.91 (indistinguishable),
against 22.17/1.49 at 256 and 21.35/2.42 at 1024. The author trained 475k of
500k images at <=512px, so 512-wide tiles keep each piece inside the regime that
has training mass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three nodes for checkpoints trained with several references on distinct slots:
- LTX Reference Images collects up to six references into one ordered list. A
plain IMAGE batch cannot carry references of different sizes, and subject
references routinely differ — a portrait crop next to a landscape product
shot — so the list keeps each at its own size and puts the slot order in one
visible place.
- LTX Multi Reference Slots attaches them as separate reference blocks, each
with its own source_id and, when the LoRA carries one, its trained slot
embedding. The embedding is read straight from the LoRA file: ComfyUI's LoRA
loader applies only LoRA weights and drops everything else, so without this
the references are untagged and the checkpoint is sampled without a signal it
was trained to use. Pointing it at a LoRA that has no slot embedding fails
with an explanation rather than silently ignoring the setting.
- LTX Reference Tags builds the "<Image 1> is the woman." prompt prefix.
Nothing in the model ties a tag to a slot — that correspondence exists only
because the training captions used it consistently, so sampling with a
different convention simply does not bind.
The identity node gains one hook: a spec may now carry a slot_vector, applied
to the raw patchified tokens before patchify_proj, which is exactly where the
trainer adds it. Specs without the key are untouched, so existing workflows
behave identically.
Builds a ShotPlan-format multishot prompt (global preamble + "Shot N: <modifier>
<size> <move> <action>" blocks) plus the ordered, gray-padded reference batch that
the multishot strata LoRA expects.
No new conditioning node is needed: LTX Identity Transfer already assigns
seg_value = (source_id + i) * phase_scale and strata_slot = i over the image batch,
so source_id=2 with layout="strata" over 4 refs reproduces the trained convention
(source_id 2/3/4/5 on slots 0/1/2/3). Batch order is shot order -- the LoRA was
trained with shuffle_reference_slots=false. Unused slots get a flat RGB-128 frame,
matching the dataset's blank.jpg placeholder.
The dropdown vocabularies are extracted from the 12,470 "Shot N:" headers of
Pensioner/shotplan train_meta_16fps.json (the span between "Shot N:" and the start
of the action sentence), covering 98% of size terms and 97% of move terms.
Modifiers (over-the-shoulder, low/high angle, POV) are a separate axis that
combines with a size rather than replacing it, as in the source captions.
Bump version to 1.21.1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LTX Multiple Controls now supports a 4th slot (identity_mask_image) for
scail2v2-style color-pointer masks, defaulting to inherit the identity
slot's own source_id/phase_scale (matches training convention). Also
updated guide/mask/identity default source_ids to match the scail2v2
3-phase recipe (1/2/3 instead of 0/0/2).
New LTX Color Mask node recolors plain white/binary SAM2-style masks
into the trained convention: 'silhouette' style (recolor, keep shape)
for the guide mask slot, or 'flat_marker' style (recolor + collapse to
a small dot at centroid, no shape) for the identity mask slot -- a
shaped identity mask re-injects a competing pose signal, the same bug
flatten_ref_masks.py fixes on the training-data side.
Bump version to 1.18.0.
Chaining multiple LTXIdentityOverlapConditioning nodes silently drops all
but the last one's reference -- each apply() overwrites transformer_options
["_id_ref_specs"] instead of merging with what the upstream node set. This
node builds every active slot's ref_specs in a single call so guide, mask,
and identity references all combine correctly. Each slot (all optional) has
its own source_id/phase_scale/layout/ref_resize_mode, defaulted (not
hardcoded) to match the trained recipe.
Same grid convention as the multi-ref LTX-2 LoRA training pipeline
(build_multiref_sheet.py): deterministic layout by ref count (1x1, 2x1,
3x1, 2x2, 3-top+2-bottom), 512x512 panels, centered onto a fixed
1536x1024 canvas regardless of how many refs are plugged in. Panel
order matches the image0/image1/... indexing used in training.
Copy of LTX Identity Transfer generalized to N reference blocks: reference_face_front
(required, sid 2, drives ArcFace projector) + optional reference_back_head (sid 3),
reference_body_front (sid 4), reference_side_profile (sid 5). Empty optional inputs are
omitted, matching the multi-view training's view-dropout. Distinct patch flags so it
never collides with the single-ref node.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Empirically the AdaLN (CAN) modulation destroys the generation; the identity gain came from the
overlap + projector + LoRA, not the CAN. Reverted the Identity Transfer node to overlap + projector,
unregistered the standalone CAN node (file kept).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ArcFace projector (appended text tokens) has ~0 impact (cross-attn can ignore them). CAN
injects identity into the self-attn AdaLN (shift+gate) of even blocks — a channel the model can't
ignore. Trained on top of the reference LoRA it lifts identity a lot (humo 0.52 -> 0.69) with no
first-frame leak. Node rebuilds CANModulation per even block from identity_adapters_*.safetensors
(can.<i>.*), extracts the reference ArcFace, and patches each even block's native forward with
shift_msa += dshift; gate_msa += tanh(dgate) — the exact training formula. Lazy imports; defensive
__init__. Weight-load + CAN forward validated headless; block patch untested in ComfyUI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Feeds the reference image through the ALREADY-LOADED ComfyUI CLIP (LTX's own multimodal Gemma 3
vision) -> feature_extractor -> connector -> CONDITIONING. Identity is in Gemma's native space
(the reason foreign CLIP/DINO/ArcFace adapters fail on LTX); no reference latent (no copy-paste/
mask), no DiT surgery. Reuses the loaded CLIP (only loads a lightweight AutoProcessor for the
multimodal chat template). Mirrors ComfyUI-LTXVideo LTXVGemmaTextEncoderModel, run multimodally.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BFSAmvGuideBuilder builds the keyframes+blank guide for the AMV av2av LoRA:
auto_scene / manual_indices / external_images, with audio-beat placement
(librosa beat_track, RMS-onset fallback) to sync cuts to the music. Adds
librosa to requirements.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pure v2v anime->real conditioning for the Bernini-R anime2real LoRA
(trained without reference). Mirrors training: guide -> source_id 1 on
both positive and negative, empty latent -> source_id 0, no source_id 2
and no amplify_reference (no reference to amplify; text-only CFG).
Reuses bernini_patches; registered in __init__.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Standalone head-swap nodes for the Bernini-R LoRA (Wan2.2-A14B):
- HeadSwapBerniniConditioning: guide video -> source_id 1, head image ->
source_id 2; output follows the guide's native size, head keeps its own.
- HeadSwapLoRADebug: reports applied LoRA patch count on a MODEL.
Bundles the WanModel context_latents patch (vendored from ComfyUI-RH-Bernini,
GPL-3.0) so no external Bernini node package is required.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>