refactor(model_resolution_picker): move the family table to families.json with a validator

- ship the table as families.json with per-family notes from the old source comments
- add table.py: load_table, pure validate_table and build_families
- families.py loads and validates at import, raising RuntimeError on problems
- include *.json as package data
This commit is contained in:
Vito Sansevero
2026-10-27 11:54:19 -07:00
parent 61b7d83593
commit db64038b25
5 changed files with 2367 additions and 809 deletions
File diff suppressed because it is too large Load Diff
@@ -1,819 +1,50 @@
"""Model family table for the Model Resolution Picker node.
Plain data: sizes are literals, authored once with ``logic.generate_size`` and
official lists. Every list is sorted by pixel count and mirrored exactly.
The data lives in ``families.json`` next to this module; it is loaded and
validated once at import (see ``table.py``). A shipped file that fails
validation is a startup error.
"""
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
CHIPS: Tuple[str, ...] = (
"1:1",
"4:5",
"5:4",
"3:4",
"4:3",
"2:3",
"3:2",
"9:16",
"16:9",
"21:9",
"9:21",
)
MIRROR: Dict[str, str] = {
"1:1": "1:1",
"4:5": "5:4",
"5:4": "4:5",
"3:4": "4:3",
"4:3": "3:4",
"2:3": "3:2",
"3:2": "2:3",
"9:16": "16:9",
"16:9": "9:16",
"21:9": "9:21",
"9:21": "21:9",
}
Size = Tuple[int, int]
class Family(NamedTuple):
"""One model family: grid constraints, size tiers and the chip size lists."""
label: str
aliases: Tuple[str, ...]
models: str
group: str
vae_ratio: int
step: int
megapixels: Tuple[float, ...]
default: Size
sizes: Dict[str, Tuple[Size, ...]]
FAMILIES: Tuple[Family, ...] = (
# generated at tiers 0.25, 0.5 MP, step 64, plus the 512/768 native sizes
Family(
label="SD 1.5",
aliases=(),
models="SD 1.5 and SD 1.5 finetunes",
group="image",
vae_ratio=8,
step=64,
megapixels=(
0.25,
0.5,
),
default=(512, 512),
sizes={
"1:1": (
(512, 512),
(704, 704),
(768, 768),
),
"4:5": (
(448, 576),
(640, 832),
),
"5:4": (
(576, 448),
(832, 640),
),
"3:4": (
(576, 768),
(768, 1024),
),
"4:3": (
(768, 576),
(1024, 768),
),
"2:3": (
(448, 640),
(512, 768),
(576, 896),
),
"3:2": (
(640, 448),
(768, 512),
(896, 576),
),
"9:16": (
(384, 704),
(512, 960),
),
"16:9": (
(704, 384),
(960, 512),
),
"21:9": (
(768, 320),
(1088, 448),
),
"9:21": (
(320, 768),
(448, 1088),
),
},
),
# SDXL model card / community bucket list
Family(
label="SDXL",
aliases=(),
models="SDXL, Pony and other SDXL finetunes",
group="image",
vae_ratio=8,
step=64,
megapixels=(1.0,),
default=(1024, 1024),
sizes={
"1:1": ((1024, 1024),),
"4:5": ((832, 1024),),
"5:4": ((1024, 832),),
"3:4": ((896, 1152),),
"4:3": ((1152, 896),),
"2:3": ((832, 1216),),
"3:2": ((1216, 832),),
"9:16": ((768, 1344),),
"16:9": ((1344, 768),),
"21:9": ((1536, 640),),
"9:21": ((640, 1536),),
},
),
# Illustrious XL v1.0 release notes resolution list (SDXL list plus larger buckets)
Family(
label="Illustrious XL",
aliases=(),
models="Illustrious XL v1.0 and later, NoobAI, WAI and other Illustrious finetunes",
group="image",
vae_ratio=8,
step=32,
megapixels=(
1.0,
1.5,
2.25,
),
default=(1024, 1024),
sizes={
"1:1": (
(1024, 1024),
(1536, 1536),
),
"4:5": (
(832, 1024),
(1248, 1536),
),
"5:4": (
(1024, 832),
(1536, 1248),
),
"3:4": (
(896, 1152),
(1344, 1792),
),
"4:3": (
(1152, 896),
(1792, 1344),
),
"2:3": (
(832, 1216),
(1024, 1536),
(1248, 1824),
),
"3:2": (
(1216, 832),
(1536, 1024),
(1824, 1248),
),
"9:16": (
(768, 1344),
(1152, 2048),
),
"16:9": (
(1344, 768),
(2048, 1152),
),
"21:9": ((1536, 640),),
"9:21": ((640, 1536),),
},
),
# generated at tiers 0.5, 1.0, 1.5 MP, step 16, sides 512-1536
Family(
label="Anima",
aliases=(),
models="Anima",
group="image",
vae_ratio=8,
step=16,
megapixels=(
0.5,
1.0,
1.5,
),
default=(1024, 1024),
sizes={
"1:1": (
(720, 720),
(1024, 1024),
(1248, 1248),
),
"4:5": (
(640, 816),
(912, 1152),
(1120, 1408),
),
"5:4": (
(816, 640),
(1152, 912),
(1408, 1120),
),
"3:4": (
(624, 832),
(880, 1184),
(1088, 1456),
),
"4:3": (
(832, 624),
(1184, 880),
(1456, 1088),
),
"2:3": (
(592, 880),
(832, 1248),
(1024, 1536),
),
"3:2": (
(880, 592),
(1248, 832),
(1536, 1024),
),
"9:16": (
(544, 960),
(768, 1360),
),
"16:9": (
(960, 544),
(1360, 768),
),
"21:9": (),
"9:21": (),
},
),
# generated at tiers 0.5, 1.0, 1.5, 2.0 MP, step 16; 16:9 2.0 MP set to 1920x1088
Family(
label="Flux.1",
aliases=(),
models="Flux.1 dev / schnell / Krea, Chroma, HiDream I1, SD3.5, Lumina 2",
group="image",
vae_ratio=8,
step=16,
megapixels=(
0.5,
1.0,
1.5,
2.0,
),
default=(1024, 1024),
sizes={
"1:1": (
(720, 720),
(1024, 1024),
(1248, 1248),
(1456, 1456),
),
"4:5": (
(640, 816),
(912, 1152),
(1120, 1408),
(1296, 1616),
),
"5:4": (
(816, 640),
(1152, 912),
(1408, 1120),
(1616, 1296),
),
"3:4": (
(624, 832),
(880, 1184),
(1088, 1456),
(1248, 1680),
),
"4:3": (
(832, 624),
(1184, 880),
(1456, 1088),
(1680, 1248),
),
"2:3": (
(592, 880),
(832, 1248),
(1024, 1536),
(1184, 1776),
),
"3:2": (
(880, 592),
(1248, 832),
(1536, 1024),
(1776, 1184),
),
"9:16": (
(544, 960),
(768, 1360),
(944, 1680),
(1088, 1920),
),
"16:9": (
(960, 544),
(1360, 768),
(1680, 944),
(1920, 1088),
),
"21:9": (
(1104, 480),
(1568, 672),
(1920, 816),
(2208, 944),
),
"9:21": (
(480, 1104),
(672, 1568),
(816, 1920),
(944, 2208),
),
},
),
# Z-Image model card resolution list
Family(
label="Z-Image",
aliases=(),
models="Z-Image Turbo and Z-Image Base",
group="image",
vae_ratio=8,
step=16,
megapixels=(
1.0,
1.6,
),
default=(1024, 1024),
sizes={
"1:1": (
(1024, 1024),
(1280, 1280),
),
"4:5": (),
"5:4": (),
"3:4": (
(864, 1152),
(896, 1152),
(1120, 1440),
(1104, 1472),
),
"4:3": (
(1152, 864),
(1152, 896),
(1440, 1120),
(1472, 1104),
),
"2:3": (
(832, 1248),
(1024, 1536),
),
"3:2": (
(1248, 832),
(1536, 1024),
),
"9:16": (
(720, 1280),
(896, 1600),
),
"16:9": (
(1280, 720),
(1600, 896),
),
"21:9": (
(1344, 576),
(1680, 720),
),
"9:21": (
(576, 1344),
(720, 1680),
),
},
),
# Qwen-Image model card resolution list. 4:3 is 1472x1104, the project
# maintainer's correction of the README's 1140 (QwenLM/Qwen-Image issue #7,
# "resolutions used in our final training stage").
Family(
label="Qwen-Image",
aliases=(),
models="Qwen-Image and Qwen-Image 2512",
group="image",
vae_ratio=8,
step=16,
megapixels=(1.76,),
default=(1328, 1328),
sizes={
"1:1": ((1328, 1328),),
"4:5": (),
"5:4": (),
"3:4": ((1104, 1472),),
"4:3": ((1472, 1104),),
"2:3": ((1056, 1584),),
"3:2": ((1584, 1056),),
"9:16": ((928, 1664),),
"16:9": ((1664, 928),),
"21:9": (),
"9:21": (),
},
),
# generated at tiers 1.0, 2.0, 4.0 MP, step 16; 16:9 2.0 MP set to 1920x1088
Family(
label="Flux.2 / Klein / MOSS",
aliases=(),
models="Flux.2 dev, Flux.2 Klein, Flux.3 Klein, MOSS-Image, Ideogram 4",
group="image",
vae_ratio=16,
step=16,
megapixels=(
1.0,
2.0,
4.0,
),
default=(1024, 1024),
sizes={
"1:1": (
(1024, 1024),
(1456, 1456),
(2048, 2048),
),
"4:5": (
(912, 1152),
(1296, 1616),
(1824, 2288),
),
"5:4": (
(1152, 912),
(1616, 1296),
(2288, 1824),
),
"3:4": (
(880, 1184),
(1248, 1680),
(1776, 2368),
),
"4:3": (
(1184, 880),
(1680, 1248),
(2368, 1776),
),
"2:3": (
(832, 1248),
(1184, 1776),
(1680, 2512),
),
"3:2": (
(1248, 832),
(1776, 1184),
(2512, 1680),
),
"9:16": (
(768, 1360),
(1088, 1920),
(1536, 2736),
),
"16:9": (
(1360, 768),
(1920, 1088),
(2736, 1536),
),
"21:9": (
(1568, 672),
(2208, 944),
(3136, 1344),
),
"9:21": (
(672, 1568),
(944, 2208),
(1344, 3136),
),
},
),
# Qwen-Image 2.1 model card list, plus generated at tier 1.0 MP, step 32
Family(
label="Qwen-Image 2.1",
aliases=(),
models="Qwen-Image 2.1 and 2.1 Turbo",
group="image",
vae_ratio=16,
step=32,
megapixels=(
1.0,
4.0,
),
default=(1024, 1024),
sizes={
"1:1": (
(1024, 1024),
(2048, 2048),
),
"4:5": ((928, 1152),),
"5:4": ((1152, 928),),
"3:4": (
(896, 1184),
(1792, 2400),
),
"4:3": (
(1184, 896),
(2400, 1792),
),
"2:3": (
(832, 1248),
(1696, 2528),
),
"3:2": (
(1248, 832),
(2528, 1696),
),
"9:16": (
(768, 1376),
(1536, 2752),
),
"16:9": (
(1376, 768),
(2752, 1536),
),
"21:9": ((1568, 672),),
"9:21": ((672, 1568),),
},
),
# HunyuanImage 2.1 model card resolution list
Family(
label="HunyuanImage 2.1",
aliases=(),
models="HunyuanImage 2.1",
group="image",
vae_ratio=32,
step=32,
megapixels=(4.0,),
default=(2048, 2048),
sizes={
"1:1": ((2048, 2048),),
"4:5": (),
"5:4": (),
"3:4": ((1792, 2304),),
"4:3": ((2304, 1792),),
"2:3": ((1664, 2496),),
"3:2": ((2496, 1664),),
"9:16": ((1536, 2560),),
"16:9": ((2560, 1536),),
"21:9": (),
"9:21": (),
},
),
# Wan 2.2 official sizes plus generated at tiers 0.4, 0.9 MP, step 16
Family(
label="Wan 2.2 (keyframes)",
aliases=(),
models="Wan 2.2 keyframes: frame sizes for first/last-frame generation; the latent output is an image latent",
group="video",
vae_ratio=16,
step=16,
megapixels=(
0.4,
0.9,
),
default=(1280, 720),
sizes={
"1:1": (
(640, 640),
(960, 960),
(976, 976),
),
"4:5": (
(576, 720),
(864, 1088),
),
"5:4": (
(720, 576),
(1088, 864),
),
"3:4": (
(560, 752),
(848, 1120),
),
"4:3": (
(752, 560),
(1120, 848),
),
"2:3": (
(528, 800),
(800, 1184),
),
"3:2": (
(800, 528),
(1184, 800),
),
"9:16": (
(480, 832),
(480, 864),
(704, 1280),
(720, 1280),
(736, 1296),
),
"16:9": (
(832, 480),
(864, 480),
(1280, 704),
(1280, 720),
(1296, 736),
),
"21:9": (
(992, 416),
(1488, 640),
),
"9:21": (
(416, 992),
(640, 1488),
),
},
),
# LTX-2 official sizes plus generated at tiers 0.4, 0.9 MP, step 32
Family(
label="LTX-2 (keyframes)",
aliases=(),
models="LTX-2 keyframes: frame sizes for first/last-frame generation; the latent output is an image latent",
group="video",
vae_ratio=32,
step=32,
megapixels=(
0.4,
0.9,
),
default=(768, 512),
sizes={
"1:1": (
(640, 640),
(960, 960),
),
"4:5": ((864, 1088),),
"5:4": ((1088, 864),),
"3:4": (
(576, 736),
(832, 1120),
),
"4:3": (
(736, 576),
(1120, 832),
),
"2:3": (
(512, 768),
(544, 800),
(800, 1184),
),
"3:2": (
(768, 512),
(800, 544),
(1184, 800),
),
"9:16": (
(480, 864),
(544, 960),
(736, 1280),
),
"16:9": (
(864, 480),
(960, 544),
(1280, 736),
),
"21:9": (
(992, 416),
(1472, 640),
),
"9:21": (
(416, 992),
(640, 1472),
),
},
),
# HunyuanVideo 1.5 official sizes plus generated at tiers 0.4, 0.9 MP, step 16
Family(
label="HunyuanVideo 1.5 (keyframes)",
aliases=(),
models="HunyuanVideo 1.5 keyframes: frame sizes for first/last-frame generation; the latent output is an image latent",
group="video",
vae_ratio=16,
step=16,
megapixels=(
0.4,
0.9,
),
default=(1280, 720),
sizes={
"1:1": (
(640, 640),
(976, 976),
),
"4:5": (
(576, 720),
(864, 1088),
),
"5:4": (
(720, 576),
(1088, 864),
),
"3:4": (
(560, 752),
(848, 1120),
),
"4:3": (
(752, 560),
(1120, 848),
),
"2:3": (
(528, 800),
(800, 1184),
),
"3:2": (
(800, 528),
(1184, 800),
),
"9:16": (
(480, 848),
(480, 864),
(720, 1280),
(736, 1296),
),
"16:9": (
(848, 480),
(864, 480),
(1280, 720),
(1296, 736),
),
"21:9": (
(992, 416),
(1488, 640),
),
"9:21": (
(416, 992),
(640, 1488),
),
},
),
# generated at tiers 0.4, 1.0 MP, step 32, plus the 864x480 default
Family(
label="MiniMax H3 (keyframes)",
aliases=(),
models="MiniMax H3 keyframes: frame sizes for first/last-frame generation; the latent output is an image latent",
group="video",
vae_ratio=16,
step=32,
megapixels=(
0.4,
1.0,
),
default=(864, 480),
sizes={
"1:1": (
(640, 640),
(1024, 1024),
),
"4:5": ((928, 1152),),
"5:4": ((1152, 928),),
"3:4": (
(576, 736),
(896, 1184),
),
"4:3": (
(736, 576),
(1184, 896),
),
"2:3": (
(544, 800),
(832, 1248),
),
"3:2": (
(800, 544),
(1248, 832),
),
"9:16": (
(480, 864),
(768, 1376),
),
"16:9": (
(864, 480),
(1376, 768),
),
"21:9": (
(992, 416),
(1568, 672),
),
"9:21": (
(416, 992),
(672, 1568),
),
},
),
from .table import (
CHIPS,
MIRROR,
Family,
Size,
build_families,
load_table,
validate_table,
)
DEFAULT_FAMILY = "SDXL"
__all__ = [
"CHIPS",
"MIRROR",
"Family",
"Size",
"FAMILIES",
"DEFAULT_FAMILY",
"family_labels",
"resolve_family",
"families_payload",
"load_shipped_families",
]
TABLE_PATH = Path(__file__).with_name("families.json")
def load_shipped_families(path: Path = TABLE_PATH) -> Tuple[Tuple[Family, ...], str]:
"""Load and validate ``path``; return its families and default label."""
data = load_table(path)
problems = validate_table(data)
if problems:
listing = "\n - ".join(problems)
raise RuntimeError(f"invalid family table {path}:\n - {listing}")
return build_families(data), data["default"]
FAMILIES, DEFAULT_FAMILY = load_shipped_families()
def family_labels() -> List[str]:
@@ -0,0 +1,265 @@
"""Load, validate and build the Model Resolution Picker family table.
The table is plain JSON (shipped as ``families.json``). ``validate_table`` is a
pure function that returns human-readable problems and never raises for data
problems, so the same check can guard the shipped file and a user override.
Unknown keys, such as a per-family ``"notes"`` string, are ignored.
"""
import json
from pathlib import Path
from typing import Any, Dict, List, NamedTuple, Tuple
CHIPS: Tuple[str, ...] = (
"1:1",
"4:5",
"5:4",
"3:4",
"4:3",
"2:3",
"3:2",
"9:16",
"16:9",
"21:9",
"9:21",
)
MIRROR: Dict[str, str] = {
"1:1": "1:1",
"4:5": "5:4",
"5:4": "4:5",
"3:4": "4:3",
"4:3": "3:4",
"2:3": "3:2",
"3:2": "2:3",
"9:16": "16:9",
"16:9": "9:16",
"21:9": "9:21",
"9:21": "21:9",
}
TABLE_VERSION = 1
GROUPS = ("image", "video")
VAE_RATIOS = (8, 16, 32)
CHIP_TOLERANCE = 0.07
Size = Tuple[int, int]
class Family(NamedTuple):
"""One model family: grid constraints, size tiers and the chip size lists."""
label: str
aliases: Tuple[str, ...]
models: str
group: str
vae_ratio: int
step: int
megapixels: Tuple[float, ...]
default: Size
sizes: Dict[str, Tuple[Size, ...]]
def load_table(path: Path) -> Dict[str, Any]:
"""Read ``path`` as JSON; raise ``ValueError`` naming the path on failure."""
try:
data = json.loads(Path(path).read_text(encoding="utf-8"))
except (OSError, ValueError) as error:
raise ValueError(f"cannot read family table {path}: {error}") from error
if not isinstance(data, dict):
raise ValueError(f"family table {path} must be a JSON object")
return data
def _is_size(value: Any) -> bool:
return (
isinstance(value, (list, tuple))
and len(value) == 2
and all(isinstance(v, int) and not isinstance(v, bool) and v > 0 for v in value)
)
def _is_number(value: Any) -> bool:
return isinstance(value, (int, float)) and not isinstance(value, bool)
def _chip_ratio(chip: str) -> float:
left, right = chip.split(":")
return int(left) / int(right)
def _validate_top_level(data: Dict[str, Any]) -> List[str]:
problems: List[str] = []
if data.get("version") != TABLE_VERSION:
problems.append(f"version must be {TABLE_VERSION}")
if data.get("chips") != list(CHIPS):
problems.append(f"chips must equal {list(CHIPS)}")
if data.get("mirror") != MIRROR:
problems.append("mirror must pair every chip with its mirrored chip")
if not isinstance(data.get("families"), list) or not data["families"]:
problems.append("families must be a non-empty list")
return problems
def _validate_names(families: List[Any]) -> List[str]:
problems: List[str] = []
seen: Dict[str, str] = {}
for index, fam in enumerate(families):
label = fam.get("label")
if not isinstance(label, str) or not label:
problems.append(f"family #{index + 1}: label must be a non-empty string")
continue
aliases = fam.get("aliases", [])
if not isinstance(aliases, list) or not all(
isinstance(a, str) for a in aliases
):
problems.append(f"family {label!r}: aliases must be a list of strings")
aliases = []
for name in [label, *aliases]:
if name in seen:
problems.append(
f"family {label!r}: duplicate label or alias {name!r} "
f"(already used by {seen[name]!r})"
)
else:
seen[name] = label
return problems
def _validate_scalars(label: str, fam: Dict[str, Any]) -> List[str]:
problems: List[str] = []
if not isinstance(fam.get("models"), str):
problems.append(f"family {label!r}: models must be a string")
if fam.get("group") not in GROUPS:
problems.append(f"family {label!r}: group must be one of {list(GROUPS)}")
if fam.get("vaeRatio") not in VAE_RATIOS:
problems.append(f"family {label!r}: vaeRatio must be one of {list(VAE_RATIOS)}")
step = fam.get("step")
if not _is_number(step) or step <= 0 or int(step) != step:
problems.append(f"family {label!r}: step must be a positive integer")
mp = fam.get("megapixels")
if not isinstance(mp, list) or not mp or not all(_is_number(m) for m in mp):
problems.append(f"family {label!r}: megapixels must be a non-empty number list")
return problems
def _validate_chip(
label: str, chip: str, entries: Any, fam: Dict[str, Any]
) -> List[str]:
where = f"family {label!r}, chip {chip}"
if not isinstance(entries, list) or not all(_is_size(s) for s in entries):
return [f"{where}: sizes must be a list of [width, height] integer pairs"]
problems: List[str] = []
step = fam.get("step")
step_ok = _is_number(step) and step > 0 and int(step) == step
target = _chip_ratio(chip)
for w, h in entries:
if step_ok and (w % step or h % step):
problems.append(f"{where}: {w}x{h} is not a multiple of step {int(step)}")
if abs(w / h - target) / target > CHIP_TOLERANCE:
problems.append(f"{where}: {w}x{h} is more than 7% off the {chip} ratio")
areas = [w * h for w, h in entries]
if areas != sorted(areas):
problems.append(f"{where}: sizes must be in ascending pixel order")
return problems
def _validate_sizes(label: str, fam: Dict[str, Any]) -> List[str]:
sizes = fam.get("sizes")
if not isinstance(sizes, dict):
return [f"family {label!r}: sizes must be an object keyed by chip"]
problems: List[str] = []
for chip in CHIPS:
if chip not in sizes:
problems.append(f"family {label!r}: sizes is missing chip {chip}")
else:
problems.extend(_validate_chip(label, chip, sizes[chip], fam))
valid = {
chip: [tuple(s) for s in sizes[chip]]
for chip in CHIPS
if chip in sizes
and isinstance(sizes[chip], list)
and all(_is_size(s) for s in sizes[chip])
}
problems.extend(_validate_mirrors(label, valid))
problems.extend(_validate_default(label, fam, valid))
return problems
def _validate_mirrors(label: str, sizes: Dict[str, List[Size]]) -> List[str]:
problems: List[str] = []
owner: Dict[Size, str] = {}
for chip, entries in sizes.items():
mirrored = sizes.get(MIRROR[chip])
for w, h in entries:
if mirrored is not None and (h, w) not in mirrored:
problems.append(
f"family {label!r}, chip {chip}: {w}x{h} has no mirror "
f"{h}x{w} under {MIRROR[chip]}"
)
if (w, h) in owner:
problems.append(
f"family {label!r}, chip {chip}: {w}x{h} is listed under more "
f"than one chip (also {owner[(w, h)]})"
)
else:
owner[(w, h)] = chip
return problems
def _validate_default(
label: str, fam: Dict[str, Any], sizes: Dict[str, Any]
) -> List[str]:
default = fam.get("default")
if not _is_size(default):
return [f"family {label!r}: default must be a [width, height] pair"]
if not any(tuple(default) in entries for entries in sizes.values()):
return [f"family {label!r}: default {list(default)} is not in its sizes"]
return []
def validate_table(data: Any) -> List[str]:
"""Return every problem found in ``data``; an empty list means it is valid."""
if not isinstance(data, dict):
return ["table must be a JSON object"]
problems = _validate_top_level(data)
families = data.get("families")
if not isinstance(families, list):
return problems
entries = [f for f in families if isinstance(f, dict)]
for index, fam in enumerate(families):
if not isinstance(fam, dict):
problems.append(f"family #{index + 1}: must be an object")
problems.extend(_validate_names(entries))
for fam in entries:
label = fam.get("label")
if not isinstance(label, str) or not label:
continue
problems.extend(_validate_scalars(label, fam))
problems.extend(_validate_sizes(label, fam))
labels = {f.get("label") for f in entries}
aliases = {
a for f in entries if isinstance(f.get("aliases"), list) for a in f["aliases"]
}
if data.get("default") not in labels | aliases:
problems.append(f"default {data.get('default')!r} is not a family label")
return problems
def build_families(data: Dict[str, Any]) -> Tuple[Family, ...]:
"""Build ``Family`` records from a table that passed ``validate_table``."""
return tuple(
Family(
label=fam["label"],
aliases=tuple(fam.get("aliases", ())),
models=fam["models"],
group=fam["group"],
vae_ratio=fam["vaeRatio"],
step=int(fam["step"]),
megapixels=tuple(fam["megapixels"]),
default=(fam["default"][0], fam["default"][1]),
sizes={
chip: tuple((w, h) for w, h in fam["sizes"].get(chip, ()))
for chip in CHIPS
},
)
for fam in data["families"]
)
+3
View File
@@ -37,6 +37,9 @@ Repository = "https://github.com/ComfyAssets/ComfyUI-KikoTools"
include = ["kikotools*"]
exclude = ["tests*", "web*"]
[tool.setuptools.package-data]
kikotools = ["**/*.json"]
[tool.comfy]
PublisherId = "kiko9"
DisplayName = "ComfyUI-KikoTools"
@@ -0,0 +1,211 @@
import copy
import json
from pathlib import Path
from typing import Any, Callable, Dict
import pytest
from kikotools.tools.model_resolution_picker import families as families_module
from kikotools.tools.model_resolution_picker.table import (
build_families,
load_table,
validate_table,
)
SHIPPED = Path(families_module.__file__).with_name("families.json")
def shipped_data() -> Dict[str, Any]:
return copy.deepcopy(json.loads(SHIPPED.read_text(encoding="utf-8")))
def family(data: Dict[str, Any], label: str) -> Dict[str, Any]:
return next(f for f in data["families"] if f["label"] == label)
def off_step(data: Dict[str, Any]) -> None:
family(data, "SD 1.5")["sizes"]["1:1"][1] = [700, 700]
def off_ratio(data: Dict[str, Any]) -> None:
sizes = family(data, "SD 1.5")["sizes"]["1:1"]
sizes[1:1] = [[576, 704], [704, 576]]
def missing_mirror(data: Dict[str, Any]) -> None:
family(data, "SD 1.5")["sizes"]["4:5"].pop()
def unsorted(data: Dict[str, Any]) -> None:
family(data, "SD 1.5")["sizes"]["1:1"].reverse()
def duplicate_size(data: Dict[str, Any]) -> None:
sizes = family(data, "SD 1.5")["sizes"]
for chip, size in (("3:4", [640, 832]), ("4:3", [832, 640])):
sizes[chip].append(size)
sizes[chip].sort(key=lambda s: s[0] * s[1])
def duplicate_label(data: Dict[str, Any]) -> None:
data["families"][2]["label"] = data["families"][0]["label"]
def missing_chip(data: Dict[str, Any]) -> None:
del family(data, "SD 1.5")["sizes"]["21:9"]
def default_not_listed(data: Dict[str, Any]) -> None:
family(data, "SD 1.5")["default"] = [520, 520]
def bad_vae_ratio(data: Dict[str, Any]) -> None:
family(data, "SD 1.5")["vaeRatio"] = 7
def bad_group(data: Dict[str, Any]) -> None:
family(data, "SD 1.5")["group"] = "audio"
def bad_step(data: Dict[str, Any]) -> None:
family(data, "SD 1.5")["step"] = 0
def empty_megapixels(data: Dict[str, Any]) -> None:
family(data, "SD 1.5")["megapixels"] = []
def duplicate_alias(data: Dict[str, Any]) -> None:
family(data, "SD 1.5")["aliases"] = ["SDXL"]
def test_validate_table_accepts_the_shipped_file():
assert validate_table(shipped_data()) == []
@pytest.mark.parametrize(
"mutate, needle, chip, count",
[
(off_step, "step", "1:1", 1),
(off_ratio, "7%", "1:1", 2),
(missing_mirror, "mirror", "5:4", 1),
(unsorted, "ascending", "1:1", 1),
(duplicate_size, "more than one chip", ("3:4", "4:3"), 2),
(missing_chip, "missing", "21:9", 1),
(default_not_listed, "default", None, 1),
(bad_vae_ratio, "vaeRatio", None, 1),
(bad_group, "group", None, 1),
(bad_step, "step", None, 1),
(empty_megapixels, "megapixels", None, 1),
],
)
def test_validate_table_reports_each_invariant(
mutate: Callable[[Dict[str, Any]], None],
needle: str,
chip: Any,
count: int,
):
data = shipped_data()
mutate(data)
problems = validate_table(data)
assert len(problems) == count, problems
for problem in problems:
assert "SD 1.5" in problem and needle in problem
if chip:
assert any(
c in problem for c in ([chip] if isinstance(chip, str) else chip)
)
@pytest.mark.parametrize(
"mutate, needle",
[(duplicate_label, "SD 1.5"), (duplicate_alias, "SDXL")],
)
def test_validate_table_reports_duplicate_names(
mutate: Callable[[Dict[str, Any]], None], needle: str
):
data = shipped_data()
mutate(data)
problems = validate_table(data)
assert len(problems) == 1 and "duplicate" in problems[0] and needle in problems[0]
def test_validate_table_checks_top_level_keys():
data = shipped_data()
data["version"] = 2
data["chips"] = data["chips"][:-1]
data["default"] = "Nope"
problems = validate_table(data)
assert any("version" in p for p in problems)
assert any("chips" in p for p in problems)
assert any("default" in p for p in problems)
def test_validate_table_never_raises_on_garbage():
assert validate_table([]) != [] # type: ignore[arg-type]
assert validate_table({}) != []
data = shipped_data()
data["families"][0] = "oops"
data["families"][1]["sizes"] = {"1:1": [[1, "x"]]}
assert validate_table(data) != []
def test_validate_table_ignores_unknown_keys():
data = shipped_data()
data["extra"] = 1
family(data, "SD 1.5")["notes"] = "kept for humans"
family(data, "SD 1.5")["whatever"] = {"a": 1}
assert validate_table(data) == []
assert build_families(data)[0].label == "SD 1.5"
def test_load_table_bad_json_names_the_path(tmp_path: Path):
bad = tmp_path / "broken.json"
bad.write_text("{not json", encoding="utf-8")
with pytest.raises(ValueError) as error:
load_table(bad)
assert str(bad) in str(error.value)
def test_load_table_missing_file_names_the_path(tmp_path: Path):
missing = tmp_path / "absent.json"
with pytest.raises(ValueError) as error:
load_table(missing)
assert str(missing) in str(error.value)
def test_families_module_is_built_from_json():
data = shipped_data()
assert [f.label for f in families_module.FAMILIES] == [
f["label"] for f in data["families"]
]
assert families_module.DEFAULT_FAMILY == data["default"]
assert families_module.families_payload()["families"][0]["sizes"] == {
chip: sizes for chip, sizes in data["families"][0]["sizes"].items()
}
def test_families_module_raises_runtime_error_on_invalid_shipped_file(
tmp_path: Path,
):
data = shipped_data()
bad_vae_ratio(data)
path = tmp_path / "families.json"
path.write_text(json.dumps(data), encoding="utf-8")
with pytest.raises(RuntimeError) as error:
families_module.load_shipped_families(path)
assert "SD 1.5" in str(error.value) and "vaeRatio" in str(error.value)