Nodes/ComfyUI-BFSNodes/BFS Shot Join
ComfyUI Node

BFS Shot Join

Put the shots back together and keep the soundtrack

By alisson-anjos·Created 7 months ago·Updated about 10 hours ago· 129
BFS Shot Join
  • images
  • shots
  • audio
  • timeline
  • images
  • audio
  • fps
  • comparison
  • video_path
  • guide
◄crossfade4►
◄comparisonfalse►
◄label►
◄comparison_height360►
◄edge_hardness0.00►
◄match_colors0.00►
◄match_regionaround the subject (swap)►
◄seamless_edgestrue►
◄paste_new_outlinetrue►
◄comparison_masktrue►
◄keep_colorsfalse►
◄keep_colors_strength1.00►
◄save_toimages (output)►
◄filename_prefixbfs_shot_loop►
◄mp4_crf17►

The other half of the trick

Splitting one long video into shots is the easy part. Joining the results back into something that plays like the original is where shot-based video generation usually goes wrong: the model-valid lengths don't match the shot lengths, so every shot has extra frames hanging off the end, and a naive concatenation gives you a video that drifts a few frames later than the source with every shot.

This node knows better because the planner told it. Each generated shot gets trimmed to its true length - the shot's own frames, not the grid-valid length that was rendered - so the total timing matches the source exactly. The frames it trims off are the overlap the planner deliberately generated from the following video, and crossfade uses them: boundaries that aren't camera cuts get blended across instead of butted together. Camera cuts stay hard, which is what you want; a 4-frame dissolve on a cut looks like a mistake.

The inputs

Required: images (the decoded frames of every shot - the list that came out of VAE Decode), shots (the list straight from the planner, so the join knows the true lengths and where the cuts are), and crossfade (0–24 frames, default 4; 0 for hard joins everywhere).

Optional, and all three matter:

  • audio - feed the planner's audio output and the soundtrack comes back with the video, already trimmed. Without it you get no audio output at all, not silence.
  • timeline - the planner's timeline output. This is the one people forget. With it, shots that were skipped (disabled by a filter, or by you) get filled with the original video, and the audio follows the same fill-and-drop rules. Without it, your skipped shots just vanish from the output and nothing warns you.
  • comparison + label + comparison_height - a side-by-side review render: original shot | references | result, with the shot's info and your label text (model, LoRA, steps, seed - whatever you write) on the top bar and the prompt underneath. This is genuinely the nicest feature in the shot loop for evaluation, and comparison_height (default 360, 0 for full size) is what keeps it from eating your VRAM on a long video.

Outputs: images (the joined video), audio, fps (so you can hand it straight to Create Video), and comparison.

How it behaves in queue loop

It's an INPUT_IS_LIST node that blocks every node after it until the last shot is done. That's why queue loop can run one shot per queue run, store each on disk, and still have a single Save Video at the end fire exactly once. It also means a long join feels like nothing is happening for a while, then everything happens - don't kill the run.

Install

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

Restart. Reading audio from the source video uses ffmpeg, falling back to PyAV when the ffmpeg binary is missing - the pack's own docs are upfront about that, and it's the first thing to check if the planner reports no usable audio track.

Where it goes wrong

crossfade on a cameraman's hard cut reads as a smear; leave the blend for the soft joins and set 0 if your material is full of fast cuts. Feeding images from a sampler instead of the decode means the shot count won't line up with shots and the trim lengths will be applied to the wrong thing. And the classic: no timeline wire, three shots skipped by the "needs a person" filter, and a final video that's nine seconds shorter than you planned with no error anywhere.

CategoryBFS/shot loop

Inputs (19)

