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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a9d2c0d9f0
commit
6bcd869d4e
+42
-5
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user