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:
@@ -18,6 +18,7 @@ import json
|
||||
import logging
|
||||
import re
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Form, HTTPException, Request, Response, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
@@ -54,6 +55,27 @@ def _lines(text: str) -> list[str]:
|
||||
return seen
|
||||
|
||||
|
||||
def _number(raw: str, name: str, *, whole: bool = True) -> Any:
|
||||
"""A filled box as a clamped number, an empty one as "".
|
||||
|
||||
The empty string is load-bearing and is not a missing value: it is how an
|
||||
administrator says "no opinion about this one", which `workflow.resolve`
|
||||
reads as "fall through to the built-in floor". Turning it into a zero here
|
||||
would silently set every instance to zero steps.
|
||||
"""
|
||||
text = (raw or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
try:
|
||||
value = float(text)
|
||||
except ValueError:
|
||||
return ""
|
||||
low, high = workflow_service.LIMITS.get(name, (None, None))
|
||||
if low is not None:
|
||||
value = min(max(value, low), high)
|
||||
return int(value) if whole else value
|
||||
|
||||
|
||||
def _config(db: Db) -> comfy.Config:
|
||||
values = settings_store.images(db)
|
||||
return comfy.Config(
|
||||
@@ -114,6 +136,21 @@ async def save_images(
|
||||
max_tries: int = Form(4),
|
||||
preserve_vram: bool = Form(False),
|
||||
instructions: str = Form(""),
|
||||
# The generation defaults. Every one is a *string* even where it is a
|
||||
# number, because "" is how an administrator says "no opinion" and an
|
||||
# `int = Form(0)` cannot express that -- zero steps is a value, and one
|
||||
# somebody could mean. `_number` below turns a filled box into a clamped
|
||||
# number and an empty one back into "".
|
||||
default_checkpoint: str = Form(""),
|
||||
default_steps: str = Form(""),
|
||||
default_cfg: str = Form(""),
|
||||
default_width: str = Form(""),
|
||||
default_height: str = Form(""),
|
||||
default_sampler: str = Form(""),
|
||||
default_scheduler: str = Form(""),
|
||||
default_denoise: str = Form(""),
|
||||
default_negative: str = Form(""),
|
||||
default_batch: str = Form(""),
|
||||
) -> Response:
|
||||
"""Save the settings.
|
||||
|
||||
@@ -142,6 +179,20 @@ async def save_images(
|
||||
"max_tries": min(max(max_tries, 1), 10),
|
||||
"preserve_vram": preserve_vram,
|
||||
"instructions": instructions.strip()[:4000],
|
||||
# Clamped here to the same bounds `workflow.LIMITS` uses on the way
|
||||
# out. Twice, deliberately: a number stored by an earlier version,
|
||||
# or written straight into the settings row, still has to be safe
|
||||
# when a generation reads it.
|
||||
"default_checkpoint": default_checkpoint.strip(),
|
||||
"default_steps": _number(default_steps, "steps"),
|
||||
"default_cfg": _number(default_cfg, "cfg", whole=False),
|
||||
"default_width": _number(default_width, "width"),
|
||||
"default_height": _number(default_height, "height"),
|
||||
"default_sampler": default_sampler.strip(),
|
||||
"default_scheduler": default_scheduler.strip(),
|
||||
"default_denoise": _number(default_denoise, "denoise", whole=False),
|
||||
"default_negative": default_negative.strip()[:500],
|
||||
"default_batch": _number(default_batch, "batch"),
|
||||
},
|
||||
key=settings_store.IMAGES,
|
||||
)
|
||||
@@ -212,7 +263,34 @@ def _workflow(db: Db, workflow_id: str) -> ImageWorkflow:
|
||||
return row
|
||||
|
||||
|
||||
def _detail(request: Request, row: ImageWorkflow, *, is_new: bool, error: str = "", **extra):
|
||||
def _placeholder_help(db: Db) -> list[tuple[str, str, str, str]]:
|
||||
"""Every placeholder, what it fills, and what it resolves to *today*.
|
||||
|
||||
The last column is the point. A legend listing names answers "what may I
|
||||
write"; the question somebody actually has, standing in front of a workflow
|
||||
that came out wrong, is "what happens if I leave this out" -- and the answer
|
||||
moved the day instance defaults arrived. Resolved through the same call a
|
||||
generation makes, so the two cannot disagree.
|
||||
"""
|
||||
resolved = workflow_service.resolve({}, settings=settings_store.images(db))
|
||||
out: list[tuple[str, str, str, str]] = []
|
||||
for name in workflow_service.PLACEHOLDERS:
|
||||
kind, what = workflow_service.DESCRIPTIONS.get(name, ("text", ""))
|
||||
if name == "prompt":
|
||||
shown = "whatever is asked for"
|
||||
elif name == "seed":
|
||||
shown = "a fresh random one"
|
||||
elif name == "model":
|
||||
shown = str(resolved.get("model") or "") or "the first checkpoint listed"
|
||||
else:
|
||||
shown = str(resolved.get(name, ""))
|
||||
out.append((name, kind, what, shown))
|
||||
return out
|
||||
|
||||
|
||||
def _detail(
|
||||
request: Request, db: Db, row: ImageWorkflow, *, is_new: bool, error: str = "", **extra
|
||||
):
|
||||
return render(
|
||||
request,
|
||||
"admin/workflow_detail.html",
|
||||
@@ -221,6 +299,7 @@ def _detail(request: Request, row: ImageWorkflow, *, is_new: bool, error: str =
|
||||
"is_new": is_new,
|
||||
"error": error,
|
||||
"placeholders": workflow_service.PLACEHOLDERS,
|
||||
"placeholder_help": _placeholder_help(db),
|
||||
"workflow_text": extra.pop(
|
||||
"workflow_text", json.dumps(row.workflow_json or {}, indent=2)
|
||||
),
|
||||
@@ -304,7 +383,7 @@ async def new_workflow(request: Request, db: Db, user: AdminUser) -> Response:
|
||||
workflow_json=json.loads(base.read_text(encoding="utf-8")),
|
||||
enabled=True,
|
||||
)
|
||||
return _detail(request, draft, is_new=True)
|
||||
return _detail(request, db, draft, is_new=True)
|
||||
|
||||
|
||||
@router.post("/workflows")
|
||||
@@ -316,6 +395,7 @@ async def create_workflow(request: Request, db: Db, user: AdminUser) -> Response
|
||||
if problem:
|
||||
return _detail(
|
||||
request,
|
||||
db,
|
||||
row,
|
||||
is_new=True,
|
||||
error=problem,
|
||||
@@ -334,7 +414,7 @@ async def create_workflow(request: Request, db: Db, user: AdminUser) -> Response
|
||||
|
||||
@router.get("/workflows/{workflow_id}/edit")
|
||||
async def edit_workflow(request: Request, db: Db, user: AdminUser, workflow_id: str) -> Response:
|
||||
return _detail(request, _workflow(db, workflow_id), is_new=False)
|
||||
return _detail(request, db, _workflow(db, workflow_id), is_new=False)
|
||||
|
||||
|
||||
@router.post("/workflows/{workflow_id}/delete")
|
||||
@@ -363,6 +443,7 @@ async def update_workflow(request: Request, db: Db, user: AdminUser, workflow_id
|
||||
draft.id = row.id
|
||||
return _detail(
|
||||
request,
|
||||
db,
|
||||
draft,
|
||||
is_new=False,
|
||||
error=problem,
|
||||
|
||||
Reference in New Issue
Block a user