From b2a05e0351c12c98502aa3ea367ad02d7387d7db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Bene=C5=A1?= Date: Wed, 5 Aug 2026 14:23:38 +0200 Subject: [PATCH] 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) --- src/lembas/__init__.py | 2 +- src/lembas/services/images/tool.py | 5 ++++- src/lembas/services/images/workflow.py | 10 +++++++++- tests/test_images_workflow.py | 21 +++++++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/lembas/__init__.py b/src/lembas/__init__.py index b2fe638..d130a77 100644 --- a/src/lembas/__init__.py +++ b/src/lembas/__init__.py @@ -1,3 +1,3 @@ """LLeMbas - a Middle-earth themed web UI for OpenAI-compatible LLM endpoints.""" -__version__ = "0.7.0" +__version__ = "0.7.1" diff --git a/src/lembas/services/images/tool.py b/src/lembas/services/images/tool.py index 59f0c92..89a961f 100644 --- a/src/lembas/services/images/tool.py +++ b/src/lembas/services/images/tool.py @@ -75,7 +75,10 @@ SCHEMA: dict[str, Any] = { }, "seed": { "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."}, "cfg": {"type": "number", "description": "Prompt adherence. Default 8."}, diff --git a/src/lembas/services/images/workflow.py b/src/lembas/services/images/workflow.py index 9f8cb89..3b47ecc 100644 --- a/src/lembas/services/images/workflow.py +++ b/src/lembas/services/images/workflow.py @@ -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` rather than omitting the key is common enough that treating it as a request 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} for name, value in (given or {}).items(): if name in PLACEHOLDERS and value is not None and 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"): values[name] = _clamp(_whole(values.get(name), DEFAULTS[name]), name) for name in ("cfg", "denoise"): diff --git a/tests/test_images_workflow.py b/tests/test_images_workflow.py index ed83efa..ac1d44e 100644 --- a/tests/test_images_workflow.py +++ b/tests/test_images_workflow.py @@ -96,6 +96,27 @@ def test_a_named_seed_is_kept(): 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, ""]) def test_null_and_empty_mean_no_opinion(empty): """A model that emits `"steps": null` rather than omitting the key is common