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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ea129e03a1
commit
e67b22a0ea
@@ -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",
|
||||
}
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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. |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user