diff --git a/js/resolution_selector.js b/js/resolution_selector.js index cace2ea..f29db98 100644 --- a/js/resolution_selector.js +++ b/js/resolution_selector.js @@ -1,26 +1,26 @@ import { app } from "/scripts/app.js"; -// Model-specific resolution presets with constraints -// Includes: model-optimized sizes + photo print (4x6, 5x7, 8x10) + digital/social + canvas art ratios +// Model-specific resolution presets. Mirror of MODEL_RESOLUTIONS in nodes.py: +// keep in sync (test_resolution_selector.py diffs the two tables). const MODEL_RESOLUTIONS = { "Flux": { - "square": [[512, 512], [768, 768], [1024, 1024], [1088, 1088], [1280, 1280], [1536, 1536], [1920, 1920], [2048, 2048]], - "portrait": [[688, 2048], [768, 1344], [832, 1216], [896, 1152], [928, 1664], [1024, 1536], [1024, 1792], [1024, 2048], [1088, 1920], [1152, 2048], [1200, 1792], [1360, 2048], [1456, 2048], [1536, 2048], [1616, 2048], [1632, 2048], [1712, 2048]], - "landscape": [[1280, 720], [1344, 768], [1216, 832], [1152, 896], [1536, 1024], [1664, 928], [1792, 1024], [1792, 1200], [1920, 1088], [2048, 688], [2048, 1024], [2048, 1152], [2048, 1360], [2048, 1456], [2048, 1536], [2048, 1616], [2048, 1632], [2048, 1712]], + "square": [[512, 512], [768, 768], [1024, 1024], [1088, 1088], [1280, 1280], [1328, 1328], [1536, 1536], [1920, 1920], [2048, 2048]], + "portrait": [[688, 2048], [768, 1344], [832, 1216], [896, 1152], [928, 1664], [1024, 1536], [1024, 1792], [1024, 2048], [1056, 1584], [1088, 1920], [1104, 1472], [1152, 2048], [1200, 1792], [1360, 2048], [1456, 2048], [1536, 2048], [1616, 2048], [1632, 2048], [1712, 2048]], + "landscape": [[1280, 720], [1344, 768], [1216, 832], [1152, 896], [1472, 1104], [1536, 1024], [1584, 1056], [1664, 928], [1792, 1024], [1792, 1200], [1920, 1088], [2048, 688], [2048, 1024], [2048, 1152], [2048, 1360], [2048, 1456], [2048, 1536], [2048, 1616], [2048, 1632], [2048, 1712]], }, "Qwen Image": { "square": [[1024, 1024], [1080, 1080], [1280, 1280], [1328, 1328], [1536, 1536], [1920, 1920], [2048, 2048]], - "portrait": [[680, 2048], [928, 1664], [1024, 1536], [1024, 2048], [1080, 1920], [1140, 1472], [1152, 2048], [1200, 1800], [1368, 2048], [1464, 2048], [1536, 2048], [1608, 2048], [1640, 2048], [1704, 2048]], - "landscape": [[1280, 720], [1472, 1140], [1536, 1024], [1664, 928], [1800, 1200], [1920, 1080], [2048, 680], [2048, 1024], [2048, 1152], [2048, 1368], [2048, 1464], [2048, 1536], [2048, 1608], [2048, 1640], [2048, 1704]], + "portrait": [[680, 2048], [928, 1664], [1024, 1536], [1024, 2048], [1056, 1584], [1080, 1920], [1104, 1472], [1152, 2048], [1200, 1800], [1368, 2048], [1464, 2048], [1536, 2048], [1608, 2048], [1640, 2048], [1704, 2048]], + "landscape": [[1280, 720], [1472, 1104], [1536, 1024], [1584, 1056], [1664, 928], [1800, 1200], [1920, 1080], [2048, 680], [2048, 1024], [2048, 1152], [2048, 1368], [2048, 1464], [2048, 1536], [2048, 1608], [2048, 1640], [2048, 1704]], }, "Z-Image": { - "square": [[512, 512], [768, 768], [1024, 1024], [1080, 1080], [1280, 1280], [1536, 1536], [1920, 1920], [2048, 2048]], - "portrait": [[680, 2048], [720, 1280], [768, 1024], [1024, 2048], [1080, 1920], [1152, 2048], [1200, 1800], [1368, 2048], [1464, 2048], [1536, 2048], [1608, 2048], [1640, 2048], [1704, 2048]], - "landscape": [[1024, 768], [1280, 720], [1800, 1200], [1920, 1080], [2048, 680], [2048, 1024], [2048, 1152], [2048, 1368], [2048, 1464], [2048, 1536], [2048, 1608], [2048, 1640], [2048, 1704]], + "square": [[512, 512], [768, 768], [1024, 1024], [1080, 1080], [1280, 1280], [1328, 1328], [1536, 1536], [1920, 1920], [2048, 2048]], + "portrait": [[680, 2048], [720, 1280], [768, 1024], [928, 1664], [1024, 2048], [1056, 1584], [1080, 1920], [1104, 1472], [1152, 2048], [1200, 1800], [1368, 2048], [1464, 2048], [1536, 2048], [1608, 2048], [1640, 2048], [1704, 2048]], + "landscape": [[1024, 768], [1280, 720], [1472, 1104], [1584, 1056], [1664, 928], [1800, 1200], [1920, 1080], [2048, 680], [2048, 1024], [2048, 1152], [2048, 1368], [2048, 1464], [2048, 1536], [2048, 1608], [2048, 1640], [2048, 1704]], }, "SD 1.5": { "square": [[512, 512], [768, 768], [1024, 1024], [1080, 1080], [1280, 1280], [1536, 1536]], - "portrait": [[512, 768], [512, 682], [512, 1024], [680, 2048], [768, 1024], [768, 1344], [1024, 2048], [1080, 1920], [1200, 1800], [1368, 2048], [1464, 2048], [1536, 2048], [1608, 2048], [1640, 2048], [1704, 2048]], + "portrait": [[512, 768], [512, 680], [512, 1024], [680, 2048], [768, 1024], [768, 1344], [1024, 2048], [1080, 1920], [1200, 1800], [1368, 2048], [1464, 2048], [1536, 2048], [1608, 2048], [1640, 2048], [1704, 2048]], "landscape": [[768, 512], [1024, 512], [1024, 768], [1280, 720], [1344, 768], [1536, 512], [1800, 1200], [1920, 1080], [2048, 680], [2048, 1024], [2048, 1368], [2048, 1464], [2048, 1536], [2048, 1608], [2048, 1640], [2048, 1704]], }, "SDXL": { @@ -231,6 +231,7 @@ app.registerExtension({ resolutionWidget.options.values = resolutions; if (!resolutions.includes(resolutionWidget.value)) { + console.warn(`ResolutionSelector: saved resolution "${resolutionWidget.value}" is not valid for model ${modelWidget.value}; reset to ${resolutions[0]}`); resolutionWidget.value = resolutions[0]; } } diff --git a/nodes.py b/nodes.py index 8984bda..b1d4a0e 100755 --- a/nodes.py +++ b/nodes.py @@ -18,8 +18,8 @@ MODEL_RESOLUTIONS = { }, "Qwen Image": { "square": [(1024, 1024), (1080, 1080), (1280, 1280), (1328, 1328), (1536, 1536), (1920, 1920), (2048, 2048)], - "portrait": [(680, 2048), (928, 1664), (1024, 1536), (1024, 2048), (1056, 1584), (1080, 1920), (1104, 1472), (1140, 1472), (1152, 2048), (1200, 1800), (1368, 2048), (1464, 2048), (1536, 2048), (1608, 2048), (1640, 2048), (1704, 2048)], - "landscape": [(1280, 720), (1472, 1104), (1472, 1140), (1536, 1024), (1584, 1056), (1664, 928), (1800, 1200), (1920, 1080), (2048, 680), (2048, 1024), (2048, 1152), (2048, 1368), (2048, 1464), (2048, 1536), (2048, 1608), (2048, 1640), (2048, 1704)], + "portrait": [(680, 2048), (928, 1664), (1024, 1536), (1024, 2048), (1056, 1584), (1080, 1920), (1104, 1472), (1152, 2048), (1200, 1800), (1368, 2048), (1464, 2048), (1536, 2048), (1608, 2048), (1640, 2048), (1704, 2048)], + "landscape": [(1280, 720), (1472, 1104), (1536, 1024), (1584, 1056), (1664, 928), (1800, 1200), (1920, 1080), (2048, 680), (2048, 1024), (2048, 1152), (2048, 1368), (2048, 1464), (2048, 1536), (2048, 1608), (2048, 1640), (2048, 1704)], "constraints": {"divisible_by": 8, "min": 256, "max": 2048, "latent_channels": 16} }, "Z-Image": { @@ -30,7 +30,7 @@ MODEL_RESOLUTIONS = { }, "SD 1.5": { "square": [(512, 512), (768, 768), (1024, 1024), (1080, 1080), (1280, 1280), (1536, 1536)], - "portrait": [(512, 768), (512, 682), (512, 1024), (680, 2048), (768, 1024), (768, 1344), (1024, 2048), (1080, 1920), (1200, 1800), (1368, 2048), (1464, 2048), (1536, 2048), (1608, 2048), (1640, 2048), (1704, 2048)], + "portrait": [(512, 768), (512, 680), (512, 1024), (680, 2048), (768, 1024), (768, 1344), (1024, 2048), (1080, 1920), (1200, 1800), (1368, 2048), (1464, 2048), (1536, 2048), (1608, 2048), (1640, 2048), (1704, 2048)], "landscape": [(768, 512), (1024, 512), (1024, 768), (1280, 720), (1344, 768), (1536, 512), (1800, 1200), (1920, 1080), (2048, 680), (2048, 1024), (2048, 1368), (2048, 1464), (2048, 1536), (2048, 1608), (2048, 1640), (2048, 1704)], "constraints": {"divisible_by": 8, "min": 256, "max": 2048, "latent_channels": 4} }, @@ -377,6 +377,13 @@ class ResolutionSelector: # Parse preset resolution width, height = parse_resolution_string(resolution) + # The resolution combo lists every model's presets (the JS extension + # filters client-side), so API-built workflows can pair a model with a + # preset that violates its constraints. Validate before the multiplier: + # multiplied presets intentionally exceed the max constraint, and an + # integer multiplier preserves divisibility. + self._validate_dimensions(model, width, height) + # Apply multiplier to preset resolution width *= multiplier height *= multiplier diff --git a/pyproject.toml b/pyproject.toml index 5957eed..e995645 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_resolutionselectorplus" description = "Custom node for ComfyUI providing easy resolution and empty latent selection for image generation workflows." -version = "2.1.5" +version = "2.1.6" license = {file = "LICENSE"} [project.urls] diff --git a/test_resolution_selector.py b/test_resolution_selector.py index 970dac3..ab0de6a 100644 --- a/test_resolution_selector.py +++ b/test_resolution_selector.py @@ -140,6 +140,39 @@ def test_qwen_official_resolutions(): print(f" ✓ All {len(want)} resolutions present in Flux, Qwen Image, Z-Image") +def test_presets_satisfy_constraints(): + """Every preset must satisfy its own model's divisibility/min/max, or the + reported width/height will not match the generated latent dimensions.""" + print("\nTesting preset dimensions against model constraints:") + for model, data in MODEL_RESOLUTIONS.items(): + c = data["constraints"] + for category in ("square", "portrait", "landscape"): + for w, h in data[category]: + assert w % c["divisible_by"] == 0 and h % c["divisible_by"] == 0, \ + f"{model} {w}x{h} not divisible by {c['divisible_by']}" + assert c["min"] <= w <= c["max"] and c["min"] <= h <= c["max"], \ + f"{model} {w}x{h} outside {c['min']}-{c['max']}" + print(" ✓ All presets satisfy their model constraints") + + +def test_js_table_matches_python(): + """js/resolution_selector.js hand-mirrors MODEL_RESOLUTIONS; catch drift.""" + print("\nTesting JS resolution table matches Python:") + import json, os, re + js_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "js", "resolution_selector.js") + with open(js_path, encoding="utf-8") as f: + src = f.read() + match = re.search(r"const MODEL_RESOLUTIONS = (\{.*?\n\});", src, re.DOTALL) + assert match, "MODEL_RESOLUTIONS table not found in resolution_selector.js" + js_table = json.loads(re.sub(r",(\s*[}\]])", r"\1", match.group(1))) + py_table = {model: {cat: [list(p) for p in data[cat]] + for cat in ("square", "portrait", "landscape")} + for model, data in MODEL_RESOLUTIONS.items()} + assert js_table == py_table, "JS MODEL_RESOLUTIONS differs from nodes.py" + print(" ✓ JS table in sync with nodes.py") + + class TestResolutionSelector(unittest.TestCase): """unittest wrapper so `python -m unittest` discovers these tests (R5). @@ -171,6 +204,12 @@ class TestResolutionSelector(unittest.TestCase): def test_qwen_official_resolutions(self): test_qwen_official_resolutions() + def test_presets_satisfy_constraints(self): + test_presets_satisfy_constraints() + + def test_js_table_matches_python(self): + test_js_table_matches_python() + if __name__ == "__main__": print("=" * 60) @@ -186,6 +225,8 @@ if __name__ == "__main__": test_all_resolutions_unique() test_latent_channels() test_qwen_official_resolutions() + test_presets_satisfy_constraints() + test_js_table_matches_python() print("\n" + "=" * 60) print("✓ ALL TESTS PASSED!")