Run the templates on FLUX, and name the loaded style

The root templates ran on SDXL base, where every gallery style washed out. A
full compiled style is ~480 tokens and SDXL's CLIP attends to ~77 per chunk, so
the style was truncated before the sampler saw it. No checkpoint swap fixes
that, Juggernaut included: it is an SDXL finetune with the same encoder.

Root set now runs FLUX.1 dev (Comfy-Org fp8 all-in-one, since BFL's own file is
UNET-only and CheckpointLoaderSimple cannot load it), compiled from the flux
target, whose T5 encoder takes the whole prompt. FluxGuidance 3.5, CFG 1.0, and
ConditioningZeroOut on the negative input: FLUX is guidance distilled and has no
negative channel, exactly like Z-Image. Uses EmptySD3LatentImage because FLUX
works in a 16-channel latent space.

The two sets load different gallery styles on purpose. How well a style demos is
a property of the style and the model together: Z-Image reproduces painterly
technique and keeps the Renaissance style, FLUX is strongest at light and
tonality and takes a noir one.

Also:

- Adds the reference-images template the root set was missing, and renumbers
  Facets to 05, so both sets are 01-05 with matching names. Both sets are now
  pinned by test against a shared story list.
- Model download metadata moves to each node's properties.models, where ComfyUI
  reads it, so a missing checkpoint offers a download instead of being silently
  substituted.
- The Load node names the style its slug resolves to, on a read-only row under
  style_ref, via a new /styleref/resolve route.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Behzad
