Derived from latent_format where that is possible, which is every single-tensor model: SDXL, Wan21 and LTXV all come out matching the numbers core's own nodes compute, including models that ship after this. H3 cannot be derived and is the reason modules exist. Its latent is a NestedTensor of a video and an audio branch, its video channel count is not the latent_channels its format reports, and its length snaps to a frame grid. So modules/models/minimax_h3 claims it and builds it, importing the grid from ComfyUI rather than copying a second definition of it. Nothing outside that folder names a model, and nothing here assumes video: a 2-D latent format yields an image latent with no branch anywhere. Modules reach core through modules/_core.py. ComfyUI imports a pack by file location, so an absolute `import core` in a module resolves under pytest and not under ComfyUI -- the node then never registers, silently. That cost the same bug twice, so there is now a test that loads the pack the way ComfyUI does, in an isolated subprocess whose cwd is NOT the repo. The first version of that test passed with the bug reintroduced, because cwd was on sys.path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
91 lines
4.1 KiB
Python
91 lines
4.1 KiB
Python
"""What a module declares, and the vocabulary it may use.
|
|
|
|
Core holds no list of features. A module announces itself and this file says
|
|
what a valid announcement looks like -- nothing here names an implementation.
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Callable, Dict, List, Optional
|
|
|
|
# Bumped when the shape of an announcement changes. A module declaring an older
|
|
# version is not quietly adapted: it is refused, because guessing at what an old
|
|
# declaration meant is how silent behaviour changes happen.
|
|
CONTRACT_VERSION = 1
|
|
|
|
# Closed set. A type outside it has no renderer and no validation, so accepting
|
|
# one would mean rendering something approximate.
|
|
TYPES = frozenset({"bool", "int", "float", "enum", "text", "multiline", "path"})
|
|
|
|
NUMERIC = frozenset({"int", "float"})
|
|
|
|
# Which renderers each type allows, first being the default. A hint outside its
|
|
# type's list is refused at import: the alternative is a panel that silently
|
|
# renders something other than what was asked for.
|
|
UI_HINTS: Dict[str, List[str]] = {
|
|
"bool": ["checkboxRow", "toggle"],
|
|
"int": ["number", "slider", "stepper", "macroSlider"],
|
|
"float": ["number", "slider", "stepper", "macroSlider"],
|
|
"enum": ["select", "segmented", "radioGroup", "filterList", "wheel"],
|
|
"text": ["input", "search"],
|
|
"multiline": ["textarea", "autoTextarea"],
|
|
"path": ["filterList"],
|
|
}
|
|
|
|
# Ordering is coarse-grained by stage, then by declared relations within it.
|
|
# Numbers are deliberately absent: a priority number is a global namespace, so
|
|
# inserting one module means renumbering the others.
|
|
STAGES: List[str] = ["load", "conditioning", "latent", "guidance", "sampling", "post"]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ModuleSpec:
|
|
"""One module's announcement, after validation."""
|
|
|
|
id: str
|
|
title: str
|
|
mount: str
|
|
settings: Dict[str, dict] = field(default_factory=dict)
|
|
requires: List[str] = field(default_factory=list) # model traits
|
|
after: List[str] = field(default_factory=list) # module ids
|
|
before: List[str] = field(default_factory=list)
|
|
stage: str = "sampling"
|
|
# ComfyUI node classes this module contributes. Kept out of `settings` on
|
|
# purpose: a setting is a preference, a node is graph structure, and letting
|
|
# a new setting change a node's socket list would rot every saved workflow.
|
|
nodes: List[type] = field(default_factory=list)
|
|
# A callable(model) -> iterable[str], contributing traits core cannot read on
|
|
# its own. This is how a model's own module teaches the system to recognise
|
|
# it, so supporting a new model is a new folder and never an edit to core.
|
|
traits: Optional[Callable] = None
|
|
# Named capabilities this module offers to OTHER modules. Core never reads a
|
|
# name out of here and never defines one: it is a lookup, so a node can ask
|
|
# "who can build one of these" without core learning what the thing is.
|
|
provides: Dict[str, Callable] = field(default_factory=dict)
|
|
ui: Optional[str] = None # served path to its ui.js
|
|
status: str = "experimental" # or "proven"
|
|
source: str = "" # dotted import path
|
|
|
|
def defaults(self) -> Dict[str, Any]:
|
|
"""The values a headless run gets when no panel has been rendered.
|
|
|
|
Derived from the same declaration the panel renders, so the two cannot
|
|
disagree about what a setting means when nobody has touched it.
|
|
"""
|
|
return {key: spec["default"] for key, spec in self.settings.items()}
|
|
|
|
def to_manifest(self) -> dict:
|
|
# `nodes` is deliberately absent: it is not JSON, and the browser has no
|
|
# use for it. The graph is ComfyUI's surface, the manifest is the app's.
|
|
return {
|
|
"id": self.id,
|
|
"title": self.title,
|
|
"mount": self.mount,
|
|
"settings": self.settings,
|
|
"requires": list(self.requires),
|
|
"after": list(self.after),
|
|
"before": list(self.before),
|
|
"stage": self.stage,
|
|
"ui": self.ui,
|
|
"status": self.status,
|
|
}
|