diff --git a/kikotools/tools/model_resolution_picker/logic.py b/kikotools/tools/model_resolution_picker/logic.py index 56469d7..fc33c41 100644 --- a/kikotools/tools/model_resolution_picker/logic.py +++ b/kikotools/tools/model_resolution_picker/logic.py @@ -5,6 +5,8 @@ import re from decimal import ROUND_HALF_UP, Decimal from typing import Optional +from .table import MAX_SIDE, MIN_SIDE + MEGAPIXEL = 1_048_576 _ALIASES = { @@ -47,7 +49,7 @@ def ratio_label(width: int, height: int) -> str: return _ALIASES.get(label, label) -_SIZE_LABEL = re.compile(r"^(\d+)×(\d+) - \S+ \(\d+\.\dMP\)$") +_SIZE_LABEL = re.compile(r"^(\d{1,5})×(\d{1,5}) - \S+ \(\d+\.\dMP\)$") def size_label(width: int, height: int, ratio_text: str) -> str: @@ -65,4 +67,5 @@ def parse_size_label(text: object) -> Optional[tuple[int, int]]: if match is None: return None width, height = int(match.group(1)), int(match.group(2)) - return (width, height) if width > 0 and height > 0 else None + in_range = MIN_SIDE <= width <= MAX_SIDE and MIN_SIDE <= height <= MAX_SIDE + return (width, height) if in_range else None diff --git a/kikotools/tools/model_resolution_picker/node.py b/kikotools/tools/model_resolution_picker/node.py index b61e6dc..dc8c864 100644 --- a/kikotools/tools/model_resolution_picker/node.py +++ b/kikotools/tools/model_resolution_picker/node.py @@ -15,6 +15,7 @@ from .families import ( resolve_family, ) from .logic import parse_size_label, round_to_step +from .table import MAX_SIDE, MIN_SIDE logger = logging.getLogger(__name__) @@ -27,7 +28,7 @@ class ModelResolutionPickerNode(ComfyAssetsBaseNode): @classmethod def INPUT_TYPES(cls) -> Dict[str, Any]: """Define the input types for the ComfyUI node.""" - size_options = {"default": 1024, "min": 64, "max": 8192, "step": 8} + size_options = {"default": 1024, "min": MIN_SIDE, "max": MAX_SIDE, "step": 8} return { "required": { "model": ( diff --git a/kikotools/tools/model_resolution_picker/table.py b/kikotools/tools/model_resolution_picker/table.py index 45e5b96..121f768 100644 --- a/kikotools/tools/model_resolution_picker/table.py +++ b/kikotools/tools/model_resolution_picker/table.py @@ -41,10 +41,12 @@ TABLE_VERSION = 1 GROUPS = ("image", "video") VAE_RATIOS = (8, 16, 32) CHIP_TOLERANCE = 0.07 -# Generous sanity bounds so absurd values are reported instead of overflowing. +# Sanity bounds so absurd values are reported instead of overflowing. MAX_STEP = 4096 MAX_MEGAPIXELS = 64 -MAX_SIDE = 65536 +# Match the node's width/height widget limits. +MIN_SIDE = 64 +MAX_SIDE = 8192 Size = Tuple[int, int] @@ -79,7 +81,7 @@ def _is_size(value: Any) -> bool: isinstance(value, (list, tuple)) and len(value) == 2 and all( - isinstance(v, int) and not isinstance(v, bool) and 0 < v <= MAX_SIDE + isinstance(v, int) and not isinstance(v, bool) and MIN_SIDE <= v <= MAX_SIDE for v in value ) ) @@ -258,7 +260,7 @@ def _validate_default_label(default: Any, entries: List[Dict[str, Any]]) -> List def validate_table(data: Any) -> List[str]: """Return every problem found in ``data``; an empty list means it is valid. - Bounds: step <= 4096, megapixels in (0, 64], each side <= 65536. + Bounds: step <= 4096, megapixels in (0, 64], each side in 64..8192. """ if not isinstance(data, dict): return ["table must be a JSON object"] diff --git a/tests/js/resolution-picker.test.mjs b/tests/js/resolution-picker.test.mjs index d50f75f..3c13e4e 100644 --- a/tests/js/resolution-picker.test.mjs +++ b/tests/js/resolution-picker.test.mjs @@ -89,3 +89,11 @@ test('modelOptions lists payload labels in order and keeps an absent current val assert.deepEqual(modelOptions(payload, undefined), ['A', 'B']); assert.deepEqual(modelOptions(null, 'Gone'), ['Gone']); }); + +test('parseSizeLabel rejects sizes outside the widget limits like the Python parser', () => { + assert.equal(parseSizeLabel('16384×16384 - 1:1 (256.0MP)'), null); + assert.equal(parseSizeLabel('32×32 - 1:1 (0.0MP)'), null); + assert.deepEqual(parseSizeLabel('64×8192 - 1:128 (0.5MP)'), [64, 8192]); + assert.equal(parseSizeLabel('8193×64 - 128:1 (0.5MP)'), null); + assert.equal(parseSizeLabel(`${'9'.repeat(5000)}×${'9'.repeat(5000)} - 1:1 (1.0MP)`), null); +}); diff --git a/tests/unit/tools/model_resolution_picker/test_logic.py b/tests/unit/tools/model_resolution_picker/test_logic.py index 03c7c04..4404f09 100644 --- a/tests/unit/tools/model_resolution_picker/test_logic.py +++ b/tests/unit/tools/model_resolution_picker/test_logic.py @@ -48,3 +48,12 @@ def test_parse_size_label_round_trips_and_rejects_garbage(): assert parse_size_label("garbage") is None assert parse_size_label("0×832 - 3:2 (1.0MP)") is None assert parse_size_label(None) is None + + +def test_parse_size_label_rejects_sizes_outside_the_widget_limits(): + assert parse_size_label("16384×16384 - 1:1 (256.0MP)") is None + assert parse_size_label("32×32 - 1:1 (0.0MP)") is None + assert parse_size_label("64×8192 - 1:128 (0.5MP)") == (64, 8192) + assert parse_size_label("8193×64 - 128:1 (0.5MP)") is None + huge = "9" * 5000 + "×" + "9" * 5000 + " - 1:1 (1.0MP)" + assert parse_size_label(huge) is None diff --git a/tests/unit/tools/model_resolution_picker/test_table.py b/tests/unit/tools/model_resolution_picker/test_table.py index 3f587a3..2cc12d3 100644 --- a/tests/unit/tools/model_resolution_picker/test_table.py +++ b/tests/unit/tools/model_resolution_picker/test_table.py @@ -233,3 +233,17 @@ def test_families_module_raises_runtime_error_on_invalid_shipped_file( families_module.load_shipped_families(path) assert "SD 1.5" in str(error.value) and "vaeRatio" in str(error.value) + + +@pytest.mark.parametrize("side", [63, 8200, 65536]) +def test_validate_table_rejects_sizes_outside_the_widget_limits(side: int): + data = shipped_data() + family(data, "SD 1.5")["sizes"]["1:1"].append([side, side]) + + assert any("sizes must be a list" in p for p in validate_table(data)) + + +def test_validate_table_accepts_sizes_at_the_widget_limits(): + from kikotools.tools.model_resolution_picker.table import MAX_SIDE, MIN_SIDE + + assert (MIN_SIDE, MAX_SIDE) == (64, 8192) diff --git a/web/ui/resolution-picker.js b/web/ui/resolution-picker.js index 388b5ba..75797e0 100644 --- a/web/ui/resolution-picker.js +++ b/web/ui/resolution-picker.js @@ -115,7 +115,7 @@ export function sizeOnModelChange(payload, newLabel, width, height, chip) { return defaultEntry(family, chip) || family.default; } -const SIZE_LABEL = /^(\d+)×(\d+) - \S+ \(\d+\.\dMP\)$/; +const SIZE_LABEL = /^(\d{1,5})×(\d{1,5}) - \S+ \(\d+\.\dMP\)$/; export function sizeLabel(width, height, ratioText) { return `${width}×${height} - ${ratioText} (${megapixels(width, height)}MP)`; @@ -125,7 +125,8 @@ export function parseSizeLabel(text) { const match = typeof text === 'string' ? SIZE_LABEL.exec(text) : null; if (!match) return null; const [width, height] = [Number(match[1]), Number(match[2])]; - return width > 0 && height > 0 ? [width, height] : null; + const inRange = (side) => side >= MIN_SIDE && side <= MAX_SIDE; + return inRange(width) && inRange(height) ? [width, height] : null; } export function dropdownOptions(payload, label, chip) {