Drop the overlap when concatenating chunks (1.39.2)
Chunked sampling built its chunks with an overlap for continuity and then concatenated them whole, so the overlap was emitted twice. 97 frames at tile 72 overlap 16 gave 15 latent frames where the clip has 13: the output ran longer than the input, and from the first seam on, frame i was no longer moment i. Pasting back made it obvious rather than subtle -- crop_bboxes has one box per source frame, so every frame after the seam went into a box belonging to a different moment, and with crop_mode: zoomed into a box of a different size too. That is the "crooked and out of sync" this was reported as. Each chunk after the first now contributes only its tail, the part the previous chunk did not already cover, so the concatenation is exactly the clip's latent length. Single-chunk runs are untouched.
This commit is contained in:
@@ -325,6 +325,22 @@ def _mask_to_latent(masks, vae, latent_t, latent_h, latent_w):
|
||||
# node
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _keep_new_frames(samples, produced, total):
|
||||
"""Drop the head a chunk shares with the one before it.
|
||||
|
||||
Chunks are sampled with an overlap for continuity, so consecutive chunks
|
||||
cover some of the same frames. Concatenating them whole makes the clip
|
||||
longer than it is and every frame after the first seam lands on the wrong
|
||||
moment -- and, with per-frame crop boxes, in the wrong box. Keeping only
|
||||
what each chunk adds makes the concatenation exactly `total` frames.
|
||||
"""
|
||||
have = samples.shape[2]
|
||||
new = max(0, min(have, total - produced))
|
||||
if new < have:
|
||||
samples = samples[:, :, have - new:]
|
||||
return samples, produced + samples.shape[2]
|
||||
|
||||
|
||||
def _inject_transformer_options(guider, model_patcher, debug=False):
|
||||
"""Copy the patched model's transformer_options INTO the guider's own dict.
|
||||
|
||||
@@ -715,6 +731,7 @@ class BFSHeadSwapMaskedSampler:
|
||||
mc = LTXMultipleControls()
|
||||
out_latents = []
|
||||
injections = []
|
||||
produced = 0
|
||||
try:
|
||||
for idx, (a, b) in enumerate(chunks):
|
||||
g = guide[a:b]
|
||||
@@ -792,6 +809,7 @@ class BFSHeadSwapMaskedSampler:
|
||||
chunk = _Loop._sample_chunk(m, noise, sampler, sigmas, gd, latent, seed_offset=idx)
|
||||
got = chunk["samples"]
|
||||
if len(chunks) > 1 and not getattr(got, "is_nested", False):
|
||||
got, produced = _keep_new_frames(got, produced, lat_t_total)
|
||||
# a long clip is many chunks: holding them all on the sampling
|
||||
# device grows VRAM with the clip while the model is still loaded
|
||||
got = got.to(comfy.model_management.intermediate_device())
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "bfsnodes"
|
||||
description = "Custom ComfyUI nodes for reference-guided video: the BFS Sampler (guide-driven IC-LoRAs with auto-configured crop, mask inpainting and temporal chunking in one node), LTX Multiple Controls and Identity Transfer (IPT2V reference + source-phase RoPE), LTXV Edit Anything, Head-Swap/Anime2Real Bernini conditioning, the AMV Guide Builder, and MiniMax-H3 single-frame VAE loaders with tile control."
|
||||
version = "1.39.1"
|
||||
version = "1.39.2"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = [
|
||||
"torch",
|
||||
|
||||
@@ -249,6 +249,42 @@ class PasteBackWithoutCropTest(unittest.TestCase):
|
||||
self.assertEqual(out.shape, self.original.shape)
|
||||
|
||||
|
||||
class ChunkOverlapTest(unittest.TestCase):
|
||||
"""Chunks are sampled with an overlap for continuity. Concatenating them
|
||||
whole made the clip longer than it is, so every frame after the first seam
|
||||
landed on the wrong moment -- and, with per-frame crop boxes, in the wrong
|
||||
box. 97 frames at tile 72 / overlap 16 produced 15 latent frames where the
|
||||
clip has 13."""
|
||||
|
||||
@staticmethod
|
||||
def run_chunks(lat_per_chunk, total):
|
||||
produced, out = 0, []
|
||||
for t in lat_per_chunk:
|
||||
got, produced = NODE._keep_new_frames(torch.zeros(1, 4, t, 2, 2), produced, total)
|
||||
out.append(got)
|
||||
return torch.cat(out, dim=2)
|
||||
|
||||
def test_the_concatenation_is_exactly_the_clip(self):
|
||||
self.assertEqual(self.run_chunks([9, 6], 13).shape[2], 13)
|
||||
|
||||
def test_it_keeps_each_chunk_s_tail_not_its_head(self):
|
||||
a = torch.zeros(1, 4, 9, 2, 2)
|
||||
b = torch.arange(6, dtype=torch.float32).view(1, 1, 6, 1, 1).expand(1, 4, 6, 2, 2).clone()
|
||||
_, produced = NODE._keep_new_frames(a, 0, 13)
|
||||
kept, _ = NODE._keep_new_frames(b, produced, 13)
|
||||
self.assertEqual(kept.shape[2], 4)
|
||||
self.assertEqual(kept[0, 0, 0, 0, 0].item(), 2.0) # frames 2..5, the new tail
|
||||
|
||||
def test_a_single_chunk_is_untouched(self):
|
||||
got, produced = NODE._keep_new_frames(torch.zeros(1, 4, 13, 2, 2), 0, 13)
|
||||
self.assertEqual(got.shape[2], 13)
|
||||
self.assertEqual(produced, 13)
|
||||
|
||||
def test_it_never_returns_more_than_is_left(self):
|
||||
got, _ = NODE._keep_new_frames(torch.zeros(1, 4, 9, 2, 2), 12, 13)
|
||||
self.assertEqual(got.shape[2], 1)
|
||||
|
||||
|
||||
class NoShadowedModulesTest(unittest.TestCase):
|
||||
"""`import comfy.x` inside a function binds `comfy` as a LOCAL for the whole
|
||||
function, so every earlier comfy.* use in it raises UnboundLocalError. It
|
||||
|
||||
Reference in New Issue
Block a user