From 6bcd869d4eba7bb8a0e618e172ed44f0b5af3026 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:09:36 +0300 Subject: [PATCH] fix(sampler): carry_i2v_guides and mid_scene_guide had no H3 path Both did LTX latent surgery unguarded: prepending frames off H3's 5k+2 grid and attaching conditioning it never reads. Carry now pins the original reference at frame 0; a mid-clip guide declines by the same rule as every other H3 pin. Co-Authored-By: Claude Opus 5 --- samplers.py | 47 ++++++++++++++++++++++++++---- tests/test_minimax_h3_sampler.py | 49 ++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/samplers.py b/samplers.py index 5d4c218e..29e763a1 100644 --- a/samplers.py +++ b/samplers.py @@ -3284,7 +3284,33 @@ class FunPackLTXAVSceneChainSampler: chunk["noise_mask"] = out_masks[0] return chunk, count + def _h3_carry_i2v_guide(self, chunk, template, positive, negative): + """carry_i2v_guides on H3: hold the ORIGINAL reference, as a frame-0 pin. + + The LTX path below prepends the template's protected frames to the chunk. On H3 that + adds video latent frames the packed layout never accounted for — off its 5k+2 grid — + and attaches LTX conditioning this model does not read, so it damaged the latent + instead of carrying anything. The same intent (every scene keeps the opening + reference) is a keyframe pin, and nothing is appended, so no tail is cropped later. + + It claims frame 0 ahead of the continuation pin: continuing from the previous shot is + the default, holding the original reference is what the user asked for by name. + """ + tensors = self._latent_tensors(template) + masks = self._latent_masks(template, len(tensors)) + if not tensors or getattr(tensors[0], "ndim", 0) < 5: + return chunk, positive, negative, 0 + protected = self._protected_prefix_frames(masks[0], self._tensor_frames(tensors[0])) + if protected <= 0: + return chunk, positive, negative, 0 + frame = self._time_slice(tensors[0], 0, 1)[:1].clone() + positive = self._h3_add_keyframes( + positive, [{"resolved_frame_index": 0, "latent": frame}], self._h3_frame_count) + return chunk, positive, negative, 0 + def _append_i2v_guides(self, chunk, template, positive, negative): + if self._is_h3: + return self._h3_carry_i2v_guide(chunk, template, positive, negative) chunk_tensors = self._latent_tensors(chunk) template_tensors = self._latent_tensors(template) template_masks = self._latent_masks(template, len(template_tensors)) @@ -5489,11 +5515,6 @@ class FunPackLTXAVSceneChainSampler: using LTX's guide attention mechanism (keyframe_idxs + guide_attention_entries). Audio-safe: appends only to the video tensor, guide tokens influence denoising through attention weights rather than overwriting hidden states.""" - try: - from comfy_extras.nodes_lt import LTXVAddGuide, _append_guide_attention_entry - except ImportError: - return chunk, positive, negative, 0 - prev_tensors = self._latent_tensors(previous_output) chunk_tensors = self._latent_tensors(chunk) if not prev_tensors or not chunk_tensors: @@ -5504,6 +5525,22 @@ class FunPackLTXAVSceneChainSampler: guide_frame = self._time_slice(prev_tensors[0], F_prev // 2, F_prev // 2 + 1) guide_frame = guide_frame.to(device=chunk_tensors[0].device, dtype=chunk_tensors[0].dtype) + if self._is_h3: + # A mid-clip pin is not something H3's packed layout can place, and the LTX path + # below would append a latent frame off its grid rather than refuse. Routed + # through the keyframe path so it declines by the same rule, and says so. + positive, negative, tail = self._append_h3_keyframe( + guide_frame, max(1, int(self._h3_frame_count)) // 2, strength, + positive, negative) + return chunk, positive, negative, tail + + # LTX from here down. Imported after the H3 branch so the H3 path does not depend on + # an LTX-only module importing. + try: + from comfy_extras.nodes_lt import LTXVAddGuide, _append_guide_attention_entry + except ImportError: + return chunk, positive, negative, 0 + # Target temporal position: middle of current chunk in pixel space F_chunk = self._tensor_frames(chunk_tensors[0]) mid_chunk = F_chunk // 2 diff --git a/tests/test_minimax_h3_sampler.py b/tests/test_minimax_h3_sampler.py index eda70cae..586aaef7 100644 --- a/tests/test_minimax_h3_sampler.py +++ b/tests/test_minimax_h3_sampler.py @@ -605,3 +605,52 @@ def test_an_unreadable_carry_source_skips_rather_than_failing_the_render(): positive = [[torch.zeros(1, 8, 16), {}]] out, applied = node._h3_continuation_pin(positive, {"samples": torch.zeros(1, 24, 4)}) assert applied is False and _pins(out) == [] + + +# ── the two carry toggles that had no H3 path at all ───────────────────────── + +def _template(video_t=37, protected=1): + latent = av_latent(video_t=video_t) + video = latent["samples"].unbind()[0] + mask = torch.ones_like(video) + mask[:, :, :protected] = 0.0 # protected = pinned prefix + latent["noise_mask"] = NT([mask, torch.ones_like(latent["samples"].unbind()[1])]) + video[:, :, 0] = 5.0 # the reference frame + return latent + + +def test_carry_i2v_guides_pins_the_reference_instead_of_growing_the_latent(): + """The LTX path prepends the template's protected frames to the chunk. On H3 that adds + video frames off its 5k+2 grid and attaches conditioning the model does not read.""" + node = h3_node() + chunk = av_latent(video_t=37) + before = chunk["samples"].unbind()[0].shape + out_chunk, positive, _neg, carried = node._append_i2v_guides( + chunk, _template(), [[torch.zeros(1, 8, 16), {}]], None) + assert out_chunk["samples"].unbind()[0].shape == before # latent untouched + assert carried == 0 # nothing to crop later + pins = _pins(positive) + assert [p["resolved_frame_index"] for p in pins] == [0] + assert float(pins[0]["latent"].max()) == 5.0 # the ORIGINAL reference + + +def test_holding_the_reference_outranks_continuing_from_the_last_shot(): + """Continuing is the default; carry_i2v_guides is the user asking for the opposite.""" + node = h3_node() + _c, positive, _n, _t = node._append_i2v_guides( + av_latent(), _template(), [[torch.zeros(1, 8, 16), {}]], None) + out, applied = node._h3_continuation_pin(positive, av_latent()) + assert applied is False + assert float(_pins(out)[0]["latent"].max()) == 5.0 + + +def test_mid_scene_guide_declines_on_h3_instead_of_damaging_the_latent(capsys): + """H3's packed layout places a pin at the first or last frame, nowhere between.""" + node = h3_node(frame_count=124) + chunk = av_latent(video_t=37) + before = chunk["samples"].unbind()[0].shape + out_chunk, positive, _neg, tail = node._append_mid_scene_guide( + chunk, av_latent(video_t=37), [[torch.zeros(1, 8, 16), {}]], None, LTXVAE(), 1.0) + assert out_chunk["samples"].unbind()[0].shape == before + assert tail == 0 and _pins(positive) == [] + assert "pins only the first" in capsys.readouterr().out