2026-07-27 13:20:07 +02:00
co-authored by Claude Sonnet 5
parent 70fcf9ef00
commit cd36fae865
15 changed files with 1978 additions and 538 deletions
+134 -5
View File
@@ -9,6 +9,10 @@
* styles) — each 8 rows deep with its own ‹ Prev / Next ›. The query filters
* both sections, so the search box narrows the library as well as the
* gallery;
* - a read-only "loaded style" line under `style_ref` naming the style that
* slug resolves to, so a graph says which style it uses without being
* queued. It is marked non-serializing and sits mid-list, the one place
* this file touches widget order;
* - a "↻ Refresh style" button that POSTs /styleref/refresh, which drops the
* server-side cache and dirties the node so the next queue re-downloads;
* - status + Sign in / Sign out buttons on StyleRef Login, backed by the
@@ -18,9 +22,13 @@
* (ai_tools, style_md, midjourney, json) but the `positive` output feeds
* a CLIP Text Encode — those are for copying out, not sampler text.
*
* It creates no custom widget type, overrides no drawing beyond the badge, and
* touches no serialization — so if a ComfyUI frontend release changes the
* extension API and this stops loading, the nodes degrade to exactly what they
* It creates no custom widget type and overrides no drawing beyond the badge.
* The one widget it inserts rather than appends is flagged out of serialization
* both ways frontends read that flag, because `widgets_values` is positional
* and a serialising widget mid-list would shift every value after it.
*
* If a ComfyUI frontend release changes the extension API and this stops
* loading, the nodes degrade to exactly what they
* are without it: text fields you type into. That is the plan's
* "degraded-but-unbreakable fallback", and it is why the node's own `search` +
* `category` inputs exist in Python too.
@@ -71,10 +79,10 @@ async function probeRoutes() {
async function fetchJson(url) {
try {
const res = await fetch(url);
if (!res.ok) return { styles: [], error: `Search failed (${res.status})` };
if (!res.ok) return { styles: [], error: `Request failed (${res.status})` };
return await res.json();
} catch (err) {
return { styles: [], error: `Search failed: ${err?.message ?? err}` };
return { styles: [], error: `Request failed: ${err?.message ?? err}` };
}
}
@@ -581,6 +589,116 @@ function setStatus(node, widget, text) {
app.canvas?.setDirty?.(true, true);
}
const NAME_LABEL = "loaded style";
const NAME_IDLE = "—";
/**
* A read-only line naming the loaded style, placed immediately under
* `style_ref` — the field it describes.
*
* Two things make this safe to insert mid-list rather than append. It is marked
* non-serializing on both the widget and its options (frontends have read one
* or the other), and `computeSize` is left alone so layout stays the frontend's
* business. That matters because `widgets_values` is a positional array: a
* widget that serialises from the middle of the list shifts every value after
* it, and a saved workflow reloads with `search` holding the category, and so
* on. The existing buttons all append precisely to avoid that question.
*
* If a frontend ignores the flags, the failure is visible immediately — reload
* a saved graph and the fields are wrong — which is what the smoke test checks.
* Nothing here is load-bearing: drop this widget and the node is unchanged.
*/
function addStyleNameWidget(node) {
const widget = node.addWidget("text", NAME_LABEL, NAME_IDLE, () => {}, {
serialize: false,
});
widget.serialize = false;
widget.serializeValue = () => undefined;
// Read-only: it reports, it is not an input. `disabled` covers the canvas
// widget; `inputEl` exists only once a DOM-backed frontend has built it.
widget.disabled = true;
if (widget.inputEl) widget.inputEl.readOnly = true;
// Move it from the end (where addWidget put it) to just after `style_ref`.
const widgets = node.widgets ?? [];
const from = widgets.indexOf(widget);
const anchor = widgets.findIndex((w) => w.name === "style_ref");
if (from > -1 && anchor > -1 && from !== anchor + 1) {
widgets.splice(from, 1);
widgets.splice(anchor + 1, 0, widget);
}
return widget;
}
/** The widget's value. Truncated so a long name cannot widen the node. */
function styleNameValue(res) {
if (!res || res.ok !== true) {
// A half-typed slug is the common case here, not a failure worth
// shouting about — the node still queues and reports properly.
return res?.error ? res.error : "not found";
}
const name = String(res.name ?? "").trim() || "Untitled style";
return name.length > 34 ? `${name.slice(0, 33)}…` : name;
}
/**
* Sets the widget's *value*, not its name — unlike the Login status line,
* which is a button whose name is its label. Here the name is the static
* "loaded style" caption and the value is the answer.
*/
function setStyleName(node, widget, text) {
widget.value = text;
node.setDirtyCanvas(true, true);
app.canvas?.setDirty?.(true, true);
}
async function refreshStyleName(node, nameWidget) {
if (!nameWidget) return;
const ref = (node.widgets?.find((w) => w.name === "style_ref")?.value ?? "")
.toString()
.trim();
if (!ref) {
setStyleName(node, nameWidget, NAME_IDLE);
return;
}
setStyleName(node, nameWidget, "resolving…");
const res = await fetchJson(`/styleref/resolve?ref=${encodeURIComponent(ref)}`);
// The field may have moved on while the request was in flight (typing is
// faster than a round trip); a stale answer must not overwrite a newer one.
const current = (node.widgets?.find((w) => w.name === "style_ref")?.value ?? "")
.toString()
.trim();
if (current !== ref) return;
setStyleName(node, nameWidget, styleNameValue(res));
}
/**
* Resolve on a settled edit, not on every keystroke — a slug is pasted or typed
* in bursts, and one request per character would hammer the API for prefixes
* that cannot resolve anyway.
*/
function watchStyleRef(node, nameWidget) {
const refWidget = node.widgets?.find((w) => w.name === "style_ref");
if (!refWidget) return;
let timer = null;
const schedule = () => {
clearTimeout(timer);
timer = setTimeout(() => refreshStyleName(node, nameWidget), 400);
};
const previous = refWidget.callback;
refWidget.callback = function (...args) {
const out = previous?.apply(this, args);
schedule();
return out;
};
// Templates and reloaded graphs arrive with the field already filled, so
// the name must appear without the user touching anything.
schedule();
}
async function refreshLoginStatus(node, statusWidget) {
try {
const res = await fetchJson("/styleref/auth/status");
@@ -618,6 +736,16 @@ app.registerExtension({
nodeType.prototype.onNodeCreated = function () {
const result = onCreated?.apply(this, arguments);
// The name line, directly under `style_ref`. A slug says
// nothing about which style is wired in, and before the first
// queue the node cannot say either — so resolve it as soon as
// the field settles.
let nameWidget = null;
if (routesAvailable !== false) {
nameWidget = addStyleNameWidget(this);
watchStyleRef(this, nameWidget);
}
// Only add the search button when the backend routes exist
// — otherwise it could only 404.
if (routesAvailable !== false) {
@@ -632,6 +760,7 @@ app.registerExtension({
refWidget.value = chosen;
refWidget.callback?.(chosen);
this.setDirtyCanvas(true, true);
refreshStyleName(this, nameWidget);
});
}