Fix invalid presets, validate preset path, sync JS resolution table (review 1, 2)
Qwen Image 1140x1472 (and mirror) and SD 1.5 512x682 were not divisible by 8, so the generated latent did not match the reported width/height; removed the stray Qwen pair (official list has 1104x1472) and corrected 682 to 680. The preset branch now runs _validate_dimensions before the multiplier, so API-built workflows cannot pair a model with another model's incompatible preset. Regenerated the hand-copied JS table from nodes.py (it had drifted by 14 entries) and added tests that fail on future drift or non-conforming presets. Bump to 2.1.6.
This commit is contained in:
+12
-11
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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]
|
||||
|
||||
@@ -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!")
|
||||
|
||||
Reference in New Issue
Block a user