Files
olivv-cs-ComfyUI-FunPack/tests/test_context_windows.py
T
digital-garbageandClaude Opus 5 e01af72445 Context windows on the Chain Sampler + fix upsampler load on ComfyUI v0.29.0
Two things, both driven by upstream ComfyUI moving 7 weeks ahead of the
local checkout (2cdaaf4a 2026-06-06 -> e651b7be 2026-07-28, 263 commits).

1. Segmented detailing crashed on ComfyUI v0.29.0. Upstream f8a3fd9d moved
   the LTX latent upsampler onto DynamicVram and made `operations` a required
   argument of LatentUpsampler.from_config; detailing.py still called it with
   one arg, so the pending live probe would have died before detecting
   anything. Now passes comfy.ops.disable_weight_init (what core's own loader
   does) with a TypeError fallback, since rentals run whatever they pulled.

2. New Chain Sampler option: context_windows. Upstream cd77c551 (CORE-3)
   added context-window sampling with real LTX2/LTXAV support - it unpacks
   the packed AV latent, maps each video window onto its proportional audio
   window, and re-slices denoise masks, keyframe_idxs and
   guide_attention_entries per window, so anchors, mid-scene guides and JoyAI
   memory survive inside a window. Nothing is ported: we build core's handler
   and hang it on the scene's model_options exactly as the stock
   LTXVContextWindows node does. Core picks it up in
   comfy.samplers.calc_cond_batch, which every FunPack sampler already routes
   through, so no sampler-loop change was needed and no graph change either -
   it rides the sampler_inputs bridge into both Editor and Easy Gen.

   Refuses (loudly, once per run, with a scene-report note) on cores without
   BaseModel.map_context_window_to_modalities: the handler class predates
   LTXAV support and would window the packed tensor blindly, which is the
   same packed-vs-unpacked trap that killed DynamicConditioning. Install and
   removal are per-scene under the existing wrapper-hygiene finally, so an
   interrupt cannot leak the handler or its two patcher wrappers into later
   runs.

   Knobs (all exposed, none hardcoded): length, overlap, schedule, blend,
   FreeNoise, and pin-anchor-in-every-window. Off by default. Engages only
   on scenes longer than the window, and says so in the scene report when it
   does not, so a scene that sampled whole never reads as a broken feature.

Verified: 8 new unit tests for the install/remove/capability-gate contract;
full suite at baseline (22 pre-existing cross-module stub failures, no new).
Editor checked live - knobs render in Engine > Chain Sampler > Experimental,
dependents gate on the toggle, and values persist into sampler_inputs.

UNVALIDATED LIVE on a rental.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:49:31 +03:00

175 lines
6.7 KiB
Python

