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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3948f64033
commit
e01af72445
+10
-1
@@ -169,7 +169,16 @@ def load_latent_upsampler(model_name):
|
||||
raise ValueError(
|
||||
f"{model_name} is not an LTX latent upsampler (expected post_upsample_res_blocks keys).")
|
||||
config = json.loads(metadata["config"])
|
||||
model = LatentUpsampler.from_config(config).to(
|
||||
# ComfyUI v0.29.0 (upstream f8a3fd9d) moved the upsampler onto DynamicVram and made
|
||||
# `operations` a required argument of from_config; older cores build nn.Modules
|
||||
# directly and reject the kwarg. Rentals run whatever they pulled, so accept both.
|
||||
try:
|
||||
import comfy.ops
|
||||
|
||||
model = LatentUpsampler.from_config(config, operations=comfy.ops.disable_weight_init)
|
||||
except TypeError:
|
||||
model = LatentUpsampler.from_config(config)
|
||||
model = model.to(
|
||||
dtype=comfy.model_management.vae_dtype(allowed_dtypes=[torch.bfloat16, torch.float32]))
|
||||
model.load_state_dict(sd)
|
||||
model.eval()
|
||||
|
||||
@@ -226,6 +226,20 @@
|
||||
{ name: "detail_denoise", label: "Detail re-noise strength", kind: "float", default: 0.85, min: 0.3, max: 0.99, step: 0.05,
|
||||
deps: [{ name: "segmented_detailing" }, { name: "detail_mode", value: "repair" }],
|
||||
hint: "How much noise the crop gets re-noised to before its 3-step refine (0.85 is the official LTX 2.3 recipe's own value). Higher = more freedom to genuinely reconstruct the region (fix bad anatomy), risking drift from the surrounding frame; lower = closer to a plain upscale — looks 'detailed' as interpolation but doesn't actually repair it. If the result looks upscaled but not corrected, raise this. Only used in 'repair' mode." },
|
||||
{ name: "context_windows", label: "Context windows (long scenes)", kind: "bool", default: false,
|
||||
hint: "EXPERIMENTAL: denoise a scene that's LONGER than the model's comfortable window as overlapping windows instead of one giant pass — ComfyUI core's own mechanism, audio-aware on LTX (it maps each video window to its audio window and re-slices anchors, guides and JoyAI memory per window). Engages only on scenes longer than the window length below; shorter scenes are untouched and pay nothing. Cost isn't a flat multiple — the total work is about the same, plus the overlap being computed twice (~28% at the defaults). Needs ComfyUI v0.29.0 or newer; on older builds it's skipped with a note in the scene report. UNVALIDATED LIVE." },
|
||||
{ name: "context_window_length", label: "Window length (frames)", kind: "int", default: 145, min: 9, max: 2049, step: 8, dependsOn: "context_windows",
|
||||
hint: "Window size in real frames. A scene at or below this length skips windowing entirely, so this doubles as the engage threshold. Keep it at or under the length the model already generates well in one pass — the point is to stay inside that range while the scene as a whole goes past it." },
|
||||
{ name: "context_window_overlap", label: "Window overlap (frames)", kind: "int", default: 40, min: 0, max: 512, step: 8, dependsOn: "context_windows",
|
||||
hint: "How many frames consecutive windows share. This is the only thing carrying motion and appearance across a window boundary, and also the only extra compute this costs. Too low shows as a seam or a motion hitch at the boundary; too high pays for frames you already have." },
|
||||
{ name: "context_window_schedule", label: "Window schedule", kind: "combo", choices: ["uniform_standard", "static_standard", "uniform_looped", "batched"], default: "uniform_standard", dependsOn: "context_windows",
|
||||
hint: "How windows are laid out each step. 'uniform_standard' (default) shifts the grid between steps so boundaries never bake in — safest. 'static_standard' keeps fixed cut points (cheapest, but a bad boundary stays bad). 'uniform_looped' wraps the end into the start for looping content. 'batched' uses disjoint chunks with no overlap logic (fastest, weakest continuity)." },
|
||||
{ name: "context_window_fuse", label: "Window blend", kind: "combo", choices: ["pyramid", "relative", "flat", "overlap-linear"], default: "pyramid", dependsOn: "context_windows",
|
||||
hint: "How overlapping windows are weighted when merged. 'pyramid' (default) fades each window toward its edges so seams go soft. 'flat' averages equally (can smear). Change this if boundaries look ghosted rather than merely misaligned." },
|
||||
{ name: "context_window_freenoise", label: "FreeNoise blending", kind: "bool", default: true, dependsOn: "context_windows",
|
||||
hint: "Shuffle rather than redraw the starting noise between windows, so overlapping regions begin from correlated noise. Free (a one-time permutation) and core's own default because it measurably improves how windows blend. Turn off only to A/B whether it's helping." },
|
||||
{ name: "context_window_retain_first", label: "Pin anchor in every window", kind: "bool", default: false, dependsOn: "context_windows",
|
||||
hint: "Keep latent frame 0 (the i2v anchor) inside every window instead of just the first. Turn on if later windows drift away from the reference image. Off by default because on a continuation scene frame 0 is the carried tail of the previous scene, not the anchor — pinning it everywhere can make the scene read as static. Turn it off again if motion stalls." },
|
||||
];
|
||||
const SAMPLER_KNOB_MAP = Object.fromEntries(SAMPLER_KNOBS.map((k) => [k.name, k]));
|
||||
|
||||
@@ -351,7 +365,7 @@
|
||||
chain_timing: ["frame_overlap", "transition_duration", "use_same_seed"],
|
||||
chain_guidance: ["cfg", "embed_guidance", "embed_guidance_source", "embed_guidance_strength", "score_slider", "score_slider_strength", "taste_nearest_prompt", "output_guidance", "output_guidance_strength", "dynashift", "dynashift_strength", "dynashift_threshold"],
|
||||
chain_decode: ["decode_noise_scale", "decode_timestep", "decode_tile_size"],
|
||||
chain_experimental: ["plateau_cache", "plateau_cache_threshold", "segmented_detailing", "detail_targets", "detail_strength", "detail_threshold", "detail_max_area", "detail_mode", "detail_denoise", "mid_scene_guide", "mid_scene_guide_strength", "joyai_memory", "joyai_memory_size", "joyai_fix_frames", "joyai_frame_select", "joyai_memory_strength", "joyai_audio_memory", "v2a_grad_scale", "alg_blur_guides", "alg_guide_blur_strength", "alg_guide_blur_sigma_threshold", "bounded_attention_enabled", "identity_transfer_enabled", "source_id", "phase_scale", "id_strength", "arcface_mode", "debug_log"],
|
||||
chain_experimental: ["context_windows", "context_window_length", "context_window_overlap", "context_window_schedule", "context_window_fuse", "context_window_freenoise", "context_window_retain_first", "plateau_cache", "plateau_cache_threshold", "segmented_detailing", "detail_targets", "detail_strength", "detail_threshold", "detail_max_area", "detail_mode", "detail_denoise", "mid_scene_guide", "mid_scene_guide_strength", "joyai_memory", "joyai_memory_size", "joyai_fix_frames", "joyai_frame_select", "joyai_memory_strength", "joyai_audio_memory", "v2a_grad_scale", "alg_blur_guides", "alg_guide_blur_strength", "alg_guide_blur_sigma_threshold", "bounded_attention_enabled", "identity_transfer_enabled", "source_id", "phase_scale", "id_strength", "arcface_mode", "debug_log"],
|
||||
};
|
||||
|
||||
function countChainView(p, id) {
|
||||
|
||||
+168
-1
@@ -2482,6 +2482,34 @@ class FunPackLTXAVSceneChainSampler:
|
||||
"default": "repair",
|
||||
"tooltip": "'repair' (default): upsample the crop, then re-denoise it through the video model for 3 extra steps — can genuinely fix wrong structure (bad anatomy) but costs real compute (~4x region area x 3 steps). 'sharpen': stop after the upsampler's own forward pass — no video-model calls at all, close to free — good for a region that's blurry/under-resolved but already correctly shaped; it CANNOT fix wrong structure (an extra finger stays an extra finger, just sharper), since a super-resolution net only adds detail consistent with what's already there.",
|
||||
}),
|
||||
"context_windows": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "EXPERIMENTAL: denoise a scene LONGER than the model's native window as overlapping context windows instead of one giant pass (ComfyUI core's own comfy.context_windows, LTX2 branch — nothing ported). Each step splits the scene into windows of context_window_length frames, denoises each, and fuses the overlaps back together. Core's LTXAV path is genuinely audio-aware: 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 i2v anchors, mid-scene guides and JoyAI memory keep working inside a window. Cost is NOT a flat multiple: total tokens stay roughly the same and windowed attention is cheaper than full-length attention, but the overlap is recomputed (overlap/length extra work, ~28% at the defaults). Engages ONLY when the scene is longer than context_window_length — shorter scenes are untouched and pay nothing. Off by default. UNVALIDATED LIVE.",
|
||||
}),
|
||||
"context_window_length": ("INT", {
|
||||
"default": 145, "min": 9, "max": 2049, "step": 8,
|
||||
"tooltip": "Window size in REAL frames (must be 8n+1; core rounds down to latent frames). A scene at or below this length skips windowing entirely, so this doubles as the engage threshold. Keep it at or under the length the model actually generates well in one pass — the whole point is to stay inside that range while the scene as a whole goes past it.",
|
||||
}),
|
||||
"context_window_overlap": ("INT", {
|
||||
"default": 40, "min": 0, "max": 512, "step": 8,
|
||||
"tooltip": "How many real frames consecutive windows share. This is the ONLY thing carrying motion/appearance continuity across a window boundary, and it is also the only extra compute this feature costs (overlap/length = the redundant fraction). Too low and boundaries show as a seam or a motion hitch; too high and you pay for frames you already have.",
|
||||
}),
|
||||
"context_window_schedule": (["uniform_standard", "static_standard", "uniform_looped", "batched"], {
|
||||
"default": "uniform_standard",
|
||||
"tooltip": "How the windows are laid out across the scene, per step. 'uniform_standard' (default, core's own LTXV default) shifts the window grid between steps so boundaries land in different places each step and never bake in — the safest general choice. 'static_standard' keeps the same fixed cut points every step (cheapest, but a bad boundary stays bad). 'uniform_looped' wraps the last window into the first, for seamless looping content. 'batched' denoises disjoint chunks with no overlap logic (fastest, weakest continuity).",
|
||||
}),
|
||||
"context_window_fuse": (["pyramid", "relative", "flat", "overlap-linear"], {
|
||||
"default": "pyramid",
|
||||
"tooltip": "Weighting used to blend overlapping windows back together. 'pyramid' (default) fades each window toward its edges, so the middle of a window dominates and seams get soft. 'flat' averages equally (can smear). 'relative' and 'overlap-linear' weight by position within the overlap. Change this if boundaries look soft/ghosted rather than merely misaligned.",
|
||||
}),
|
||||
"context_window_freenoise": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "Shuffle (rather than redraw) the initial noise between windows so overlapping regions start from correlated noise. Costs nothing — it is a one-time permutation of the starting noise — and is core's default for LTXV because it measurably improves how well windows blend. Turn it off only to A/B whether it is helping.",
|
||||
}),
|
||||
"context_window_retain_first": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Pin latent frame 0 (the i2v anchor) into EVERY window, in both the conditioning and the noise latent, instead of only the first window. Helps when later windows drift away from the reference image. Off by default because on a CONTINUATION scene frame 0 is the carried tail of the previous scene, not the anchor — pinning it there re-shows the same content in every window and can read as the scene going static. Turn it on if later windows lose the reference; turn it off if the scene stops moving.",
|
||||
}),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID",
|
||||
@@ -2537,6 +2565,18 @@ class FunPackLTXAVSceneChainSampler:
|
||||
raise ValueError("Scene chain latents must have a time dimension at index 2.")
|
||||
return int(tensor.shape[2])
|
||||
|
||||
def _context_scene_latent_frames(self, chunk):
|
||||
"""Video latent frames in this scene's chunk, or None if it can't be read.
|
||||
|
||||
Reported only — core decides for itself whether a scene is long enough to window.
|
||||
Read off the VIDEO tensor (index 0 of the nested AV latent), while the chunk is
|
||||
still unpacked 5D; after comfy packs it the time axis is gone.
|
||||
"""
|
||||
try:
|
||||
return self._tensor_frames(self._latent_tensors(chunk)[0])
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _latent_tensors(self, latent):
|
||||
samples = latent.get("samples")
|
||||
if self._is_nested(samples):
|
||||
@@ -4285,6 +4325,79 @@ class FunPackLTXAVSceneChainSampler:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Wrapper keys used by context windows; core registers them under these names.
|
||||
_CTX_WRAPPER_KEYS = ("ContextWindows_prepare_sampling", "ContextWindows_sampler_sample")
|
||||
|
||||
def _install_context_windows(self, model, length, overlap, schedule, fuse,
|
||||
freenoise, retain_first):
|
||||
"""Hand this scene's denoise to ComfyUI core's context-window sampler.
|
||||
|
||||
Nothing is ported: core owns the whole mechanism (comfy.context_windows), we only
|
||||
build its handler and hang it on this scene's model_options, the same way the stock
|
||||
LTXVContextWindows node does. Core picks it up inside comfy.samplers.calc_cond_batch,
|
||||
which every FunPack sampler already goes through (they all call model(x, sigma, ...)
|
||||
on the CFGGuider rather than apply_model directly), so no sampler change is needed.
|
||||
|
||||
Returns (remove_fn, latent_len) or None when the running ComfyUI core predates LTXAV
|
||||
window support. That capability gate is the important part: the handler class itself
|
||||
has existed for a long time for unpacked video, but the packed AV latent needs
|
||||
BaseModel.map_context_window_to_modalities (+ resize_cond_for_context_window) to
|
||||
unpack the AV stream, map each video window onto its audio window and re-slice
|
||||
keyframe_idxs / guide_attention_entries. Without those, enabling this on LTXAV would
|
||||
window the packed tensor blindly and quietly wreck audio and guides — the same class
|
||||
of packed-vs-unpacked mistake that killed DynamicConditioning. Refuse instead.
|
||||
"""
|
||||
try:
|
||||
import comfy.context_windows as _cw
|
||||
import comfy.patcher_extension as _pe
|
||||
except ImportError:
|
||||
return None
|
||||
base = getattr(model, "model", None)
|
||||
if not hasattr(base, "map_context_window_to_modalities"):
|
||||
return None
|
||||
# Real frames -> latent frames, exactly as core's LTXVContextWindows node does it.
|
||||
latent_len = max(((int(length) - 1) // 8) + 1, 1)
|
||||
latent_overlap = max(int(overlap) // 8, 0)
|
||||
retain = "0" if retain_first else ""
|
||||
try:
|
||||
handler = _cw.IndexListContextHandler(
|
||||
context_schedule=_cw.get_matching_context_schedule(schedule),
|
||||
fuse_method=_cw.get_matching_fuse_method(fuse),
|
||||
context_length=latent_len,
|
||||
context_overlap=latent_overlap,
|
||||
context_stride=1,
|
||||
closed_loop=False,
|
||||
dim=2,
|
||||
freenoise=bool(freenoise),
|
||||
cond_retain_index_list=retain,
|
||||
latent_retain_index_list=retain,
|
||||
split_conds_to_windows=False,
|
||||
)
|
||||
except TypeError:
|
||||
return None # signature drifted from the one this was written against
|
||||
prev = model.model_options.get("context_handler")
|
||||
had_prev = "context_handler" in model.model_options
|
||||
model.model_options["context_handler"] = handler
|
||||
_cw.create_prepare_sampling_wrapper(model)
|
||||
if freenoise:
|
||||
_cw.create_sampler_sample_wrapper(model)
|
||||
|
||||
def _remove():
|
||||
if had_prev:
|
||||
model.model_options["context_handler"] = prev
|
||||
else:
|
||||
model.model_options.pop("context_handler", None)
|
||||
for wrapper_type, key in (
|
||||
(_pe.WrappersMP.PREPARE_SAMPLING, self._CTX_WRAPPER_KEYS[0]),
|
||||
(_pe.WrappersMP.SAMPLER_SAMPLE, self._CTX_WRAPPER_KEYS[1]),
|
||||
):
|
||||
try:
|
||||
model.remove_wrappers_with_key(wrapper_type, key)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return _remove, latent_len
|
||||
|
||||
def _build_plateau_cache_wrapper(self, model, threshold):
|
||||
"""Plateau step-cache (MixCache/Chorus-family, adapted to LTX2.3's distilled schedule).
|
||||
|
||||
@@ -4700,6 +4813,9 @@ class FunPackLTXAVSceneChainSampler:
|
||||
segmented_detailing=False, detail_targets="hands",
|
||||
detail_upsampler="None", detail_strength=1.0, detail_threshold=0.35,
|
||||
detail_max_area=0.35, detail_denoise=0.85, detail_mode="repair",
|
||||
context_windows=False, context_window_length=145, context_window_overlap=40,
|
||||
context_window_schedule="uniform_standard", context_window_fuse="pyramid",
|
||||
context_window_freenoise=True, context_window_retain_first=False,
|
||||
unique_id=None, prompt=None):
|
||||
if not isinstance(positive, list) or not positive:
|
||||
raise ValueError("positive conditioning must contain at least one scene entry.")
|
||||
@@ -4758,6 +4874,12 @@ class FunPackLTXAVSceneChainSampler:
|
||||
dynashift=dynashift, dynashift_strength=dynashift_strength,
|
||||
dynashift_threshold=dynashift_threshold,
|
||||
plateau_cache=plateau_cache, plateau_cache_threshold=plateau_cache_threshold,
|
||||
context_windows=context_windows, context_window_length=context_window_length,
|
||||
context_window_overlap=context_window_overlap,
|
||||
context_window_schedule=context_window_schedule,
|
||||
context_window_fuse=context_window_fuse,
|
||||
context_window_freenoise=context_window_freenoise,
|
||||
context_window_retain_first=context_window_retain_first,
|
||||
)
|
||||
|
||||
max_scene_count = max(1, int(max_scenes))
|
||||
@@ -4890,6 +5012,7 @@ class FunPackLTXAVSceneChainSampler:
|
||||
_identity_overlap_state: dict = {}
|
||||
_detail_upsampler_model = None # lazy: resolved+loaded at the first detailed scene
|
||||
_detail_disabled_reason = None # set on resolve/load failure: don't retry per scene
|
||||
_ctx_unsupported_reported = False # print the "core too old" line once, not per scene
|
||||
|
||||
for scene_index, scene_cond in enumerate(scene_conditionings):
|
||||
scene_positive = [scene_cond]
|
||||
@@ -5009,7 +5132,39 @@ class FunPackLTXAVSceneChainSampler:
|
||||
_v2a_handles = []
|
||||
_identity_overlap_handle = None
|
||||
_plateau_stats = None
|
||||
_ctx_remove = None
|
||||
try:
|
||||
# Context windows: core-owned, installed first so every FunPack wrapper below
|
||||
# sits inside the per-window forward rather than around the whole clip.
|
||||
if context_windows:
|
||||
_ctx = self._install_context_windows(
|
||||
model, context_window_length, context_window_overlap,
|
||||
context_window_schedule, context_window_fuse,
|
||||
context_window_freenoise, context_window_retain_first)
|
||||
if _ctx is None:
|
||||
if not _ctx_unsupported_reported:
|
||||
print("[FunPackSceneChain] context_windows requested but this ComfyUI "
|
||||
"build has no LTXAV context-window support (needs the CORE-3 "
|
||||
"context-windows change, ComfyUI >= v0.29.0) — skipped.")
|
||||
_ctx_unsupported_reported = True
|
||||
run_mechanisms.append("context_windows(SKIPPED: ComfyUI core too old)")
|
||||
else:
|
||||
_ctx_remove, _ctx_latent_len = _ctx
|
||||
# Core engages windowing only when the scene is longer than the window,
|
||||
# and logs that decision itself; say so here too so a scene that silently
|
||||
# sampled whole doesn't read as the feature being broken.
|
||||
_scene_latent_len = self._context_scene_latent_frames(chunk)
|
||||
if _scene_latent_len is not None and _scene_latent_len <= _ctx_latent_len:
|
||||
run_mechanisms.append(
|
||||
f"context_windows(inactive: scene {_scene_latent_len} <= window "
|
||||
f"{_ctx_latent_len} latent frames — raise num_frames_per_scene or "
|
||||
f"lower context_window_length)")
|
||||
else:
|
||||
run_mechanisms.append(
|
||||
f"context_windows({context_window_schedule},{context_window_fuse},"
|
||||
f"len={_ctx_latent_len},ovl={max(int(context_window_overlap) // 8, 0)}"
|
||||
f"{',freenoise' if context_window_freenoise else ''}"
|
||||
f"{',retain_first' if context_window_retain_first else ''})")
|
||||
# Innermost wrapper (installed first): caches the raw base-model forward on the
|
||||
# near-noise plateau so later plateau steps reuse it. All guidance wrappers below
|
||||
# layer around it and still post-process each step's (cached-or-fresh) prediction.
|
||||
@@ -5135,6 +5290,8 @@ class FunPackLTXAVSceneChainSampler:
|
||||
run_mechanisms.append(
|
||||
f"plateau_cache(skipped {_reused}/{_total} plateau fwd, thr={plateau_cache_threshold})")
|
||||
finally:
|
||||
if _ctx_remove is not None:
|
||||
_ctx_remove()
|
||||
self._remove_v2a_scale(_v2a_handles)
|
||||
self._strip_identity_overlap(_identity_overlap_handle)
|
||||
if model.model_options.get("model_function_wrapper") is not _scene_base_wrapper:
|
||||
@@ -5456,7 +5613,11 @@ class FunPackLTXAVSceneChainSampler:
|
||||
output_guidance=False, output_guidance_strength=0.02,
|
||||
dynashift=False, dynashift_strength=0.3, dynashift_threshold=0.6,
|
||||
alg_guide_blur_strength=2.0, alg_guide_blur_sigma_threshold=0.975,
|
||||
plateau_cache=False, plateau_cache_threshold=0.975):
|
||||
plateau_cache=False, plateau_cache_threshold=0.975,
|
||||
context_windows=False, context_window_length=145,
|
||||
context_window_overlap=40, context_window_schedule="uniform_standard",
|
||||
context_window_fuse="pyramid", context_window_freenoise=True,
|
||||
context_window_retain_first=False):
|
||||
"""Sample one chain per Studio-packed variant entry (seed + index), persisting each result
|
||||
(latent + preview + per-entry cond + manifest) under ComfyUI temp for rating in Studio.
|
||||
Reuses sample() per entry with only the seed changed, so each entry is a clean generation."""
|
||||
@@ -5510,6 +5671,12 @@ class FunPackLTXAVSceneChainSampler:
|
||||
dynashift=dynashift, dynashift_strength=dynashift_strength,
|
||||
dynashift_threshold=dynashift_threshold,
|
||||
plateau_cache=plateau_cache, plateau_cache_threshold=plateau_cache_threshold,
|
||||
context_windows=context_windows, context_window_length=context_window_length,
|
||||
context_window_overlap=context_window_overlap,
|
||||
context_window_schedule=context_window_schedule,
|
||||
context_window_fuse=context_window_fuse,
|
||||
context_window_freenoise=context_window_freenoise,
|
||||
context_window_retain_first=context_window_retain_first,
|
||||
unique_id=None, prompt=None,
|
||||
)
|
||||
last = out
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user