A seed of -1 means random, as it does everywhere else
Omitting the seed was already random. Passing -1 was not: it went through the uint64 wrap and arrived as 18446744073709551615, which is a perfectly valid *fixed* seed -- so "give me something new" returned the identical picture every time, silently, and the retry loop would have redrawn the same rejected image until it ran out of attempts. -1 is what ComfyUI's own interface uses for random, and A1111, and everything else that has ever asked somebody for a seed. A model that has read any of them will write it, so the one reading that had to work was the one that did not. Any negative value, not only -1, because the sentinel is the *idea* rather than the number and a model that writes -2 means the same thing. Zero stays a real seed: it is the boundary this change could easily have swallowed, and it is one somebody deliberately picks. Confirmed against the real ComfyUI: -1 now sends a random uint64 that it accepts and draws from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
"""LLeMbas - a Middle-earth themed web UI for OpenAI-compatible LLM endpoints."""
|
"""LLeMbas - a Middle-earth themed web UI for OpenAI-compatible LLM endpoints."""
|
||||||
|
|
||||||
__version__ = "0.7.0"
|
__version__ = "0.7.1"
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ SCHEMA: dict[str, Any] = {
|
|||||||
},
|
},
|
||||||
"seed": {
|
"seed": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Omit for a new random image; repeat one to get the same image again.",
|
"description": (
|
||||||
|
"Omit it, or pass -1, for a new random image. Repeat a seed you were "
|
||||||
|
"told about to get the same image again."
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"steps": {"type": "integer", "description": "Sampling steps. Default 20."},
|
"steps": {"type": "integer", "description": "Sampling steps. Default 20."},
|
||||||
"cfg": {"type": "number", "description": "Prompt adherence. Default 8."},
|
"cfg": {"type": "number", "description": "Prompt adherence. Default 8."},
|
||||||
|
|||||||
@@ -86,13 +86,21 @@ def resolve(given: dict[str, Any]) -> dict[str, Any]:
|
|||||||
Absent and null are both "no opinion". A model that emits `"seed": null`
|
Absent and null are both "no opinion". A model that emits `"seed": null`
|
||||||
rather than omitting the key is common enough that treating it as a request
|
rather than omitting the key is common enough that treating it as a request
|
||||||
for seed zero would be a bug nobody could see.
|
for seed zero would be a bug nobody could see.
|
||||||
|
|
||||||
|
**A negative seed means random**, which is what `-1` means in ComfyUI's own
|
||||||
|
interface, in A1111, and in every other thing that has ever asked somebody
|
||||||
|
for a seed. A model that has read any of them will write it, and without
|
||||||
|
this it went through the uint64 wrap and came out as 18446744073709551615 --
|
||||||
|
a perfectly valid *fixed* seed, so "give me something new" produced the same
|
||||||
|
picture every time. Exactly the wrong answer, arrived at silently.
|
||||||
"""
|
"""
|
||||||
values: dict[str, Any] = {**DEFAULTS}
|
values: dict[str, Any] = {**DEFAULTS}
|
||||||
for name, value in (given or {}).items():
|
for name, value in (given or {}).items():
|
||||||
if name in PLACEHOLDERS and value is not None and value != "":
|
if name in PLACEHOLDERS and value is not None and value != "":
|
||||||
values[name] = value
|
values[name] = value
|
||||||
|
|
||||||
values["seed"] = _whole(values.get("seed"), default=random_seed()) % (MAX_SEED + 1)
|
seed = _whole(values.get("seed"), default=-1)
|
||||||
|
values["seed"] = random_seed() if seed < 0 else seed % (MAX_SEED + 1)
|
||||||
for name in ("steps", "width", "height"):
|
for name in ("steps", "width", "height"):
|
||||||
values[name] = _clamp(_whole(values.get(name), DEFAULTS[name]), name)
|
values[name] = _clamp(_whole(values.get(name), DEFAULTS[name]), name)
|
||||||
for name in ("cfg", "denoise"):
|
for name in ("cfg", "denoise"):
|
||||||
|
|||||||
@@ -96,6 +96,27 @@ def test_a_named_seed_is_kept():
|
|||||||
assert wf.resolve({"prompt": "x", "seed": 1234})["seed"] == 1234
|
assert wf.resolve({"prompt": "x", "seed": 1234})["seed"] == 1234
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("sentinel", [-1, -5, "-1"])
|
||||||
|
def test_a_negative_seed_means_random(sentinel):
|
||||||
|
"""`-1` is what ComfyUI's own interface, A1111 and everything else that has
|
||||||
|
ever asked for a seed use for "surprise me", so a model that has read any of
|
||||||
|
them will write it.
|
||||||
|
|
||||||
|
Without this it went through the uint64 wrap and came out as
|
||||||
|
18446744073709551615 -- a perfectly valid *fixed* seed, so asking for
|
||||||
|
something new produced the same picture every time.
|
||||||
|
"""
|
||||||
|
seeds = {wf.resolve({"prompt": "x", "seed": sentinel})["seed"] for _ in range(8)}
|
||||||
|
assert len(seeds) == 8
|
||||||
|
assert all(0 <= seed <= wf.MAX_SEED for seed in seeds)
|
||||||
|
|
||||||
|
|
||||||
|
def test_seed_zero_is_a_real_seed():
|
||||||
|
"""It is the boundary the negative test could easily have swallowed, and
|
||||||
|
zero is a seed somebody deliberately picks."""
|
||||||
|
assert wf.resolve({"prompt": "x", "seed": 0})["seed"] == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("empty", [None, ""])
|
@pytest.mark.parametrize("empty", [None, ""])
|
||||||
def test_null_and_empty_mean_no_opinion(empty):
|
def test_null_and_empty_mean_no_opinion(empty):
|
||||||
"""A model that emits `"steps": null` rather than omitting the key is common
|
"""A model that emits `"steps": null` rather than omitting the key is common
|
||||||
|
|||||||
Reference in New Issue
Block a user