Defaults an administrator can actually set
There were none. `workflow.DEFAULTS` was the only source, so 512x512, euler and twenty steps were what every instance got whatever card it was running on -- and 512 square on an SDXL checkpoint is precisely what the tool's own description warns produces duplicated limbs. The two ways round it were both bad: bake literals into a template where the placeholders should be, or write prose in the instructions box and hope. Three rungs now, most specific winning, with DEFAULTS staying underneath as the floor so an instance that sets nothing behaves exactly as it did and a floor improved in code still reaches everybody. An empty box is "no opinion" rather than zero, which matters: read as a number it would set every instance to zero steps, and ComfyUI refuses that in a way that looks like a broken model. The right control for each, because a text box is wrong for most of them. The samplers and schedulers were already being discovered by the Test button, stored, and read by nothing at all -- they are the pickers now. A stored value missing from the list is kept as an option anyway, or opening this page and pressing Save would silently clear a working setting. Checkpoints are chosen rather than typed, and the instance default is a rung of its own instead of "whatever happens to be first in a textarea somebody filled in some order". And batch, at last: `batch_size` was a literal 1 in the base template, so an administrator whose card can comfortably make four had no way of saying so. Deliberately not something a model may set -- one asking for six because it is unsure is the exact cost this must not invite. The tool's schema restates the defaults it quotes. Every "Default 20." in there was written when there was one set of defaults in the world; left alone, an instance drawing at 1024 would go on telling the model 512, and the model reasons from that sentence rather than ignoring it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,12 +40,25 @@ PLACEHOLDERS = (
|
||||
"sampler",
|
||||
"scheduler",
|
||||
"denoise",
|
||||
# How many pictures one run produces. Late to the list, and the reason is
|
||||
# worth stating: `batch_size` was a literal `1` in the base template, so an
|
||||
# administrator whose card can comfortably make four at a time had no way of
|
||||
# saying so short of editing the JSON. Not a tool parameter -- a model asking
|
||||
# for six images because it is unsure is exactly the cost this should not
|
||||
# invite -- so it fills from the instance default and nowhere else.
|
||||
"batch",
|
||||
)
|
||||
|
||||
# The defaults, taken from the base template. `seed` is deliberately absent: it
|
||||
# has no fixed default, because one would make every generation that did not
|
||||
# name a seed identical -- and would make the retry loop produce the same
|
||||
# rejected image four times over.
|
||||
# What a model may name. Everything else in `PLACEHOLDERS` fills from a default.
|
||||
MODEL_SETTABLE = tuple(name for name in PLACEHOLDERS if name != "batch")
|
||||
|
||||
# The floor, taken from the base template. An instance's own defaults sit above
|
||||
# this (see `resolve`), and this stays as the last resort so a fresh install
|
||||
# behaves exactly as it always did.
|
||||
#
|
||||
# `seed` is deliberately absent: it has no fixed default, because one would make
|
||||
# every generation that did not name a seed identical -- and would make the
|
||||
# retry loop produce the same rejected image four times over.
|
||||
DEFAULTS: dict[str, Any] = {
|
||||
"negative": "text, watermark",
|
||||
"steps": 20,
|
||||
@@ -55,6 +68,26 @@ DEFAULTS: dict[str, Any] = {
|
||||
"sampler": "euler",
|
||||
"scheduler": "normal",
|
||||
"denoise": 1.0,
|
||||
"batch": 1,
|
||||
}
|
||||
|
||||
# What each hole is for, and what it lands as. Read by the workflow editor, so
|
||||
# somebody writing a template is told what `{{sampler}}` fills without reading
|
||||
# this file -- and in particular is told the two names that do not match
|
||||
# ComfyUI's own, which is the mistake that costs an afternoon.
|
||||
DESCRIPTIONS: dict[str, tuple[str, str]] = {
|
||||
"model": ("text", "The checkpoint. Fills ComfyUI's `ckpt_name`, not `model`."),
|
||||
"prompt": ("text", "What to draw. The only value a model must supply."),
|
||||
"negative": ("text", "What to keep out of the picture."),
|
||||
"seed": ("number", "The noise seed. Absent or negative means a fresh random one."),
|
||||
"steps": ("number", "How many denoising steps. More is slower, not always better."),
|
||||
"cfg": ("number", "How closely to follow the prompt. A decimal."),
|
||||
"width": ("number", "Pixels across. A multiple of 64."),
|
||||
"height": ("number", "Pixels down. A multiple of 64."),
|
||||
"sampler": ("text", "The sampling method. Fills ComfyUI's `sampler_name`, not `sampler`."),
|
||||
"scheduler": ("text", "The noise schedule."),
|
||||
"denoise": ("number", "How much of the latent to redraw. 1.0 for text-to-image."),
|
||||
"batch": ("number", "How many images one run makes. Fills `batch_size`."),
|
||||
}
|
||||
|
||||
# ComfyUI's own ranges, read off `/object_info`. Clamped rather than refused: a
|
||||
@@ -66,6 +99,10 @@ LIMITS: dict[str, tuple[float, float]] = {
|
||||
"width": (64, 2048),
|
||||
"height": (64, 2048),
|
||||
"denoise": (0.0, 1.0),
|
||||
# Not ComfyUI's ceiling, which is 4096, but a sane one: this multiplies
|
||||
# every generation's time and VRAM, and an administrator who wants more than
|
||||
# eight at once wants a different workflow rather than a bigger number here.
|
||||
"batch": (1, 8),
|
||||
}
|
||||
|
||||
# ComfyUI's seed is a uint64. Generated here rather than left to the far side
|
||||
@@ -80,12 +117,43 @@ def random_seed() -> int:
|
||||
return secrets.randbelow(MAX_SEED)
|
||||
|
||||
|
||||
def resolve(given: dict[str, Any]) -> dict[str, Any]:
|
||||
"""The full parameter set: what was asked for, over the defaults.
|
||||
def instance_defaults(values: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""The `default_*` keys out of the image settings, as placeholder names.
|
||||
|
||||
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.
|
||||
Only the ones actually set: an absent or empty key means "no opinion", and
|
||||
must fall through to `DEFAULTS` rather than land as an empty string in a
|
||||
workflow. That is the same reading `resolve` gives a model's own arguments,
|
||||
and it is why an administrator can set two of these and leave the rest.
|
||||
"""
|
||||
out: dict[str, Any] = {}
|
||||
for name in PLACEHOLDERS:
|
||||
if name in ("prompt", "seed", "model"):
|
||||
# A default prompt is not a thing; a default seed would make every
|
||||
# picture identical; the checkpoint has its own setting and its own
|
||||
# per-chat override, resolved before this is reached.
|
||||
continue
|
||||
value = (values or {}).get(f"default_{name}")
|
||||
if value is None or value == "":
|
||||
continue
|
||||
out[name] = value
|
||||
return out
|
||||
|
||||
|
||||
def resolve(given: dict[str, Any], *, settings: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""The full parameter set: what was asked for, over what this instance
|
||||
prefers, over the built-in floor.
|
||||
|
||||
Three rungs, most specific winning, and the middle one is the new part. For
|
||||
the whole life of this feature there were only two -- so 512x512, euler and
|
||||
twenty steps were the values every instance got, whatever card it was
|
||||
running on, and the only ways to move them were to bake literals into a
|
||||
template instead of placeholders or to write prose in the instructions box
|
||||
and hope. `DEFAULTS` stays underneath so an instance that sets nothing
|
||||
behaves exactly as it did.
|
||||
|
||||
Absent and null are both "no opinion", at both levels. 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
|
||||
@@ -94,14 +162,17 @@ def resolve(given: dict[str, Any]) -> dict[str, Any]:
|
||||
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, **instance_defaults(settings)}
|
||||
for name, value in (given or {}).items():
|
||||
if name in PLACEHOLDERS and value is not None and value != "":
|
||||
# `batch` is absent from `MODEL_SETTABLE`, so a model naming it is
|
||||
# ignored here rather than refused -- the tool schema never offered it,
|
||||
# and one that invents the key has guessed rather than misbehaved.
|
||||
if name in MODEL_SETTABLE and value is not None and value != "":
|
||||
values[name] = value
|
||||
|
||||
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", "batch"):
|
||||
values[name] = _clamp(_whole(values.get(name), DEFAULTS[name]), name)
|
||||
for name in ("cfg", "denoise"):
|
||||
values[name] = _clamp(_decimal(values.get(name), DEFAULTS[name]), name)
|
||||
|
||||
Reference in New Issue
Block a user