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:
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"]
|
||||
)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user