"""Context-window install/remove contract on the Scene Chain sampler.
The mechanism itself is ComfyUI core's (comfy.context_windows) — nothing here tests
core's windowing math. What IS ours, and what these cover:
1. The capability gate. Core's handler class predates LTXAV support; only builds that
also have BaseModel.map_context_window_to_modalities can unpack the packed AV latent
and re-slice guides/audio per window. On an older core we must REFUSE, not window a
packed tensor blindly (the DynamicConditioning failure mode).
2. Real-frame -> latent-frame conversion matches core's own LTXVContextWindows node.
3. Install/remove leaves the shared model exactly as found, including on an interrupt
mid-scene — the same guarantee _strip_funpack_scene_wrappers gives the function
wrappers, but for patcher wrappers and the context_handler model_option.
"""
import sys
import types
from pathlib import Path
import torch
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
for _name in (
"comfy", "comfy.k_diffusion", "comfy.k_diffusion.sampling",
"comfy.model_sampling", "comfy.nested_tensor", "comfy.sample",
"comfy.samplers", "comfy.utils",
):
sys.modules.setdefault(_name, types.ModuleType(_name))
sys.modules["comfy.nested_tensor"].NestedTensor = object
import samplers # noqa: E402
class _Handler:
def __init__(self, **kwargs):
self.kwargs = kwargs
def _install_fake_core(monkeypatch, ltxav_support=True, accept_latent_retain=True):
"""Stand in for comfy.context_windows + comfy.patcher_extension.
Records what the sampler asks core to do, so the test asserts on the request
rather than on core's behaviour.
"""
calls = {"prepare": 0, "sampler_sample": 0, "removed": []}
cw = types.ModuleType("comfy.context_windows")
def _handler(**kwargs):
if not accept_latent_retain and "latent_retain_index_list" in kwargs:
raise TypeError("unexpected keyword argument 'latent_retain_index_list'")
return _Handler(**kwargs)
cw.IndexListContextHandler = _handler
cw.get_matching_context_schedule = lambda s: f"sched:{s}"
cw.get_matching_fuse_method = lambda f: f"fuse:{f}"
def _prep(model):
calls["prepare"] += 1
def _samp(model):
calls["sampler_sample"] += 1
cw.create_prepare_sampling_wrapper = _prep
cw.create_sampler_sample_wrapper = _samp
pe = types.ModuleType("comfy.patcher_extension")
pe.WrappersMP = types.SimpleNamespace(
PREPARE_SAMPLING="prepare_sampling", SAMPLER_SAMPLE="sampler_sample")
monkeypatch.setitem(sys.modules, "comfy.context_windows", cw)
monkeypatch.setitem(sys.modules, "comfy.patcher_extension", pe)
inner = types.SimpleNamespace()
if ltxav_support:
inner.map_context_window_to_modalities = lambda *a, **k: None
model = types.SimpleNamespace(
model_options={},
model=inner,
remove_wrappers_with_key=lambda t, k: calls["removed"].append((t, k)),
)
return model, calls
def _install(sampler, model, **overrides):
kwargs = dict(length=145, overlap=40, schedule="uniform_standard",
fuse="pyramid", freenoise=True, retain_first=False)
kwargs.update(overrides)
return sampler._install_context_windows(model, **kwargs)
def test_refuses_on_core_without_ltxav_window_support(monkeypatch):
model, calls = _install_fake_core(monkeypatch, ltxav_support=False)
s = samplers.FunPackLTXAVSceneChainSampler()
assert _install(s, model) is None
# Nothing installed: a refusal must not half-configure the shared model.
assert "context_handler" not in model.model_options
assert calls["prepare"] == 0 and calls["sampler_sample"] == 0
def test_refuses_when_handler_signature_drifted(monkeypatch):
model, calls = _install_fake_core(monkeypatch, accept_latent_retain=False)
s = samplers.FunPackLTXAVSceneChainSampler()
assert _install(s, model) is None
assert "context_handler" not in model.model_options
def test_real_frames_convert_to_latent_frames_like_core_node(monkeypatch):
model, _ = _install_fake_core(monkeypatch)
s = samplers.FunPackLTXAVSceneChainSampler()
remove, latent_len = _install(s, model, length=145, overlap=40)
# Core's LTXVContextWindows: ((145 - 1) // 8) + 1 = 19, 40 // 8 = 5.
assert latent_len == 19
handler = model.model_options["context_handler"]
assert handler.kwargs["context_length"] == 19
assert handler.kwargs["context_overlap"] == 5
assert handler.kwargs["dim"] == 2
remove()
def test_retain_first_sets_both_cond_and_latent_retain(monkeypatch):
model, _ = _install_fake_core(monkeypatch)
s = samplers.FunPackLTXAVSceneChainSampler()
remove, _ = _install(s, model, retain_first=True)
handler = model.model_options["context_handler"]
# i2v anchors live in the noise latent, not only in conditioning channels, so both
# lists must be set — cond alone would drop the anchor from later windows.
assert handler.kwargs["cond_retain_index_list"] == "0"
assert handler.kwargs["latent_retain_index_list"] == "0"
remove()
def test_freenoise_off_skips_the_sampler_sample_wrapper(monkeypatch):
model, calls = _install_fake_core(monkeypatch)
s = samplers.FunPackLTXAVSceneChainSampler()
remove, _ = _install(s, model, freenoise=False)
assert calls["prepare"] == 1
assert calls["sampler_sample"] == 0
remove()
def test_remove_restores_model_options_and_drops_both_wrappers(monkeypatch):
model, calls = _install_fake_core(monkeypatch)
s = samplers.FunPackLTXAVSceneChainSampler()
remove, _ = _install(s, model)
assert "context_handler" in model.model_options
remove()
assert "context_handler" not in model.model_options
assert calls["removed"] == [
("prepare_sampling", "ContextWindows_prepare_sampling"),
("sampler_sample", "ContextWindows_sampler_sample"),
]
def test_remove_restores_a_pre_existing_foreign_handler(monkeypatch):
"""A user-placed LTXVContextWindows node upstream must survive our scene."""
model, _ = _install_fake_core(monkeypatch)
foreign = object()
model.model_options["context_handler"] = foreign
s = samplers.FunPackLTXAVSceneChainSampler()
remove, _ = _install(s, model)
assert model.model_options["context_handler"] is not foreign
remove()
assert model.model_options["context_handler"] is foreign
def test_scene_latent_frame_count_reads_the_video_stream():
s = samplers.FunPackLTXAVSceneChainSampler()
chunk = {"samples": torch.zeros(1, 128, 23, 8, 8)}
assert s._context_scene_latent_frames(chunk) == 23
# Unreadable latent reports None rather than raising: this value is only ever used
# for the scene report, never for control flow.
assert s._context_scene_latent_frames({"samples": None}) is None