NameTypeDefaultDescription
imagesIMAGEDecoded frames of every shot (the list from VAE Decode). Not requested when every shot is already stored: nothing is generated again.
shotsBFS_SHOTThe shots list from BFS Shot Planner.
crossfadeINT40–24Frames to blend across boundaries that are not camera cuts, using the overlap each shot generated past its end. 0 = hard joins everywhere.
audiooptAUDIOSoundtrack to return with the video (e.g. the planner's audio output).
timelineoptBFS_SHOT_TIMELINEThe planner's timeline. With it, shots that did not run are filled with the original video (or dropped, per the planner's setting) and the soundtrack follows.
comparisonoptBOOLEANfalseAlso output a side-by-side video: original shot | references | result, with the shot's info on top and its prompt below.
labeloptSTRINGExtra text for the comparison's top bar, e.g. the model, LoRA, steps and seed.
comparison_heightoptINT3600–4096Height of each column in the comparison (0 = full size). Smaller is much lighter for long videos.
edge_hardnessoptFLOAT0.000–1Cropped / Frame + paste shots: harden the soft edge of the paste (0 = as feathered, 1 = hard cut). Raise it when a faint ghost of the original person shows around the new one. From NKD Inpaint Stitch.
match_colorsoptFLOAT0.000–1Cropped / Frame + paste shots: correct the colour / brightness drift of the generated patch (Reinhard in LAB, statistics over the whole shot, so no flicker). 0.5-1 typical. From NKD Inpaint Stitch.
match_regionoptCOMBOaround the subject (swap)Where the colour statistics are measured. 'around the subject (swap)': a ring of background around the mask, so a new person keeps their own colours and only the scene's light drift is fixed. 'inside the subject': inside the mask (retouching the same person/content).
seamless_edgesoptBOOLEANtrueCropped / Frame + paste shots: Poisson blend (OpenCV seamlessClone) for stubborn seams. Slower; can shift colours near the edge. From NKD Inpaint Stitch.
paste_new_outlineoptBOOLEANtrue'Frame + paste' shots: also paste where the NEW subject is (SAM 3 on the result near the original one, with the shot's 'new subject' text from the planner, default 'person'), so a bigger silhouette (hair, body) is not cut. Off = only the original mask (+ expand).
comparison_maskoptBOOLEANtrueIn the comparison, shots with a crop or a generation mask (Mask only / Crop + mask) show the selected mask over the original column; Colored Mark uses its selected colors, other masks use red. Crop boxes are yellow.
keep_colorsoptBOOLEANfalseShot by shot / queue loop with continuity: shots that continue from the previous one can drift in colour over a long take. On: the new subject's colours (chroma) of every continuing shot are matched to the first shot of the take before it is stored (the background and the lightness are not touched; a camera cut or a shot without continuity starts a new take). Off by default: the match is one shift for the whole shot, so with continuity 'head' (whose first frames already continue the previous shot) it shows as a colour jump at every seam. Turn it on only for a long take whose colours visibly drift.
keep_colors_strengthoptFLOAT1.000–1How much of that correction is applied (1 = fully to the first shot).
save_tooptCOMBOimages (output)images: output the video as IMAGE (connect Create Video / Save Video); the whole video is held in memory. mp4 file: the join writes the video (and the comparison) straight to MP4 files in the output folder, one shot at a time, so memory stays at about one shot: use it for long videos. The images and comparison outputs still carry the video, read back from the files at a size that fits the free RAM (the files keep the full size), the node shows the files, and video_path gives the path.
filename_prefixoptSTRINGbfs_shot_loopmp4 file: the file name in the output folder (a counter is added).
mp4_crfoptINT170–51mp4 file: H.264 quality (lower = better and bigger; 0 = lossless, 17 = visually lossless, 23 = ffmpeg's default).

Outputs (6)

NameTypeDescription
imagesIMAGEThe joined video.
audioAUDIOIts soundtrack.
fpsFLOATIts frame rate.
comparisonIMAGEoriginal | references | result, side by side (needs 'comparison').
video_pathSTRINGThe MP4 written in 'mp4 file' mode (empty otherwise).
guideIMAGEThe original frames under each output frame, same length and timing as 'images', at comparison_height (needs 'comparison'): connect it with 'images' to BFS Video Compare.