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>
171 lines
5.8 KiB
Python
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))
|