From e67b22a0ea78df09feaa8549e5f8549c9856aaac Mon Sep 17 00:00:00 2001 From: alisson-anjos Date: Fri, 2 Oct 2026 20:10:32 +0000 Subject: [PATCH] Shot loop: BFS Shot Repack to put edited pieces back into a shot (1.48.2) Unpack -> any processing (e.g. remove the reference background) -> Repack. Unconnected inputs keep the shot's values; replacement guide frames are fitted to the shot's generation size and length, and timing, cuts and audio are unchanged so the join still works. Runs once per shot like the rest of the loop. Co-Authored-By: Claude Opus 5.5 --- bfs_shot_loop.py | 48 +++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- tests/test_bfs_shot_loop.py | 16 +++++++++++++ web/docs/BFSShotPlanner.md | 1 + 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/bfs_shot_loop.py b/bfs_shot_loop.py index 7cdfa24..b3c7073 100644 --- a/bfs_shot_loop.py +++ b/bfs_shot_loop.py @@ -710,6 +710,52 @@ class BFSShotUnpack: shot["frames"][:1], audio, shot["width"], shot["height"], shot["index"]) +class BFSShotRepack: + """Put edited pieces back into a shot (runs once per shot), e.g. a reference with its background removed.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": {"shot": ("BFS_SHOT",)}, + "optional": { + "guide_frames": ("IMAGE", {"tooltip": "Replacement guide frames. Resized to the shot's generation " + "size; padded or trimmed to its length."}), + "ref_image": ("IMAGE", {"tooltip": "Replacement reference (e.g. background removed)."}), + "ref_image_2": ("IMAGE", {"tooltip": "Replacement second reference."}), + "prompt": ("STRING", {"forceInput": True, "tooltip": "Replacement prompt."}), + }, + } + + RETURN_TYPES = ("BFS_SHOT",) + RETURN_NAMES = ("shot",) + FUNCTION = "repack" + CATEGORY = "BFS/shot loop" + DESCRIPTION = ("Rebuild a shot after editing its pieces (Unpack -> any processing -> Repack). Inputs left " + "unconnected keep the shot's original values; timing, cuts and audio are unchanged.") + + def repack(self, shot, guide_frames=None, ref_image=None, ref_image_2=None, prompt=None): + out = dict(shot) + if guide_frames is not None: + fr = guide_frames + H, W = shot["height"], shot["width"] + if tuple(fr.shape[1:3]) != (H, W): + arr = (fr.clamp(0, 1) * 255).round().to(torch.uint8).cpu().numpy() + fr = torch.from_numpy(np.stack([_fit(a, (W, H)) for a in arr]).astype(np.float32) / 255.0) + n = shot["gen_length"] + if fr.shape[0] != n: + print(f"[BFS Shot Repack] shot {shot['index'] + 1}: guide has {fr.shape[0]} frames, " + f"expected {n}; {'trimming' if fr.shape[0] > n else 'holding the last frame'}") + fr = fr[:n] if fr.shape[0] > n else torch.cat([fr, fr[-1:].expand(n - fr.shape[0], -1, -1, -1)], 0) + out["frames"] = fr + if ref_image is not None: + out["ref"] = ref_image[:1] + if ref_image_2 is not None: + out["ref2"] = ref_image_2[:1] + if prompt is not None: + out["prompt"] = prompt + return (out,) + + class BFSShotH3Conditioning: """Native MiniMax H3 conditioning for one shot: references, prompt and the shot as a guide.""" @@ -930,12 +976,14 @@ BFSShotJoin._join_timeline = _join_timeline_impl NODE_CLASS_MAPPINGS = { "BFSShotPlanner": BFSShotPlanner, "BFSShotUnpack": BFSShotUnpack, + "BFSShotRepack": BFSShotRepack, "BFSShotH3Conditioning": BFSShotH3Conditioning, "BFSShotJoin": BFSShotJoin, } NODE_DISPLAY_NAME_MAPPINGS = { "BFSShotPlanner": "BFS Shot Planner", "BFSShotUnpack": "BFS Shot Unpack", + "BFSShotRepack": "BFS Shot Repack", "BFSShotH3Conditioning": "BFS Shot H3 Conditioning", "BFSShotJoin": "BFS Shot Join", } diff --git a/pyproject.toml b/pyproject.toml index 9b439a8..f8edb15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "bfsnodes" description = "Custom ComfyUI nodes for reference-guided video: BFS Sampler (guide-driven IC-LoRAs with auto crop, mask inpainting and temporal chunking), LTX Multiple Controls and Identity Transfer, LTXV Edit Anything, Head-Swap/Anime2Real conditioning, AMV Guide Builder, MiniMax-H3 downscaled latent guides with training-matched geometry, overlap/sidecar source-phase RoPE, single-frame/video targets and VAE loaders, LoRA Surgery (scale or drop a LoRA's layers live to find a defect, save the result), and Shot Planner / Shot Loop (split long videos into model-sized shots at camera cuts with PySceneDetect, YOLO person/face filters, a reference and prompt per shot, run any workflow per shot, join with the original timing and audio)." -version = "1.48.1" +version = "1.48.2" license = {file = "LICENSE"} dependencies = [ "torch", diff --git a/tests/test_bfs_shot_loop.py b/tests/test_bfs_shot_loop.py index a1c205b..07e7d1a 100644 --- a/tests/test_bfs_shot_loop.py +++ b/tests/test_bfs_shot_loop.py @@ -176,5 +176,21 @@ class TimelineJoinTest(unittest.TestCase): self.assertAlmostEqual(float(video[30, 0, 0, 0]), 128 / 255, places=3) + +class RepackTest(unittest.TestCase): + def test_replaces_only_connected_pieces_and_fits_the_guide(self): + shot = dict(_shot(0, 0, 20, gen=22), width=8, height=8, frames=torch.zeros(22, 8, 8, 3), + ref=torch.zeros(1, 4, 4, 3), ref2=None, prompt="old", audio=None) + ref = torch.ones(1, 6, 6, 3) + out = SL.BFSShotRepack().repack(shot, ref_image=ref)[0] + self.assertTrue(torch.equal(out["ref"], ref)) + self.assertEqual(out["prompt"], "old") + self.assertIs(out["frames"], shot["frames"]) + out = SL.BFSShotRepack().repack(shot, guide_frames=torch.ones(30, 16, 16, 3), prompt="new")[0] + self.assertEqual(tuple(out["frames"].shape), (22, 8, 8, 3)) + self.assertEqual(out["prompt"], "new") + self.assertEqual(out["length"], 20) + + if __name__ == "__main__": unittest.main() diff --git a/web/docs/BFSShotPlanner.md b/web/docs/BFSShotPlanner.md index 8cd00c5..46ed7a0 100644 --- a/web/docs/BFSShotPlanner.md +++ b/web/docs/BFSShotPlanner.md @@ -18,6 +18,7 @@ every shot inside that range. |---|---| | **BFS Shot Planner** | Pick a video, split it on a timeline, set a reference and prompt per shot. Outputs the shots as a ComfyUI **list**. | | **BFS Shot Unpack** | Opens one shot into plain values: guide frames, reference, second reference, prompt, length, first frame, audio, size, index. Use it with any model. | +| **BFS Shot Repack** | Puts edited pieces back into a shot (Unpack -> e.g. remove the reference background -> Repack). Unconnected inputs keep their values; timing, cuts and audio are unchanged. | | **BFS Shot H3 Conditioning** | Ready-made MiniMax H3 conditioning for one shot, built with the native nodes (Reference to Video + Add Guide). | | **BFS Shot Join** | Concatenates the decoded shots in order, trims each to its true length, cross-fades soft joins and returns the soundtrack. |