Files
digital-garbageandClaude Opus 5 fc977f4068 sampling: check the values where they are used, not only where they are made
The NaN guard was on one of two doors. A FUNPACK_SETTINGS socket is typed by
a string tag, so a dict reaches the load node without necessarily having
passed through the settings node -- a raw API prompt does exactly that.

Through that door, until_sigma = NaN made ALG a permanent no-op: sigma > nan
is False at every step, so the anchor was never loosened, and the node
reported "1 modifier applied: alg". Nothing about the run looked wrong,
which is the only reason this is worth a commit of its own.

Values are now checked against the declaration at the point they are
consumed. The producer still checks too; that is not redundant, it is where
a person editing JSON gets told immediately.

Also recorded ALG's standing assumption rather than leaving it implicit: the
anchor is latent frame 0. True of everything it can currently be wired into,
untrue in general, and the denoise_mask already carries what would be needed
to do it properly. Left undone because nothing yet pins elsewhere, and a
derivation with no consumer cannot be checked against reality.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 13:05:28 +03:00

171 lines
5.8 KiB
Python

"""ALG, as far as a machine with no GPU can take it.
What IS checked here: the blur does what a low-pass does, the per-step choice
follows sigma, the inert cases are inert, and the whole thing runs through
ComfyUI's real KSAMPLER with a stand-in sampler function -- so the anchor a
denoiser sees really does change between steps.
What is NOT checked here, and cannot be: whether the result looks better. That
needs a GPU and real weights.
"""
import pytest
@pytest.fixture(autouse=True)
def _needs_comfy(comfyui):
"""Imports comfy and torch."""
def _latent(frames=4, size=16, seed=0):
import torch
torch.manual_seed(seed)
return torch.randn(1, 3, frames, size, size)
# --- the filter ------------------------------------------------------------
def test_blurring_removes_high_frequency_detail():
"""A low-pass, stated as something measurable: neighbouring pixels of the
blurred frame differ less than they did."""
import torch
from modules.sampling.alg.blur import blur_frames
sharp = _latent()
out = blur_frames(sharp, kappa=4.0, frame_indices=(0,))
def roughness(frame):
return (frame[..., 1:] - frame[..., :-1]).abs().mean()
assert roughness(out[:, :, 0]) < roughness(sharp[:, :, 0])
def test_only_the_named_frames_change():
"""The anchor is frame 0. Blurring the whole clip would remove the detail
every other frame is supposed to resolve."""
import torch
from modules.sampling.alg.blur import blur_frames
sharp = _latent(frames=4)
out = blur_frames(sharp, kappa=4.0, frame_indices=(0,))
assert not torch.equal(out[:, :, 0], sharp[:, :, 0])
for index in (1, 2, 3):
assert torch.equal(out[:, :, index], sharp[:, :, index]), f"frame {index} changed"
def test_the_original_is_not_modified():
"""The sharp anchor is swapped back in later, so it has to survive."""
import torch
from modules.sampling.alg.blur import blur_frames
sharp = _latent()
keep = sharp.clone()
blur_frames(sharp, kappa=4.0)
assert torch.equal(sharp, keep)
def test_shape_and_dtype_are_preserved():
import torch
from modules.sampling.alg.blur import blur_frames
sharp = _latent().to(torch.float16)
out = blur_frames(sharp, kappa=4.0)
assert out.shape == sharp.shape and out.dtype == sharp.dtype
@pytest.mark.parametrize("bad", ["not a tensor", None])
def test_a_latent_it_does_not_understand_yields_none(bad):
from modules.sampling.alg.blur import blur_frames
assert blur_frames(bad, kappa=4.0) is None
def test_a_latent_of_the_wrong_rank_yields_none():
"""An image latent has no anchor frame to loosen. None means "not
applicable", and the caller leaves sampling alone."""
import torch
from modules.sampling.alg.blur import blur_frames
assert blur_frames(torch.randn(1, 4, 64, 64), kappa=4.0) is None
def test_a_frame_index_past_the_end_is_skipped_not_guessed():
from modules.sampling.alg.blur import blur_frames
assert blur_frames(_latent(frames=2), kappa=4.0, frame_indices=(9,)) is None
def test_a_strength_that_would_change_nothing_yields_none():
"""kappa 1.0 downsamples to the same size, so the "blurred" copy would be
the sharp one and every step would swap between identical tensors."""
from modules.sampling.alg.blur import blur_frames
assert blur_frames(_latent(), kappa=1.0) is None
# --- the per-step choice ---------------------------------------------------
@pytest.mark.parametrize("sigma,threshold,expected", [
(0.9, 0.6, True), # early, high noise: loosen it
(0.6, 0.6, False), # exactly at the threshold: sharp
(0.1, 0.6, False), # late: sharp, so detail can resolve
])
def test_the_choice_follows_sigma(sigma, threshold, expected):
from modules.sampling.alg.blur import use_blurred
assert use_blurred(sigma, threshold) is expected
def test_a_sigma_it_cannot_read_falls_back_to_sharp():
"""Never cost a step over the guidance: unreadable means untouched."""
from modules.sampling.alg.blur import use_blurred
assert use_blurred("nonsense", 0.6) is False
def test_a_tensor_sigma_is_read_like_the_sampler_passes_it():
import torch
from modules.sampling.alg.blur import use_blurred
assert use_blurred(torch.tensor([0.9]), 0.6) is True
# --- the proxy -------------------------------------------------------------
def test_the_proxy_repins_the_anchor_from_each_calls_own_sigma():
"""Multi-evaluation samplers call the denoiser more than once per step at
different sigmas, and each call must get the anchor its own sigma calls for."""
from modules.sampling.alg.blur import AnchorSwap
class Denoiser:
latent_image = None
def __call__(self, x, sigma, **kwargs):
return self.latent_image
inner = Denoiser()
proxy = AnchorSwap(inner, sharp="SHARP", blurred="BLURRED", threshold=0.6)
assert proxy(None, 0.9) == "BLURRED"
assert proxy(None, 0.2) == "SHARP"
assert proxy(None, 0.7) == "BLURRED"
def test_the_proxy_is_transparent_for_everything_else():
from modules.sampling.alg.blur import AnchorSwap
class Denoiser:
inner_model = "the real model"
def __call__(self, x, sigma, **kwargs):
return None
inner = Denoiser()
proxy = AnchorSwap(inner, "S", "B", 0.6)
assert proxy.inner_model == "the real model"
proxy.something = 7
assert inner.something == 7, "an assignment did not reach the real denoiser"
def test_a_threshold_that_is_not_a_number_would_disable_it_everywhere():
"""Why the check at the load node matters, in ALG's own terms. NaN compares
False against every sigma, so the anchor is never loosened at any step --
and nothing about the run looks wrong."""
from modules.sampling.alg.blur import use_blurred
assert all(use_blurred(sigma, float("nan")) is False
for sigma in (0.99, 0.5, 0.01))