BFS H3 Duet Conditioning (prompt writer)
The duet panel, without the sampler
- clip
- vae
- panel
- vlm
- model
- audio_vae
- guide
- keep_mask
- keep_video
- ref_images
- setting
- positive
- latent
- model
- panel_info
- prompt
- canvas_preview
What this one exists for
BFS H3 Duet is the friendly version: it builds the conditioning, pins the panel, samples and decodes, all behind one node. That's great until you want something the author didn't anticipate - a different guider, a LoRA you load yourself, an inpainting pass, your own noise. So there's this: the same pinned-panel conditioning, handed to you as conditioning to wire into whatever sampler you like.
Use it when you care about the graph. Use the all-in-one when you just want the result.
What it hands you
The node pins a panel (your source clip, or a still) beside the video inside the same latent, then outputs positive, latent, model, panel_info, prompt and canvas_preview.
That model output is the part that's easy to miss. In rope_mode = shifted the RoPE shift is applied to the model, so you must connect the model through this node and use the output - feed the original model to your sampler and the conditioning and the model will disagree about where things are. In the default canvas mode the model passes through untouched. The chain people run:
BFS H3 Duet Conditioning ─ positive ─► BasicGuider / SamplerCustomAdvanced
─ latent ─► (same sampler)
─ panel_info ─► BFS H3 Side Panel Crop (feed it the sampled latent)
─ model ─► sampler (shifted mode)
BFS H3 Side Panel Crop takes panel_info plus either the sampled latent or decoded images, so crop the latent before VAE Decode - same picture, less work.
The prompt, and the VLM
You get three tiers, in order of who wins: your own text in prompt; otherwise a connected VLM; otherwise a draft.
vlm is a normal CLIPLoader running a Qwen3-VL text encoder (the same encoder family H3 uses). Connect it and the node asks it to write a full duet prompt for task + instruction while looking at the clip and the references - genuinely useful, because the draft path can't see anything and therefore produces generic text. The prompt it writes is a six-section REF2VA prompt, and the result comes out on the prompt output so you can copy the good ones into your own prompt box and stop paying for the VLM.
task picks the shape of the request (character swap, style, setting, appearance, lighting/weather), and instruction is the one useful sentence about what changes - "a 1990s anime cel style", "an elderly woman with grey hair". For a straight character swap you can leave instruction empty, since the person comes from the references.
The prompt-writing rules are the same as the all-in-one node and they're the difference between a good result and a mushy one: the panel has no tag, it is invisible to the text encoder, so refer to it by position ("the LEFT half is the kept footage") or drop {layout} in and let the node fill it for the current side and size. Never describe the panel's performer, wardrobe or room - not even to say what it isn't. Anything you leave unspecified is copied from the panel, which is the whole point.
Inputs worth knowing about
clip,vae,panelare required;panelis the clip you pin (one image per frame for a clip, or a single still for a static panel).width/height/length- output geometry;length0 follows the panel clip, and frames snap to17k+5.panel_noise- 0 copies the panel exactly. 0.1–0.2 gives the model room for big changes.position,size,fit,gap,hold- where the strip goes, how big, how the clip is fitted into it (containkeeps the whole frame with grey around it;covercrops), the grey separator width, and whether the panel is held for the whole clip or just its first latent frame.rope_mode/rope_gap-canvasorshifted. In shifted mode keeprope_gaptiny; a large gap against the video's width makes H3 draw its own split-screen.keep_mask+keep_video- the inpainting door. With a mask, the video area starts fromkeep_video(or the panel clip) and only the masked region is regenerated, so background and lighting stay pixel-exact. This is how you swap a person without the room drifting.- Optional
guideis an aligned latent guide in the video area, andref_imagesgrows as you connect reference pictures - they become the<Picture n>slots your prompt refers to.## Install
Manager → search ComfyUI-BFSNodes, or:
cd ComfyUI/custom_nodes
git clone https://github.com/alisson-anjos/ComfyUI-BFSNodes
cd ComfyUI-BFSNodes && pip install -r requirements.txt
Then restart. You need an H3 build of ComfyUI and the H3 model + VAE; the pack's other dependencies (insightface==0.7.3, onnxruntime, scenedetect, librosa, opencv-python) come along whether or not you use them.
If something's off
If BFS H3 Duet Conditioning isn't in your node list at all while BFS H3 Side Panel is, your ComfyUI predates the comfy_api.latest node interface - the pack registers these two through it and skips them quietly when the import fails. If you're in shifted mode and the frame looks like a genuine split screen with a seam, you almost certainly didn't route the model output into your sampler, or rope_gap is too large. And if the VLM writes something bland, that's usually the encoder: the pack's own docs warn that some smaller or modified Qwen3-VL builds return empty answers to certain wordings.
Inputs (31)
| Name | Type | Default | Description |
|---|---|---|---|
| clip | CLIP | The MiniMax H3 text encoder (Qwen3-VL 32B). | |
| vae | VAE | — | |
| panel | IMAGE | The clip pinned beside the video (copied in sync), or a picture. | |
| task | COMBO | character swap | What to do. With a VLM connected it writes the prompt for this task. |
| instruction | STRING | What changes, in seen words: 'a 1990s anime cel style', 'a sunny beach at sunset', 'an elderly woman with short grey hair', or for custom anything you want done. Empty is fine for character swap (the person comes from the pictures). | |
| prompt | STRING | Your own prompt (wins over the VLM and the draft). How to write a duet prompt (TSC, newer strategy): H3 has no idea what is pinned and what is noised, it reads one description of one video. Describe the finished split screen as plain footage: never write 'kept footage', 'kept exactly', 'generated', 'preserved', 'in sync with', 'seamlessly'. The pinned clip gets no tag and no retention line: name its region by where it is plus what is seen there, in one clause ('the top quarter shows a man dancing in a small bedroom'; {panel_region} / {video_region} give the region names for the current layout, {layout} the whole split-screen sentence). Write the other region as its own scene in a real place with concrete details. Spell out the motion beat by beat, with times for the big moments (cuts, strikes, jumps, a line), on the video's clock. The soundscape is the sounds of the new scene. Cuts: '[Shot 2] At MM:SS.mmm, both halves cut together to ...'. | |
| width | INT | 44832–4096 | — |
| height | INT | 80032–4096 | — |
| length | INT | 00–3600 | Frames (17k+5). 0 = the panel clip's length. |
| position | COMBO | left | 4 options: top, left, right, bottom |
| size | FLOAT | 1.000.1–1.5 | — |
| panel_noise | FLOAT | 0.000–1 | 0 pins the panel exactly; 0.1-0.2 for big changes. |
| rope_mode | COMBO | canvas | canvas (one wide grid) or shifted (connect the model and use the model output). |
| fit | COMBO | contain | 3 options: contain, cover, stretch |
| gap | INT | 00–8 | — |
| hold | COMBO | all frames | 2 options: all frames, first latent frame |
| rope_gap | FLOAT | 00–256 | — |
| ref_image_size | COMBO | match | 2 options: match, max |
| vlm_max_tokens | INT | 1024128–4096 | — |
| vlmopt | CLIP | Optional VLM (CLIPLoader with a Qwen3-VL text encoder). Empty prompt: it writes one from the task + instruction. Prompt with {...} fields: it fills them from the clip and the pictures. Prompt without fields: used as written. | |
| modelopt | MODEL | Needed for rope_mode = shifted. | |
| audio_vaeopt | VAE | — | |
| guideopt | IMAGE | Optional aligned latent guide in the video area. | |
| keep_maskopt | MASK | Inpainting inside the duet: 1 = regenerate (e.g. the person, dilated), 0 = keep. The video area starts from keep_video (or the panel clip) and only the masked region is generated, so the background and its light stay pixel-exact. | |
| keep_videoopt | IMAGE | The video kept outside the mask (default: the panel clip). | |
| ref_imagesopt | COMFY_AUTOGROW_V3 | — | |
| source_bluropt | COMBO | off | Blur the source person in what the model sees (the panel / the guide): '<region> (<what stays sharp>)'. keep mouth: lips, teeth and tongue stay sharp; keep lips + tongue: the teeth are blurred too; mouth too: nothing stays sharp. masked area: the shot's own mask (elsewhere: the whole person). It reads the pose and motion but has no face to copy. The output is never blurred. Works with or instead of panel_static. |
| source_blur_segmenteropt | COMBO | SAM 3.1 | What finds the face / head / hands / person and the mouth for source_blur. SAM 3.1: text prompts. Sapiens2: body-part classes in one pass, finer lips and teeth (downloads facebook/sapiens2-seg-0.4b, 1.6 GB, to models/sapiens2 the first time). 'keep lips + tongue' always uses Sapiens2 for the lips. |
| source_blur_effectopt | COMBO | blur | blur: no detail left. invert colours: keeps the shapes and the expression but not the skin tone or colours. |
| panel_staticopt | COMBO | off | TSC's fix for swaps in the same world: cover the face (or the whole person) of the pinned clip in TV static, every frame (SAM 3), so the generated half cannot copy it and takes the identity from the reference. The output is the generated half only, so the static never shows. |
| settingopt | IMAGE | The place picture (BFS Setting Picture: a frame of the clip with the person in static). It becomes the last <Picture n> and the prompt says it shows the setting (write {setting} to place its tag). |
Outputs (6)
| Name | Type | Description |
|---|---|---|
| positive | CONDITIONING | — |
| latent | LATENT | — |
| model | MODEL | — |
| panel_info | BFS_H3_PANEL | — |
| prompt | STRING | — |
| canvas_preview | IMAGE | — |