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