feat: 8 resolution slots with per-slot seed + node outputs seed

- Resolution entries expanded from 6 to 8 fixed slots
- Each slot now stores [w, h, seed] (migrates old [w, h] entries to [w, h, 0])
- UI adds seed number input + casino randomize button per row
- ProjectResolution node now outputs (width, height, seed) instead of (width, height)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 11:27:11 +02:00
parent 062f7880a6
commit 55900e7c43
3 changed files with 70 additions and 36 deletions
+7 -6
View File
@@ -311,8 +311,8 @@ class ProjectResolution:
}, },
} }
RETURN_TYPES = ("INT", "INT") RETURN_TYPES = ("INT", "INT", "INT")
RETURN_NAMES = ("width", "height") RETURN_NAMES = ("width", "height", "seed")
FUNCTION = "fetch_resolution" FUNCTION = "fetch_resolution"
CATEGORY = "JSON Manager/project" CATEGORY = "JSON Manager/project"
OUTPUT_NODE = False OUTPUT_NODE = False
@@ -332,20 +332,21 @@ class ProjectResolution:
data = _fetch_data(manager_url, project_name, file_name, sequence_number) data = _fetch_data(manager_url, project_name, file_name, sequence_number)
if data.get("error") in ("http_error", "network_error", "parse_error"): if data.get("error") in ("http_error", "network_error", "parse_error"):
logger.warning("ProjectResolution.fetch_resolution failed: %s", data.get("message")) logger.warning("ProjectResolution.fetch_resolution failed: %s", data.get("message"))
return (512, 512) return (512, 512, 0)
series = data.get(key_name) series = data.get(key_name)
if not isinstance(series, list) or len(series) == 0: if not isinstance(series, list) or len(series) == 0:
logger.warning("ProjectResolution: key '%s' is not a resolution series", key_name) logger.warning("ProjectResolution: key '%s' is not a resolution series", key_name)
return (512, 512) return (512, 512, 0)
clamped = max(0, min(index, len(series) - 1)) clamped = max(0, min(index, len(series) - 1))
entry = series[clamped] entry = series[clamped]
if not isinstance(entry, (list, tuple)) or len(entry) < 2: if not isinstance(entry, (list, tuple)) or len(entry) < 2:
logger.warning("ProjectResolution: entry at index %d is malformed: %r", clamped, entry) logger.warning("ProjectResolution: entry at index %d is malformed: %r", clamped, entry)
return (512, 512) return (512, 512, 0)
return (to_int(entry[0]), to_int(entry[1])) seed = to_int(entry[2]) if len(entry) >= 3 else 0
return (to_int(entry[0]), to_int(entry[1]), seed)
# --- Mappings --- # --- Mappings ---
+37 -17
View File
@@ -553,34 +553,54 @@ def _render_sequence_card(i, seq, batch_list, data, file_path, state,
dict_textarea('Specific Negative', seq, 'negative').classes( dict_textarea('Specific Negative', seq, 'negative').classes(
'w-full q-mt-sm').props('outlined rows=2') 'w-full q-mt-sm').props('outlined rows=2')
# --- Resolutions (6 fixed slots) --- # --- Resolutions (8 fixed slots) ---
ui.label('Resolutions').classes('text-caption text-weight-bold q-mt-md') ui.label('Resolutions').classes('text-caption text-weight-bold q-mt-md')
if 'resolutions' not in seq or len(seq.get('resolutions', [])) < 6:
resolutions = seq.setdefault('resolutions', []) resolutions = seq.setdefault('resolutions', [])
while len(resolutions) < 6: changed = False
resolutions.append([512, 512]) while len(resolutions) < 8:
resolutions.append([512, 512, 0])
changed = True
# Migrate old [w, h] entries to [w, h, seed]
for i, entry in enumerate(resolutions):
if len(entry) < 3:
resolutions[i] = list(entry) + [0]
changed = True
if changed:
commit() commit()
resolutions = seq['resolutions'] for idx in range(8):
for idx in range(6):
entry = resolutions[idx] entry = resolutions[idx]
with ui.row().classes('items-center w-full q-mt-xs'): with ui.row().classes('items-center w-full q-mt-xs no-wrap'):
ui.label(str(idx)).classes('text-caption').style('min-width:20px') ui.label(str(idx)).classes('text-caption').style('min-width:16px')
w_inp = ui.number(value=int(entry[0]), min=1, step=1, label='W').classes( w_inp = ui.number(value=int(entry[0]), min=1, step=1, label='W').style(
'col').props('outlined dense hide-bottom-space') 'width:70px').props('outlined dense hide-bottom-space')
h_inp = ui.number(value=int(entry[1]), min=1, step=1, label='H').classes( h_inp = ui.number(value=int(entry[1]), min=1, step=1, label='H').style(
'col').props('outlined dense hide-bottom-space') 'width:70px').props('outlined dense hide-bottom-space')
seed_inp = ui.number(value=int(entry[2]), min=0, step=1, label='Seed').style(
'flex:1; min-width:60px').props('outlined dense hide-bottom-space')
def _sync_wh(i=idx, wi=w_inp, hi=h_inp): def _sync_entry(i=idx, wi=w_inp, hi=h_inp, si=seed_inp):
seq['resolutions'][i] = [ seq['resolutions'][i] = [
int(wi.value) if wi.value else 512, int(wi.value) if wi.value else 512,
int(hi.value) if hi.value else 512, int(hi.value) if hi.value else 512,
int(si.value) if si.value else 0,
] ]
commit() commit()
w_inp.on('blur', lambda _, s=_sync_wh: s()) def _randomize(si=seed_inp, i=idx):
w_inp.on('update:model-value', lambda _, s=_sync_wh: s()) import random
h_inp.on('blur', lambda _, s=_sync_wh: s()) si.value = random.randint(0, 2**32 - 1)
h_inp.on('update:model-value', lambda _, s=_sync_wh: s()) seq['resolutions'][i][2] = int(si.value)
commit()
ui.button(icon='casino', on_click=_randomize).props(
'flat dense round').classes('q-ml-xs')
w_inp.on('blur', lambda _, s=_sync_entry: s())
w_inp.on('update:model-value', lambda _, s=_sync_entry: s())
h_inp.on('blur', lambda _, s=_sync_entry: s())
h_inp.on('update:model-value', lambda _, s=_sync_entry: s())
seed_inp.on('blur', lambda _, s=_sync_entry: s())
seed_inp.on('update:model-value', lambda _, s=_sync_entry: s())
with splitter.after: with splitter.after:
# Mode # Mode
+25 -12
View File
@@ -353,46 +353,59 @@ class TestProjectResolution:
assert "index" in inputs["required"] assert "index" in inputs["required"]
assert inputs["required"]["index"][0] == "INT" assert inputs["required"]["index"][0] == "INT"
def test_two_outputs(self): def test_three_outputs(self):
from project_loader import ProjectResolution from project_loader import ProjectResolution
assert ProjectResolution.RETURN_TYPES == ("INT", "INT") assert ProjectResolution.RETURN_TYPES == ("INT", "INT", "INT")
assert ProjectResolution.RETURN_NAMES == ("width", "height") assert ProjectResolution.RETURN_NAMES == ("width", "height", "seed")
def test_fetch_resolution_basic(self): def test_fetch_resolution_basic(self):
from project_loader import ProjectResolution from project_loader import ProjectResolution
node = ProjectResolution() node = ProjectResolution()
data = {"resolutions": [[512, 512], [768, 1344], [1344, 768]]} data = {"resolutions": [[512, 512, 0], [768, 1344, 12345], [1344, 768, 99]]}
with patch("project_loader._fetch_data", return_value=data): with patch("project_loader._fetch_data", return_value=data):
result = node.fetch_resolution( result = node.fetch_resolution(
source_label="src", key_name="resolutions", index=1, source_label="src", key_name="resolutions", index=1,
manager_url="http://localhost:8080", project_name="p", manager_url="http://localhost:8080", project_name="p",
file_name="f", sequence_number=1, file_name="f", sequence_number=1,
) )
assert result == (768, 1344) assert result == (768, 1344, 12345)
def test_fetch_resolution_index_zero(self): def test_fetch_resolution_index_zero(self):
from project_loader import ProjectResolution from project_loader import ProjectResolution
node = ProjectResolution() node = ProjectResolution()
data = {"resolutions": [[512, 512], [1024, 1024]]} data = {"resolutions": [[512, 512, 42], [1024, 1024, 0]]}
with patch("project_loader._fetch_data", return_value=data): with patch("project_loader._fetch_data", return_value=data):
result = node.fetch_resolution( result = node.fetch_resolution(
source_label="src", key_name="resolutions", index=0, source_label="src", key_name="resolutions", index=0,
manager_url="http://localhost:8080", project_name="p", manager_url="http://localhost:8080", project_name="p",
file_name="f", sequence_number=1, file_name="f", sequence_number=1,
) )
assert result == (512, 512) assert result == (512, 512, 42)
def test_fetch_resolution_clamps_on_out_of_bounds(self): def test_fetch_resolution_clamps_on_out_of_bounds(self):
from project_loader import ProjectResolution from project_loader import ProjectResolution
node = ProjectResolution() node = ProjectResolution()
data = {"resolutions": [[512, 512], [1024, 1024]]} data = {"resolutions": [[512, 512, 0], [1024, 1024, 7]]}
with patch("project_loader._fetch_data", return_value=data): with patch("project_loader._fetch_data", return_value=data):
result = node.fetch_resolution( result = node.fetch_resolution(
source_label="src", key_name="resolutions", index=99, source_label="src", key_name="resolutions", index=99,
manager_url="http://localhost:8080", project_name="p", manager_url="http://localhost:8080", project_name="p",
file_name="f", sequence_number=1, file_name="f", sequence_number=1,
) )
assert result == (1024, 1024) # last entry assert result == (1024, 1024, 7) # last entry
def test_fetch_resolution_old_format_no_seed(self):
"""Old [w, h] entries without seed should return seed=0."""
from project_loader import ProjectResolution
node = ProjectResolution()
data = {"resolutions": [[576, 384], [960, 640]]}
with patch("project_loader._fetch_data", return_value=data):
result = node.fetch_resolution(
source_label="src", key_name="resolutions", index=0,
manager_url="http://localhost:8080", project_name="p",
file_name="f", sequence_number=1,
)
assert result == (576, 384, 0)
def test_fetch_resolution_missing_key_returns_defaults(self): def test_fetch_resolution_missing_key_returns_defaults(self):
from project_loader import ProjectResolution from project_loader import ProjectResolution
@@ -403,7 +416,7 @@ class TestProjectResolution:
manager_url="http://localhost:8080", project_name="p", manager_url="http://localhost:8080", project_name="p",
file_name="f", sequence_number=1, file_name="f", sequence_number=1,
) )
assert result == (512, 512) assert result == (512, 512, 0)
def test_fetch_resolution_network_error_returns_defaults(self): def test_fetch_resolution_network_error_returns_defaults(self):
from project_loader import ProjectResolution from project_loader import ProjectResolution
@@ -415,7 +428,7 @@ class TestProjectResolution:
manager_url="http://localhost:8080", project_name="p", manager_url="http://localhost:8080", project_name="p",
file_name="f", sequence_number=1, file_name="f", sequence_number=1,
) )
assert result == (512, 512) assert result == (512, 512, 0)
def test_fetch_resolution_malformed_entry_returns_defaults(self): def test_fetch_resolution_malformed_entry_returns_defaults(self):
from project_loader import ProjectResolution from project_loader import ProjectResolution
@@ -427,7 +440,7 @@ class TestProjectResolution:
manager_url="http://localhost:8080", project_name="p", manager_url="http://localhost:8080", project_name="p",
file_name="f", sequence_number=1, file_name="f", sequence_number=1,
) )
assert result == (512, 512) assert result == (512, 512, 0)
def test_category(self): def test_category(self):
from project_loader import ProjectResolution from project_loader import ProjectResolution