Old project/workflow names and browser settings migrate on load; /funpack redirects to /fun. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016acRss69eH1235eRdFTZHY
225 lines
7.4 KiB
Python
225 lines
7.4 KiB
Python
"""Traits: what is read off a model, and what a model's own module contributes.
|
|
|
|
The split under test is the one the architecture depends on -- core reads only
|
|
facts that need no knowledge of any particular model, and everything else is
|
|
announced. If core ever learned a model's name, supporting the next one would
|
|
mean editing core, which is the whole thing this avoids.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from core.contract import ModuleSpec
|
|
from core.traits import contributed, has_block, missing_for, split, traits_of, universal
|
|
|
|
|
|
class _LatentFormat:
|
|
def __init__(self, dims, tdr=1):
|
|
self.latent_dimensions = dims
|
|
self.temporal_downscale_ratio = tdr
|
|
|
|
|
|
class _Kind:
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
|
|
class _Model:
|
|
"""The shape traits_of() reads: patcher -> model -> model_config."""
|
|
|
|
def __init__(self, dims=3, tdr=4, kind="FLOW"):
|
|
outer = self
|
|
|
|
class Inner:
|
|
model_type = _Kind(kind)
|
|
|
|
class model_config:
|
|
latent_format = _LatentFormat(dims, tdr)
|
|
|
|
self.model = Inner()
|
|
del outer
|
|
|
|
|
|
def test_latent_rank_is_read_not_guessed():
|
|
assert "temporal_latent" in universal(_Model(dims=3))
|
|
assert "spatial_latent" in universal(_Model(dims=2))
|
|
assert "waveform_latent" in universal(_Model(dims=1))
|
|
|
|
|
|
def test_the_prediction_type_is_named():
|
|
assert "predict_flow" in universal(_Model(kind="FLOW"))
|
|
assert "predict_eps" in universal(_Model(kind="EPS"))
|
|
|
|
|
|
def test_an_unreadable_model_yields_nothing_rather_than_raising():
|
|
class Bare:
|
|
model = None
|
|
|
|
assert traits_of(Bare()) == []
|
|
|
|
|
|
def test_a_module_contributes_traits_core_could_not_read():
|
|
spec = ModuleSpec(id="m", title="M", mount="", traits=lambda model: ["audio_stream"])
|
|
assert "audio_stream" in traits_of(_Model(), [spec])
|
|
|
|
|
|
def test_a_provider_that_raises_does_not_silence_the_others():
|
|
def explodes(model):
|
|
raise RuntimeError("boom")
|
|
|
|
bad = ModuleSpec(id="bad", title="B", mount="", traits=explodes)
|
|
good = ModuleSpec(id="good", title="G", mount="", traits=lambda m: ["audio_stream"])
|
|
|
|
found = traits_of(_Model(), [bad, good])
|
|
assert "audio_stream" in found, "one bad provider disabled every other module"
|
|
assert "temporal_latent" in found
|
|
|
|
|
|
def test_a_provider_that_raises_is_not_reported_as_one_that_never_loaded():
|
|
"""The log line, not only the surviving traits.
|
|
|
|
A traits provider checks whether the model is its own FIRST, so reaching the
|
|
exception means it loaded, recognised the model and then broke. Saying "did
|
|
not load" sends a reader after an import error that never happened -- and
|
|
losing a model's traits quietly narrows which modifiers are considered
|
|
compatible, so the log is the only thing pointing at the cause.
|
|
"""
|
|
from core import log
|
|
|
|
def explodes(model):
|
|
raise RuntimeError("adaln lookup exploded")
|
|
|
|
log._reset()
|
|
traits_of(_Model(), [ModuleSpec(id="bad", title="B", mount="", traits=explodes)])
|
|
|
|
said = log.history()
|
|
assert said, "a provider broke and nothing was said"
|
|
assert not any("did not load" in r["message"] for r in said), \
|
|
[r["message"] for r in said]
|
|
assert any("failed while" in r["message"] for r in said), [r["message"] for r in said]
|
|
assert said[-1]["level"] == log.WARNING
|
|
|
|
|
|
def test_declaring_no_traits_means_compatible_with_everything():
|
|
# Narrowing, not opting in -- the property that lets a modifier reach models
|
|
# FunUI knows nothing about.
|
|
plain = ModuleSpec(id="plain", title="P", mount="")
|
|
compatible, incompatible = split([plain], [])
|
|
assert compatible == [plain] and incompatible == []
|
|
|
|
|
|
def test_declaring_a_trait_excludes_the_models_that_lack_it():
|
|
picky = ModuleSpec(id="picky", title="P", mount="", requires=["audio_stream"])
|
|
compatible, incompatible = split([picky], ["temporal_latent"])
|
|
assert incompatible == [picky]
|
|
assert missing_for(picky, ["temporal_latent"]) == ["audio_stream"]
|
|
|
|
|
|
def test_a_structural_probe_finds_a_block_by_class_name():
|
|
class Target:
|
|
pass
|
|
|
|
class Root:
|
|
def modules(self):
|
|
return [self, Target()]
|
|
|
|
class Model:
|
|
class model:
|
|
diffusion_model = Root()
|
|
|
|
# The question a modifier actually has: does this model have the thing I
|
|
# touch -- not "is this the model I was written for".
|
|
assert has_block(Model(), "Target")
|
|
assert not has_block(Model(), "SomethingElse")
|
|
|
|
|
|
def test_a_probe_on_a_model_with_no_module_tree_is_false_not_fatal():
|
|
class Model:
|
|
model = None
|
|
|
|
assert has_block(Model(), "Anything") is False
|
|
|
|
|
|
# --- against ComfyUI's real model configs ---------------------------------
|
|
|
|
@pytest.fixture(scope="module")
|
|
def real_configs(comfyui):
|
|
import comfy.supported_models as sm
|
|
return sm.models
|
|
|
|
|
|
def test_every_real_model_config_can_be_read(real_configs):
|
|
# The point is coverage, not a particular answer: whatever ships upstream,
|
|
# reading it must not raise and must not invent anything.
|
|
known = {"temporal_latent", "spatial_latent", "waveform_latent",
|
|
"temporal_compression"}
|
|
for cfg in real_configs:
|
|
model = _Model.__new__(_Model)
|
|
|
|
class Inner:
|
|
model_type = None
|
|
model_config = cfg
|
|
|
|
model.model = Inner()
|
|
found = universal(model)
|
|
assert found <= known, f"{cfg.__name__} produced an unexpected trait: {found - known}"
|
|
|
|
|
|
def test_traits_describe_the_tensor_not_an_assumed_modality(real_configs):
|
|
"""QwenImage is an IMAGE model with a 3-D latent. LTXV is a video model with
|
|
a 3-D latent. They must read the same, because the latent really is the same
|
|
shape -- claiming otherwise would be FunUI guessing at modality, which
|
|
ComfyUI itself does not classify."""
|
|
by_name = {cfg.__name__: cfg for cfg in real_configs}
|
|
qwen, ltxv = by_name.get("QwenImage"), by_name.get("LTXV")
|
|
if qwen is None or ltxv is None:
|
|
pytest.skip("upstream renamed these configs")
|
|
|
|
def read(cfg):
|
|
model = _Model.__new__(_Model)
|
|
|
|
class Inner:
|
|
model_type = None
|
|
model_config = cfg
|
|
|
|
model.model = Inner()
|
|
return universal(model)
|
|
|
|
assert "temporal_latent" in read(qwen)
|
|
assert "temporal_latent" in read(ltxv)
|
|
|
|
|
|
def test_a_bare_probe_name_collides_across_architectures():
|
|
"""`Attention`, `MLP`, `Block` are not distinctive. A bare name matches any
|
|
of them, which is why a probe that needs to be sure must qualify."""
|
|
|
|
class Root:
|
|
def modules(self):
|
|
# A class named Attention, but from an unrelated architecture.
|
|
other = type("Attention", (), {})
|
|
other.__module__ = "somewhere.else.entirely"
|
|
return [self, other()]
|
|
|
|
class Model:
|
|
class model:
|
|
diffusion_model = Root()
|
|
|
|
assert has_block(Model(), "Attention") # the collision, unqualified
|
|
assert not has_block(Model(), "comfy.ldm.minimax.model.Attention")
|
|
|
|
|
|
def test_a_qualified_probe_matches_its_own_architecture():
|
|
class Root:
|
|
def modules(self):
|
|
target = type("AdalnProj", (), {})
|
|
target.__module__ = "comfy.ldm.minimax.model"
|
|
return [self, target()]
|
|
|
|
class Model:
|
|
class model:
|
|
diffusion_model = Root()
|
|
|
|
assert has_block(Model(), "comfy.ldm.minimax.model.AdalnProj")
|
|
assert has_block(Model(), "minimax.model.AdalnProj") # tail match
|
|
assert has_block(Model(), "AdalnProj") # distinctive enough
|
|
assert not has_block(Model(), "other.pack.AdalnProj")
|