Files
StyleRef-styleref-comfyui/scripts/build_workflows.py
T
BehzadandClaude Sonnet 5 70fcf9ef00 Demo a style whose effect is actually visible
The templates loaded a documentary photography style that compiles to
"photography, naturalistic lighting, deep focus, RAW-like" — close to what
SDXL produces unprompted — so the first queue looked like the nodes had done
nothing, and the demo subjects were unrelated scenery on top of that.

Load Renaissance Mythic Classicism instead: tempera-on-panel technique and a
muted Renaissance palette, unmistakable against a base render, with no
typography for the sampler to turn into gibberish and no subject it rules
out. Subjects are now concrete everyday scenes carrying no style words of
their own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 23:35:22 +02:00

746 lines
29 KiB
Python

#!/usr/bin/env python3
"""
Generate the shipped workflow templates in workflows/.
Hand-maintaining four ComfyUI workflow files means hand-maintaining node ids,
link ids, and slot indices across all of them — the kind of bookkeeping that is
silently wrong until someone drags the file onto a canvas. This builds them from
a description instead, so the ids are correct by construction.
Layout policy: node positions are never guessed. Every node's height
is estimated from its widget count, and nodes stack per column with a fixed
vertical gap — so no template can ship with overlapping nodes at default sizes.
A test asserts that invariant on the committed files.
Instruction policy: user-facing instructions live in visible core
`Note` nodes on the canvas. `extra.styleref.notes` carries only a one-line
provenance string — ComfyUI never renders it, so it must not carry anything a
user needs to read.
Run after changing a node's inputs or outputs:
python scripts/build_workflows.py
CI checks the committed files match this script's output.
"""
from __future__ import annotations
import json
import os
import sys
from typing import Any
_PACK_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _PACK_ROOT not in sys.path:
sys.path.insert(0, _PACK_ROOT)
# Single-sourced from the node itself (the Facets output list is data-driven
# and long — duplicating it here would guarantee drift).
from styleref_nodes.facets import StyleRefFacets # noqa: E402
WORKFLOW_DIR = os.path.join(_PACK_ROOT, "workflows")
PROVENANCE = "Generated by scripts/build_workflows.py — edit the script, not this file."
# Slot layouts, mirroring each node's RETURN_TYPES / INPUT_TYPES order.
OUTPUTS = {
"StyleRefLoad": [
("style", "STYLEREF_STYLE"),
("search_results", "STRING"),
],
"StyleRefApply": [("positive", "STRING"), ("negative", "STRING"), ("attribution", "STRING")],
"StyleRefFacets": list(zip(StyleRefFacets.RETURN_NAMES, StyleRefFacets.RETURN_TYPES)),
"StyleRefReferenceImages": [("images", "IMAGE"), ("count", "INT")],
"StyleRefLogin": [("status", "STRING")],
"CheckpointLoaderSimple": [("MODEL", "MODEL"), ("CLIP", "CLIP"), ("VAE", "VAE")],
"UNETLoader": [("MODEL", "MODEL")],
"CLIPLoader": [("CLIP", "CLIP")],
"VAELoader": [("VAE", "VAE")],
"CLIPTextEncode": [("CONDITIONING", "CONDITIONING")],
"ConditioningZeroOut": [("CONDITIONING", "CONDITIONING")],
"EmptyLatentImage": [("LATENT", "LATENT")],
"EmptySD3LatentImage": [("LATENT", "LATENT")],
"KSampler": [("LATENT", "LATENT")],
"VAEDecode": [("IMAGE", "IMAGE")],
"LoadImage": [("IMAGE", "IMAGE"), ("MASK", "MASK")],
"SaveImage": [],
"PreviewImage": [],
"PreviewAny": [],
"Note": [],
}
INPUTS = {
"StyleRefApply": [("style", "STYLEREF_STYLE")],
"StyleRefFacets": [("style", "STYLEREF_STYLE")],
"StyleRefReferenceImages": [("style", "STYLEREF_STYLE")],
"CLIPTextEncode": [("clip", "CLIP"), ("text", "STRING")],
"ConditioningZeroOut": [("conditioning", "CONDITIONING")],
"KSampler": [
("model", "MODEL"),
("positive", "CONDITIONING"),
("negative", "CONDITIONING"),
("latent_image", "LATENT"),
],
"VAEDecode": [("samples", "LATENT"), ("vae", "VAE")],
"SaveImage": [("images", "IMAGE")],
"PreviewImage": [("images", "IMAGE")],
# PreviewAny accepts anything — ComfyUI's wildcard type.
"PreviewAny": [("source", "*")],
}
# ── layout constants ─────────────────────────────────────────────────────────
NODE_WIDTH = 320
HEADER_H = 60 # title bar
SLOT_H = 24 # one input/output slot row
WIDGET_ROW_H = 28 # one single-line widget
MULTILINE_H = 90 # one multiline text widget
V_GAP = 48 # vertical gap between stacked nodes
COL_W = 380 # column pitch; > NODE_WIDTH so columns never touch
# Widgets that render multiline per node type (by widget index).
MULTILINE_WIDGETS = {
"StyleRefApply": {0, 2}, # subject, extra_negative
"CLIPTextEncode": {0},
"Note": {0},
}
# Node titles longer than this truncate in both frontends.
MAX_TITLE = 28
# The gallery style every ready-to-run template loads. Picked for how legible
# its effect is on the first queue: it compiles to tempera-on-panel technique
# and a muted Renaissance palette, so the render is obviously not what the base
# checkpoint would have produced alone. Two styles that read well on the
# gallery make poor demos here — a naturalistic photography style lands close
# to what SDXL does unprompted (the node looks like it did nothing), and a
# typography-led graphic style asks the sampler for headline text it renders as
# gibberish. This one carries no type at all and constrains no subject.
GALLERY_STYLE_REF = "sbdlwwly-66ae1a89efdc" # Renaissance Mythic Classicism
# Placeholder ref for the "your own style" templates. Refs are opaque ids now
# (no name lookup), so the templates ship an obvious placeholder the user swaps
# for their own style's id — or fills via the node's "Search styles…" picker.
OWN_STYLE_REF = "<your style id>"
# Subjects for the consistency grid. Deliberately varied — interior, open
# landscape, night exterior — because the grid's whole claim is that one style
# holds across unrelated subjects. All three are plain scenes with no style
# words of their own, so anything stylistic in the render came from StyleRef.
GRID_SUBJECTS = [
"two friends flipping through crates of vinyl in a record shop",
"a lone surfer walking back along the beach at dusk",
"a roadside diner at night seen from the parking lot",
]
# The single-subject templates all use the first grid subject, so a user who
# opens two templates sees the same prompt render consistently.
DEMO_SUBJECT = GRID_SUBJECTS[0]
# Frontend model-download metadata: a missing checkpoint becomes a guided
# download instead of a red error.
MODEL_SDXL = {
"name": "sd_xl_base_1.0.safetensors",
"url": "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors",
"directory": "checkpoints",
}
# The sections the Facets templates surface on canvas. Facets has 24 outputs;
# previewing all of them would be a wall of nodes, so the templates show a
# representative six and the note points at the rest. Names are looked up in
# the node's own RETURN_NAMES, so a schema reorder can never mis-slot them.
FACET_SHOWCASE = [
"colors",
"light_shadow",
"mood_personality",
"surface_material",
"shape_language",
"guardrails",
]
# Z-Image Turbo (Tongyi-MAI) — an S3-DiT model with its own loader stack (a
# UNET, a Qwen-3-4B text encoder, and a Flux-style 16-channel VAE), not a
# single checkpoint. URLs are the Comfy-Org repacks. It compiles well from the
# `diffusion` target: qwen handles the tag-weighted output that SDXL/SD1.5 use.
MODEL_ZIMAGE_UNET = {
"name": "z_image_turbo_bf16.safetensors",
"url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/diffusion_models/z_image_turbo_bf16.safetensors",
"directory": "diffusion_models",
}
MODEL_ZIMAGE_CLIP = {
"name": "qwen_3_4b.safetensors",
"url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/text_encoders/qwen_3_4b.safetensors",
"directory": "text_encoders",
}
MODEL_ZIMAGE_VAE = {
"name": "ae.safetensors",
"url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/vae/ae.safetensors",
"directory": "vae",
}
MODELS_ZIMAGE = [MODEL_ZIMAGE_UNET, MODEL_ZIMAGE_CLIP, MODEL_ZIMAGE_VAE]
def node_height(node_type: str, widgets: list[Any]) -> int:
"""
Estimated on-canvas height: header + one row per input/output
slot + one row per widget. The slot term matters for wide-output nodes —
Facets alone has dozens of outputs.
"""
slots = max(len(INPUTS.get(node_type, [])), len(OUTPUTS.get(node_type, [])))
multiline = MULTILINE_WIDGETS.get(node_type, set())
rows = sum(1 for i in range(len(widgets)) if i not in multiline)
tall = sum(1 for i in range(len(widgets)) if i in multiline)
return HEADER_H + slots * SLOT_H + rows * WIDGET_ROW_H + tall * MULTILINE_H
class Graph:
"""
Workflow builder with column layout.
`place(node_type, col, …)` stacks nodes top-to-bottom within a column,
spacing them by each node's estimated height plus V_GAP — overlap-free by
construction. Columns are indexed (0, 1, 2, …) and spaced COL_W apart.
"""
def __init__(self) -> None:
self.nodes: list[dict[str, Any]] = []
self.links: list[list[Any]] = []
self._node_id = 0
self._link_id = 0
self._cursor: dict[int, int] = {} # column index → next free y
def place(
self,
node_type: str,
col: int,
widgets: list[Any] | None = None,
title: str | None = None,
height: int | None = None,
) -> int:
if title is not None and len(title) > MAX_TITLE:
raise ValueError(f"title longer than {MAX_TITLE} chars truncates: {title!r}")
widgets = widgets or []
h = height if height is not None else node_height(node_type, widgets)
x = 40 + col * COL_W
y = self._cursor.get(col, 60)
self._cursor[col] = y + h + V_GAP
self._node_id += 1
node: dict[str, Any] = {
"id": self._node_id,
"type": node_type,
"pos": [x, y],
"size": [NODE_WIDTH, h],
"flags": {},
"order": len(self.nodes),
"mode": 0,
"inputs": [
{"name": name, "type": kind, "link": None}
for name, kind in INPUTS.get(node_type, [])
# `text` on CLIPTextEncode is a widget until something connects
# to it; declaring it as an input is what lets Apply drive it.
],
"outputs": [
{"name": name, "type": kind, "links": [], "slot_index": index}
for index, (name, kind) in enumerate(OUTPUTS.get(node_type, []))
],
"properties": {"Node name for S&R": node_type},
"widgets_values": widgets,
}
if title:
node["title"] = title
self.nodes.append(node)
return self._node_id
def note(self, col: int, text: str, title: str = "Read me") -> int:
"""A visible core Note node — the instruction channel."""
# Height scales with the text so long notes don't clip at default size.
h = HEADER_H + 30 + 18 * (text.count("\n") + len(text) // 55)
return self.place("Note", col, [text], title=title, height=h)
def link(self, src: int, src_slot: int, dst: int, dst_slot: int) -> None:
src_node = self._node(src)
dst_node = self._node(dst)
kind = src_node["outputs"][src_slot]["type"]
self._link_id += 1
self.links.append([self._link_id, src, src_slot, dst, dst_slot, kind])
src_node["outputs"][src_slot]["links"].append(self._link_id)
dst_node["inputs"][dst_slot]["link"] = self._link_id
def _node(self, node_id: int) -> dict[str, Any]:
return next(n for n in self.nodes if n["id"] == node_id)
def to_json(self, models: list[dict[str, str]] | None = None) -> dict[str, Any]:
payload: dict[str, Any] = {
"last_node_id": self._node_id,
"last_link_id": self._link_id,
"nodes": self.nodes,
"links": self.links,
"groups": [],
"config": {},
"extra": {"styleref": {"notes": PROVENANCE}},
"version": 0.4,
}
if models:
payload["models"] = models
return payload
# Shared widget rows (single source for the values the nodes expect, in the
# INPUT_TYPES order of each node).
def load_widgets(style_ref: str) -> list[Any]:
# style_ref, search, category, use_cache, save_to_library
return [style_ref, "", "any", True, False]
def apply_widgets(subject: str, target: str) -> list[Any]:
# subject, target, extra_negative, compact, sections, subject_first
# Targets mirror the web app's copy list: diffusion drives SDXL/SD1.5
# checkpoints, flux drives FLUX ones.
return [subject, target, "", False, "", True]
def sampler_widgets(cfg: float) -> list[Any]:
# Seeds randomize: a fixed seed makes the second queue a silent
# cache no-op — deadly in a demo.
return [0, "randomize", 25, cfg, "euler", "normal", 1.0]
def _sdxl_tail(graph: Graph, apply_node: int, checkpoint: int, col: int) -> None:
"""Apply → two CLIP encoders → KSampler → VAEDecode → SaveImage."""
positive = graph.place("CLIPTextEncode", col, [""], title="Positive (from StyleRef)")
negative = graph.place("CLIPTextEncode", col, [""], title="Negative (from StyleRef)")
latent = graph.place("EmptyLatentImage", col, [1024, 1024, 1])
sampler = graph.place("KSampler", col + 1, sampler_widgets(cfg=7.0))
decode = graph.place("VAEDecode", col + 2)
save = graph.place("SaveImage", col + 2, ["StyleRef"])
graph.link(apply_node, 0, positive, 1)
graph.link(apply_node, 1, negative, 1)
graph.link(checkpoint, 1, positive, 0)
graph.link(checkpoint, 1, negative, 0)
graph.link(checkpoint, 0, sampler, 0)
graph.link(positive, 0, sampler, 1)
graph.link(negative, 0, sampler, 2)
graph.link(latent, 0, sampler, 3)
graph.link(sampler, 0, decode, 0)
graph.link(checkpoint, 2, decode, 1)
graph.link(decode, 0, save, 0)
def _facet_board(graph: Graph, load: int, col: int) -> int:
"""
Load → Facets, with a Preview Any on each showcased section.
Facets outputs plain STRINGs, so the only thing a template can *show*
without depending on third-party packs is the text itself — which is the
point: seeing `colors` as readable lines is what tells a user they can drag
that output into any text input downstream. Returns the Facets node id so
callers can wire more sections if they want.
"""
facets = graph.place("StyleRefFacets", col, [], title="Facets — every section")
graph.link(load, 0, facets, 0)
for section in FACET_SHOWCASE:
slot = StyleRefFacets.RETURN_NAMES.index(section)
preview = graph.place(
"PreviewAny", col + 1, [], title=f"Facet — {section.replace('_', ' ')}"
)
graph.link(facets, slot, preview, 0)
return facets
def _preview(graph: Graph, apply_node: int, col: int, suffix: str = "") -> None:
"""
Compiled prompts made visible, in every template: Preview Any on Apply's
positive and negative kills the "empty text box on the encoder looks
broken" perception and teaches what StyleRef actually produces.
"""
pos = graph.place("PreviewAny", col, [], title=f"Compiled positive{suffix}")
neg = graph.place("PreviewAny", col, [], title=f"Compiled negative{suffix}")
graph.link(apply_node, 0, pos, 0)
graph.link(apply_node, 1, neg, 0)
def zimage_sampler_widgets() -> list[Any]:
"""
Z-Image Turbo defaults: ~8 steps, CFG 1.0, euler/simple.
CFG stays at 1.0 because Z-Image has no negative channel to activate (see
`_zimage_tail`) — raising it only oversaturates and slows the sample down.
"""
return [0, "randomize", 8, 1.0, "euler", "simple", 1.0]
def _zimage_loaders(graph: Graph) -> tuple[int, int, int]:
"""Place the Z-Image UNET + CLIP + VAE loaders. Returns their node ids."""
unet = graph.place(
"UNETLoader", 0, [MODEL_ZIMAGE_UNET["name"], "default"], title="Z-Image UNET"
)
clip = graph.place(
"CLIPLoader", 0, [MODEL_ZIMAGE_CLIP["name"], "lumina2", "default"], title="Qwen text encoder"
)
vae = graph.place("VAELoader", 0, [MODEL_ZIMAGE_VAE["name"]], title="Z-Image VAE")
return unet, clip, vae
def _zimage_tail(
graph: Graph, apply_node: int, unet: int, clip: int, vae: int, col: int
) -> None:
"""
Apply → one CLIP encoder → KSampler → VAEDecode → SaveImage, on the Z-Image
loader stack.
**No negative encoder.** Z-Image's official pipeline runs without
classifier-free guidance — the authors state the model does not use negative
prompts at all — so a second encoder here would be dead weight wired to a
sampler input that cannot read it. The sampler's negative gets a zeroed
conditioning instead, the same shape a FLUX graph uses. Apply's `negative`
output stays connected to its Preview Any so the compiled exclusions are
still readable and copyable; anything you need from them belongs in
`subject` on Apply.
"""
positive = graph.place("CLIPTextEncode", col, [""], title="Positive (from StyleRef)")
zero_out = graph.place("ConditioningZeroOut", col, [], title="Z-Image has no negative")
latent = graph.place("EmptySD3LatentImage", col, [1024, 1024, 1])
sampler = graph.place("KSampler", col + 1, zimage_sampler_widgets())
decode = graph.place("VAEDecode", col + 2)
save = graph.place("SaveImage", col + 2, ["StyleRef"])
graph.link(apply_node, 0, positive, 1)
graph.link(clip, 0, positive, 0)
graph.link(unet, 0, sampler, 0)
graph.link(positive, 0, sampler, 1)
graph.link(positive, 0, zero_out, 0)
graph.link(zero_out, 0, sampler, 2)
graph.link(latent, 0, sampler, 3)
graph.link(sampler, 0, decode, 0)
graph.link(vae, 0, decode, 1)
graph.link(decode, 0, save, 0)
_ZIMAGE_SETUP_NOTE = (
"Z-Image Turbo (S3-DiT). Three model files download on first use: the UNET "
"(diffusion_models/), the Qwen text encoder (text_encoders/), and the VAE "
"(vae/).\n"
"Sampler defaults are turbo: 8 steps, CFG 1.0, euler/simple.\n"
"Z-Image takes no negative prompt. The model runs without classifier-free "
"guidance, so there is no negative encoder on this canvas — the sampler's "
"negative input gets a zeroed conditioning. StyleRef still compiles the "
"negative and previews it; put anything you need from it into `subject` on "
"Apply as a positive phrase (\"fully clothed\", \"no text or watermark\"). "
"Raising CFG does not bring the negative back.\n"
"Style prompts use the `diffusion` target — Z-Image reads the same "
"tag-weighted output SDXL/SD1.5 use."
)
def workflow_zimage_01_quickstart() -> dict[str, Any]:
"""Z-Image quickstart: Load a gallery style → Apply → generate."""
g = Graph()
g.note(
0,
"Z-Image quickstart — no account needed.\n"
"1. Queue — the three Z-Image files download on first run.\n"
"2. Change `subject` on StyleRef Apply and queue again (seeds randomize).\n"
"Swap `style_ref` on StyleRef Load for any gallery slug.\n\n" + _ZIMAGE_SETUP_NOTE,
title="Z-Image quickstart",
)
load = g.place(
"StyleRefLoad", 0, load_widgets(GALLERY_STYLE_REF), title="Load — swap this slug"
)
unet, clip, vae = _zimage_loaders(g)
apply_node = g.place(
"StyleRefApply", 1, apply_widgets(DEMO_SUBJECT, "diffusion")
)
g.link(load, 0, apply_node, 0)
_preview(g, apply_node, 1)
_zimage_tail(g, apply_node, unet, clip, vae, col=2)
return g.to_json(models=MODELS_ZIMAGE)
def workflow_zimage_02_consistency() -> dict[str, Any]:
"""One style across three subjects on Z-Image."""
g = Graph()
g.note(
0,
"Z-Image consistency grid — no account needed.\n"
"One style feeds three Apply nodes with three subjects; each renders "
"through its own Z-Image sampler. Queue once and compare: one style, "
"many subjects, a coherent set within this model.\n\n" + _ZIMAGE_SETUP_NOTE,
title="Z-Image consistency",
)
load = g.place("StyleRefLoad", 0, load_widgets(GALLERY_STYLE_REF))
unet, clip, vae = _zimage_loaders(g)
subjects = GRID_SUBJECTS
for index, subject in enumerate(subjects):
apply_node = g.place(
"StyleRefApply", 1, apply_widgets(subject, "diffusion"), title=f"Subject {index + 1}"
)
g.link(load, 0, apply_node, 0)
_preview(g, apply_node, 1, suffix=f" {index + 1}")
_zimage_tail(g, apply_node, unet, clip, vae, col=2)
return g.to_json(models=MODELS_ZIMAGE)
def workflow_zimage_03_your_own_style() -> dict[str, Any]:
"""Your own style, made on the web, rendered on Z-Image."""
g = Graph()
g.note(
0,
"Use your own style on Z-Image.\n"
"Extract a style from any image at https://styleref.io, then load it "
"here by its share slug or id.\n"
"1. Queue the StyleRef Login node alone first (use its Sign in button).\n"
"2. Put your style's id in `style_ref` on StyleRef Load, or pick it\n"
" with the Search styles… button.\n"
"3. Queue.\n\n" + _ZIMAGE_SETUP_NOTE,
title="Z-Image — your style",
)
login = g.place("StyleRefLogin", 0, [], title="Login — check status")
assert login
load = g.place(
"StyleRefLoad", 0, load_widgets(OWN_STYLE_REF), title="Load — your own style"
)
unet, clip, vae = _zimage_loaders(g)
apply_node = g.place(
"StyleRefApply", 1, apply_widgets(DEMO_SUBJECT, "diffusion")
)
g.link(load, 0, apply_node, 0)
_preview(g, apply_node, 1)
_zimage_tail(g, apply_node, unet, clip, vae, col=2)
return g.to_json(models=MODELS_ZIMAGE)
def workflow_zimage_04_reference_images() -> dict[str, Any]:
"""Z-Image render + the style's inspiration images side by side."""
g = Graph()
g.note(
0,
"Z-Image — style prompt and reference images together.\n"
"StyleRef Apply drives the Z-Image sampler with the style's prompt; "
"StyleRef Reference Images pulls the style's inspiration images out as "
"a batch, previewed alongside. The prompt says how the style reads; the "
"images show how it looks — wire the images into IPAdapter or a "
"reference-only ControlNet to combine both.\n"
"Use a gallery style that has inspiration images.\n\n" + _ZIMAGE_SETUP_NOTE,
title="Z-Image + references",
)
load = g.place(
"StyleRefLoad", 0, load_widgets(GALLERY_STYLE_REF), title="Load — a gallery slug"
)
unet, clip, vae = _zimage_loaders(g)
refs = g.place("StyleRefReferenceImages", 1, [12], title="Reference images")
ref_preview = g.place("PreviewImage", 1, [], title="Inspiration images")
g.link(load, 0, refs, 0)
g.link(refs, 0, ref_preview, 0)
apply_node = g.place(
"StyleRefApply", 1, apply_widgets(DEMO_SUBJECT, "diffusion")
)
g.link(load, 0, apply_node, 0)
_preview(g, apply_node, 1)
_zimage_tail(g, apply_node, unet, clip, vae, col=2)
return g.to_json(models=MODELS_ZIMAGE)
def workflow_01_quickstart() -> dict[str, Any]:
"""Load a gallery style → Apply → generate. The 60-second demo."""
g = Graph()
g.note(
0,
"Quickstart — no account needed.\n"
"1. Set your SDXL checkpoint below (a missing model offers a download).\n"
"2. Queue. Seeds randomize, so every queue is a fresh image.\n"
"3. Change `subject` on StyleRef Apply and queue again.\n"
"Swap `style_ref` on StyleRef Load for any slug from "
"https://styleref.io/gallery — or use the Search styles… button.",
title="Quickstart",
)
load = g.place(
"StyleRefLoad",
0,
load_widgets(GALLERY_STYLE_REF),
title="Load — swap this slug",
)
checkpoint = g.place("CheckpointLoaderSimple", 0, [MODEL_SDXL["name"]])
apply_node = g.place(
"StyleRefApply", 1, apply_widgets(DEMO_SUBJECT, "diffusion")
)
_preview(g, apply_node, 1)
g.link(load, 0, apply_node, 0)
_sdxl_tail(g, apply_node, checkpoint, col=2)
return g.to_json(models=[MODEL_SDXL])
def workflow_02_consistency() -> dict[str, Any]:
"""
One style across three subjects, side by side.
Consistency is the claim this demonstrates, and it is scoped precisely: the
same style applied within this one model produces a coherent set. It is not
a before/after, and not a claim that other tools match this output.
"""
g = Graph()
g.note(
0,
"Consistency grid — no account needed.\n"
"One style feeds three Apply nodes with three different subjects; each "
"renders through its own sampler. Queue once and compare: one style, "
"many subjects, a coherent set within this model.\n"
"Change any subject, or swap the style on StyleRef Load, and queue again.",
title="Consistency grid",
)
load = g.place("StyleRefLoad", 0, load_widgets(GALLERY_STYLE_REF))
checkpoint = g.place("CheckpointLoaderSimple", 0, [MODEL_SDXL["name"]])
subjects = GRID_SUBJECTS
for index, subject in enumerate(subjects):
apply_node = g.place(
"StyleRefApply", 1, apply_widgets(subject, "diffusion"), title=f"Subject {index + 1}"
)
g.link(load, 0, apply_node, 0)
_preview(g, apply_node, 1, suffix=f" {index + 1}")
_sdxl_tail(g, apply_node, checkpoint, col=2)
return g.to_json(models=[MODEL_SDXL])
def workflow_03_your_own_style() -> dict[str, Any]:
"""Your own style, made on the web, loaded here (template 03)."""
g = Graph()
g.note(
0,
"Use your own style.\n"
"Extract a style from any image at https://styleref.io — it appears in "
"your library — then load it here by its share slug or id.\n"
"1. Queue the StyleRef Login node alone first (Queue runs every node, "
"so an empty workflow is the clean way). If status says signed out, "
"set action to `sign in` and queue again.\n"
"2. Put your style's id in `style_ref` on StyleRef Load, or pick it\n"
" with the Search styles… button.\n"
"3. Set your SDXL checkpoint and queue.",
title="Your own style",
)
login = g.place("StyleRefLogin", 0, [], title="Login — check status")
assert login # not linked: it is a status helper, not a data node
load = g.place(
"StyleRefLoad",
0,
load_widgets(OWN_STYLE_REF),
title="Load — your own style",
)
checkpoint = g.place("CheckpointLoaderSimple", 0, [MODEL_SDXL["name"]])
apply_node = g.place(
"StyleRefApply", 1, apply_widgets(DEMO_SUBJECT, "diffusion")
)
g.link(load, 0, apply_node, 0)
_preview(g, apply_node, 1)
_sdxl_tail(g, apply_node, checkpoint, col=2)
return g.to_json(models=[MODEL_SDXL])
_FACETS_NOTE = (
"Inside the style — the Facets node.\n"
"Apply gives you one prompt string. Facets gives you the style's sections "
"one output at a time — 24 of them, exactly the sections you see on the "
"style board at styleref.io, plus the six custom items.\n"
"Six are previewed here so you can read them. Every other output is on the "
"node too; drag any of them into a text input.\n"
"They are plain STRINGs, so they go anywhere text goes: a second CLIP "
"encoder for regional conditioning, an IPAdapter or ControlNet prompt, a "
"LoRA trigger line, or your own nodes. That is the escape hatch — use the "
"palette without taking the whole prompt.\n"
"The render alongside is the paved road (Apply), so you can compare the "
"assembled prompt against the parts it was built from."
)
def workflow_04_facets() -> dict[str, Any]:
"""Every section of a style, exposed and readable, next to a normal render."""
g = Graph()
g.note(0, _FACETS_NOTE, title="Facets")
load = g.place(
"StyleRefLoad",
0,
load_widgets(GALLERY_STYLE_REF),
title="Load — swap this slug",
)
checkpoint = g.place("CheckpointLoaderSimple", 0, [MODEL_SDXL["name"]])
_facet_board(g, load, col=1)
apply_node = g.place(
"StyleRefApply", 3, apply_widgets(DEMO_SUBJECT, "diffusion")
)
g.link(load, 0, apply_node, 0)
_preview(g, apply_node, 3)
_sdxl_tail(g, apply_node, checkpoint, col=4)
return g.to_json(models=[MODEL_SDXL])
def workflow_zimage_05_facets() -> dict[str, Any]:
"""The Facets story on the Z-Image loader stack."""
g = Graph()
g.note(0, _FACETS_NOTE + "\n\n" + _ZIMAGE_SETUP_NOTE, title="Facets on Z-Image")
load = g.place(
"StyleRefLoad",
0,
load_widgets(GALLERY_STYLE_REF),
title="Load — swap this slug",
)
unet, clip, vae = _zimage_loaders(g)
_facet_board(g, load, col=1)
apply_node = g.place(
"StyleRefApply", 3, apply_widgets(DEMO_SUBJECT, "diffusion")
)
g.link(load, 0, apply_node, 0)
_preview(g, apply_node, 3)
_zimage_tail(g, apply_node, unet, clip, vae, col=4)
return g.to_json(models=MODELS_ZIMAGE)
# Keyed by path relative to workflows/. The SDXL set lives at the root; the
# Z-Image set lives in workflows/z-image/.
WORKFLOWS = {
"01-quickstart.json": workflow_01_quickstart,
"02-consistency-grid.json": workflow_02_consistency,
"03-your-own-style.json": workflow_03_your_own_style,
"04-facets.json": workflow_04_facets,
"z-image/01-quickstart.json": workflow_zimage_01_quickstart,
"z-image/02-consistency-grid.json": workflow_zimage_02_consistency,
"z-image/03-your-own-style.json": workflow_zimage_03_your_own_style,
"z-image/04-reference-images.json": workflow_zimage_04_reference_images,
"z-image/05-facets.json": workflow_zimage_05_facets,
}
def main() -> None:
for relpath, builder in WORKFLOWS.items():
path = os.path.join(WORKFLOW_DIR, relpath)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as fh:
json.dump(builder(), fh, indent=2)
fh.write("\n")
print(f"wrote {path}")
if __name__ == "__main__":
main()