fix(model_resolution_picker): bound sizes to the width/height widget limits

- table validation accepts sides 64..8192, matching the widget min/max
- parse_size_label (Python and JS) caps digits and rejects out-of-range
  sizes so create() falls back to width/height instead of overflowing
This commit is contained in:
Vito Sansevero
2026-10-27 12:49:10 -07:00
parent 623915fd7d
commit 47e95f95c3
7 changed files with 47 additions and 9 deletions
@@ -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
@@ -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": (
@@ -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"]
+8
View File
@@ -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);
});
@@ -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
@@ -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)
+3 -2
View File
@@ -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) {