Files
olivv-cs-ComfyUI-FunPack/core/contract.py
T
digital-garbageandClaude Opus 5 fa2caa8860 latent: an empty latent the model decides the shape of
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>
2026-08-29 12:21:25 +03:00

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,
}