From 0fd477759dedd458024cf189410e006447d885d5 Mon Sep 17 00:00:00 2001 From: malkuthro Date: Sun, 19 Jul 2026 21:37:23 +0200 Subject: [PATCH] =?UTF-8?q?fix(ltx):=20Soften=20Schedule=20=E2=80=94=20cov?= =?UTF-8?q?erage=20log=20follows=20the=20anchor's=20schedule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review P3: when runtime sigmas were present but malformed/empty while a valid SIGMAS input was wired, behavior fell back to the wired anchor but the coverage log still read the malformed runtime object and could report "first 0 of 0 steps". Bind cov to whichever schedule the anchor actually came from, and extend the fallback to a zero-start runtime schedule (unusable) instead of going inert while a wired schedule is available. Tooltip now states the wired input's runtime-fallback role. Adds wiring tests for the denoise-mask hook: runtime-anchor coverage, malformed/zero-start runtime fallback to wired, and warn-once inertness. Co-Authored-By: Claude Fable 5 --- k_ltx_keyframe_soften_schedule.py | 22 ++++--- .../test_ltx_keyframe_soften_schedule.py | 65 +++++++++++++++++++ 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/k_ltx_keyframe_soften_schedule.py b/k_ltx_keyframe_soften_schedule.py index b1d27a6..22e036c 100644 --- a/k_ltx_keyframe_soften_schedule.py +++ b/k_ltx_keyframe_soften_schedule.py @@ -136,7 +136,8 @@ class LTXKeyframeSoftenSchedule: "optional": { "sigmas": ("SIGMAS", { "tooltip": "Optional — connect your sampler's SIGMAS to show the " - "exact steps on the 'info' output.", + "exact steps on the 'info' output; also used as the " + "fallback schedule when the run doesn't supply sigmas.", }), }, } @@ -171,7 +172,8 @@ class LTXKeyframeSoftenSchedule: prev_fn = m.model_options.get("denoise_mask_function") # Fallback from the connected SIGMAS (if wired). Used when a run omits - # extra_options["sigmas"] — gives the wired input a real runtime role, + # extra_options["sigmas"] or supplies an unusable one — gives the wired + # input a real runtime role, # not just the info readout. Keep the WHOLE schedule, not only the anchor: # the coverage log needs the real step count to stay honest (a wired # 16-step schedule must report "of 16 steps", not a synthetic 1). @@ -195,16 +197,20 @@ class LTXKeyframeSoftenSchedule: # different sampler schedule — so no stale cached anchor, and a run # that momentarily lacks runtime sigmas can never latch the node # permanently off (the old cache-0.0 path). Falls back to the wired - # SIGMAS when the runtime doesn't supply them. + # SIGMAS when the runtime doesn't supply usable ones. rt = (extra_options or {}).get("sigmas") ref = None + cov = None # the schedule the anchor came from — coverage log follows it if rt is not None: try: ref = float(rt[0]) + cov = rt except Exception: ref = None - if ref is None: - ref = connected_ref + if ref is None or ref <= 0.0: + # Runtime sigmas absent or unusable (malformed/empty/zero-start): + # anchor AND coverage both come from the wired schedule instead. + ref, cov = connected_ref, connected_sigmas if not ref or ref <= 0.0: # No usable reference sigma from the run and none wired: the node # is patched but inert this run. Warn ONCE so a silent no-op on a @@ -228,9 +234,9 @@ class LTXKeyframeSoftenSchedule: if not logged[0]: logged[0] = True nested = getattr(denoise_mask, "is_nested", False) - # Report coverage against the schedule actually in force: the - # runtime one, else the wired one we anchored to. - cov = rt if rt is not None else connected_sigmas + # cov is the schedule the anchor came from (see above), so a + # malformed runtime object can't be reported while the wired + # schedule is the one actually in force. try: count, n_steps = steps_in_range(cov, soften_range) except Exception: diff --git a/tests/nodes/test_ltx_keyframe_soften_schedule.py b/tests/nodes/test_ltx_keyframe_soften_schedule.py index 0675881..7627796 100644 --- a/tests/nodes/test_ltx_keyframe_soften_schedule.py +++ b/tests/nodes/test_ltx_keyframe_soften_schedule.py @@ -159,3 +159,68 @@ def test_steps_flat_top_curve_spans_many_steps(): def test_steps_short_or_empty(): assert steps([1.0], 0.5) == (0, 0) assert steps([], 0.5) == (0, 0) + + +# --- denoise_mask_function wiring: connected-SIGMAS fallback + honest coverage log + + +class _FakeModelPatcher: + """Stub of the ModelPatcher surface patch() touches.""" + + def __init__(self): + self.model_options = {} + self.mask_fn = None + + def clone(self): + return self + + def set_model_denoise_mask_function(self, fn): + self.mask_fn = fn + + +WIRED = [1.0, 0.9, 0.8, 0.6, 0.3, 0.0] # 5 steps; top 25% covers the first 3 + + +def _patched_fn(sigmas=WIRED): + node = _m.LTXKeyframeSoftenSchedule() + fake = _FakeModelPatcher() + node.patch(fake, max_soften=0.5, soften_range=0.25, sigmas=sigmas) + return fake.mask_fn + + +def test_runtime_sigmas_anchor_and_log_coverage(caplog): + # a valid runtime schedule wins over the wired one, for anchor AND log + fn = _patched_fn() + with caplog.at_level("INFO", logger=_m.log.name): + out = fn(1.0, 0.2, extra_options={"sigmas": [1.0, 0.5, 0.0]}) + assert out == pytest.approx(soften(0.2, 0.5)) # progress 0 -> full gain + assert "first 1 of 2 steps" in caplog.text + + +def test_malformed_runtime_sigmas_fall_back_to_wired_for_anchor_and_log(caplog): + # runtime sigmas present but empty: behavior anchors to the WIRED schedule, + # and the coverage log must report the wired steps too — not "0 of 0". + fn = _patched_fn() + with caplog.at_level("INFO", logger=_m.log.name): + out = fn(1.0, 0.2, extra_options={"sigmas": []}) + assert out == pytest.approx(soften(0.2, 0.5)) + assert "first 3 of 5 steps" in caplog.text + assert "0 of 0" not in caplog.text + + +def test_zero_start_runtime_sigmas_fall_back_to_wired(caplog): + # a degenerate runtime schedule (starts at 0) is unusable -> wired fallback + fn = _patched_fn() + with caplog.at_level("INFO", logger=_m.log.name): + out = fn(1.0, 0.2, extra_options={"sigmas": [0.0, 0.0]}) + assert out == pytest.approx(soften(0.2, 0.5)) + assert "first 3 of 5 steps" in caplog.text + + +def test_no_usable_sigmas_anywhere_warns_once_and_passes_through(caplog): + fn = _patched_fn(sigmas=None) + with caplog.at_level("WARNING", logger=_m.log.name): + out1 = fn(1.0, 0.2, extra_options={}) + out2 = fn(1.0, 0.2, extra_options={}) + assert out1 == 0.2 and out2 == 0.2 # mask untouched + assert caplog.text.count("soften inactive") == 1