Files
olivv-cs-ComfyUI-FunPack/tests/test_second_pass_layout_conds.py
digital-garbageandClaude Opus 5 e7b624d684 fix: drop guide keyframes on a resized second pass; unify the two ALG switches
Two unrelated fixes.

**Guide keyframes vs a resolution-changing second_pass_op.** Guide keyframes are
recorded as TOKEN indices into pass 1's latent grid, so upscale_2x — which
doubles each spatial dim and thus quadruples the tokens per latent frame —
leaves them addressing the wrong tokens. Since the LTX-2.5 commit core rejects
that outright ("keyframe_idxs holds N tokens, which is not a whole number of
M-token latent frames"), which is how this surfaced; older cores silently
mis-placed the guides instead, which is worse. Pass 2 now drops them and says
so in the scene report, exactly as segmented detailing already does for its
crop — so detailing's stripper becomes public rather than being duplicated. Only
pass 2 is affected; pass 1 ran at the grid the guides were recorded against and
keeps every one. The pin loss and the guide loss now share one predicate
(_latent_spatial_changed) so they cannot drift apart, and an unreadable shape
answers "no change" so a probe failure never costs a guide.

**One ALG control instead of two.** The Distilled Flow panel had its own
alg_enabled while the chain sampler's alg_anchor did the same guidance on any
sampler and already drove the Distilled Flow one when set — two switches for one
behaviour, with precedence depending on which you had touched. The Editor now
shows only alg_anchor. The node input stays for hand-built graphs; what moved is
the Editor's control. A project still carrying the old switch is migrated once,
strength and threshold included, and told so, rather than left running ALG with
nothing on screen to turn it off.

The migration is deferred out of the render pass and guarded by a module-level
set of project ids: every store write re-renders the pane synchronously, so
migrating inline recursed until the stack blew (caught in the browser, not by
the suite).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:39:47 +03:00

96 lines
3.5 KiB
Python

"""Guide keyframes must not survive a resolution-changing second_pass_op.
Guide keyframes are recorded as TOKEN indices into pass 1's latent grid. `upscale_2x`
doubles each spatial dim, so a latent frame is suddenly 4x as many tokens and those indices
address the wrong ones. Since the LTX-2.5 commit core raises on it outright ("keyframe_idxs
holds N tokens, which is not a whole number of M-token latent frames"); before that it
silently mis-placed the guides, which is worse. Pass 2 therefore drops them — and only
pass 2, because pass 1 ran at the grid they were recorded against.
"""
import sys
import types
from pathlib import Path
import torch
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import detailing # noqa: E402
import samplers # noqa: E402
def _cond(**meta):
return [[torch.zeros(1, 4, 128), dict(meta)]]
def _latent(h, w, frames=3):
return {"samples": torch.zeros(1, 128, frames, h, w)}
def _chain():
return samplers.FunPackLTXAVSceneChainSampler()
# --- the shared predicate ------------------------------------------------------------------
def test_spatial_change_is_detected():
chain = _chain()
assert chain._latent_spatial_changed(_latent(32, 48), _latent(16, 24)) is True
def test_same_size_is_not_a_change():
chain = _chain()
assert chain._latent_spatial_changed(_latent(16, 24), _latent(16, 24)) is False
def test_more_frames_alone_is_not_a_spatial_change():
"""Only H/W matter: token-per-frame count is spatial. A differing frame count must not
be read as a resize, or 'sharpen' runs would drop guides for no reason."""
chain = _chain()
assert chain._latent_spatial_changed(_latent(16, 24, frames=5), _latent(16, 24, frames=3)) is False
def test_unreadable_shapes_report_no_change():
"""A probe failure must not cost the user their guides."""
chain = _chain()
assert chain._latent_spatial_changed({"samples": "not a tensor"}, _latent(16, 24)) is False
# --- the stripper -------------------------------------------------------------------------
def test_layout_keys_are_detected_and_stripped():
conds = _cond(keyframe_idxs=torch.zeros(1, 3, 512), pooled_output=None)
assert detailing.has_layout_conds(conds) is True
stripped = detailing.strip_layout_conds(conds)
assert "keyframe_idxs" not in stripped[0][1]
assert "pooled_output" in stripped[0][1], "only layout keys go; text conditioning stays"
def test_every_layout_key_is_covered():
conds = _cond(**{k: 1 for k in detailing._LAYOUT_COND_KEYS})
assert detailing.strip_layout_conds(conds)[0][1] == {}
def test_plain_text_conditioning_has_no_layout_keys():
"""The common case: no guides, so pass 2 must be told nothing was dropped."""
assert detailing.has_layout_conds(_cond(pooled_output=None)) is False
def test_stripping_does_not_mutate_the_input():
"""Pass 1's conditioning is the same object; mutating it would retroactively strip the
pass that legitimately used the guides."""
conds = _cond(keyframe_idxs=torch.zeros(1, 3, 512))
detailing.strip_layout_conds(conds)
assert "keyframe_idxs" in conds[0][1]
def test_tensor_identity_is_preserved():
"""Only the meta dict is rebuilt — the conditioning tensor must not be copied."""
conds = _cond(keyframe_idxs=1)
assert detailing.strip_layout_conds(conds)[0][0] is conds[0][0]
def test_non_entry_items_pass_through_untouched():
assert detailing.strip_layout_conds([None, "junk"]) == [None, "junk"]
assert detailing.has_layout_conds([None, "junk"]) is False