Files
StyleRef-styleref-comfyui/scripts/build_workflows.py
T
BehzadandClaude Sonnet 5 cd36fae865 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>
2026-07-27 13:20:07 +02:00

921 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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")],
"FluxGuidance": [("CONDITIONING", "CONDITIONING")],
"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")],
"FluxGuidance": [("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 each set loads. Deliberately different per model, because
# "which style demos well" is a property of the pair, not of the style.
#
# Z-Image renders a painterly style faithfully — panel texture, contour-led
# figures, a muted Renaissance palette — so it gets the painterly one.
#
# FLUX does not. It has a strong photoreal house look and reverts to it,
# and it is specifically weak at reproducing painterly technique; the usual
# remedy in the ecosystem is a style LoRA, which is far too much apparatus for
# a first-run demo. Rather than ship a demo that quietly under-delivers, the
# FLUX set uses a style whose identity is *light and tonality* — hard key,
# crushed blacks, monochrome. That is what FLUX is genuinely good at, and
# monochrome high-contrast output is unmistakably styled: nobody has to wonder
# whether the node did anything.
#
# Both are subject-agnostic (the consistency grid needs that) and both carry
# inspiration images (the reference-images template needs that).
#
# Two styles that read well on the gallery still demo badly anywhere here: a
# naturalistic photography style lands close to what a base model does
# unprompted, and a typography-led graphic style asks the sampler for headline
# text. Neither of these does either.
FLUX_STYLE_REF = "72e1zdae-e2d54a18d090" # Noir Low-Key Portrait
ZIMAGE_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 strolled back along the beach at dusk, carrying her large "
"surfing board.",
"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.
#
# The Comfy-Org all-in-one fp8 repack — UNET + CLIP/T5 + VAE in one file, so
# CheckpointLoaderSimple can load it. BFL's own flux1-dev.safetensors is
# UNET-only and cannot.
#
# Why FLUX rather than SDXL, which more people have installed: StyleRef
# compiles a long style spec (~480 tokens for a full style), and SDXL's CLIP
# attends to ~77 tokens per chunk — the style is mostly truncated before it
# reaches the sampler, whatever the checkpoint. FLUX reads the prompt with T5,
# which takes the whole thing. This is the first model where the compiled
# style and the text encoder actually fit each other.
#
# `flux1-schnell-fp8.safetensors` (Comfy-Org/flux1-schnell) is a drop-in swap:
# Apache-2.0 instead of dev's non-commercial licence, and ~4 steps instead of
# 20, at some cost in fidelity.
MODEL_FLUX = {
"name": "flux1-dev-fp8.safetensors",
"url": "https://huggingface.co/Comfy-Org/flux1-dev/resolve/main/flux1-dev-fp8.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,
models: list[dict[str, str]] | 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 models:
# ComfyUI reads download metadata from the *node's* properties —
# this is what turns a missing file into a download offer instead
# of a silently-substituted combo value. The workflow-level
# "models" key alone does not drive that prompt.
node["properties"]["models"] = models
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 _flux_checkpoint(graph: Graph) -> int:
"""
Place the FLUX checkpoint loader with its download metadata attached.
A helper rather than a bare `place()` call for the same reason
`_zimage_loaders` is one: the metadata has to travel with the node. When
declaring the download was a separate step, every template placed the
loader correctly and none of them offered the download.
"""
return graph.place(
"CheckpointLoaderSimple", 0, [MODEL_FLUX["name"]], models=[MODEL_FLUX]
)
def flux_sampler_widgets() -> list[Any]:
"""
FLUX dev defaults: 20 steps, CFG 1.0, euler/simple.
CFG stays at 1.0 for the same reason it does on Z-Image — FLUX is guidance
distilled, and prompt strength is set with FluxGuidance instead. Raising CFG
here does not sharpen the style, it just doubles the work and burns the
image.
"""
# Seeds randomize: a fixed seed makes the second queue a silent cache
# no-op — deadly in a demo.
return [0, "randomize", 20, 1.0, "euler", "simple", 1.0]
def _flux_tail(graph: Graph, apply_node: int, checkpoint: int, col: int) -> None:
"""
Apply → CLIP encoder → FluxGuidance → KSampler → VAEDecode → SaveImage.
**No negative encoder**, the same story as `_zimage_tail`: at CFG 1.0 the
negative branch is multiplied out entirely, so a second encoder would be
dead weight. The sampler's negative input takes a zeroed conditioning.
Apply's `negative` output stays wired to its Preview Any so the compiled
exclusions remain readable; fold anything you need from them into `subject`
as a positive phrase.
The latent is `EmptySD3LatentImage`, not `EmptyLatentImage`: FLUX works in a
16-channel latent space and the plain node emits 4 channels. This graph
previously shipped with the 4-channel node — it is the kind of mistake that
surfaces as a shape error or noise at sample time, never at build time.
"""
positive = graph.place("CLIPTextEncode", col, [""], title="Positive (from StyleRef)")
# FLUX's own default. It briefly shipped at 5.0 here, to force a painterly
# style that FLUX was never going to reproduce; with a style the model is
# actually good at, that compensation stops helping and starts costing the
# subject. This style asks for crushed blacks and no fill, and guidance is
# a blunt "obey harder" dial — over-driven, the frame goes to pure black and
# the subject dissolves into it. Style adherence is the pairing's job, not
# this number's.
guidance = graph.place("FluxGuidance", col, [3.5], title="Flux guidance")
zero_out = graph.place("ConditioningZeroOut", col, [], title="FLUX has no negative")
latent = graph.place("EmptySD3LatentImage", col, [1024, 1024, 1])
sampler = graph.place("KSampler", col + 1, flux_sampler_widgets())
decode = graph.place("VAEDecode", col + 2)
save = graph.place("SaveImage", col + 2, ["StyleRef"])
graph.link(apply_node, 0, positive, 1)
graph.link(checkpoint, 1, positive, 0)
graph.link(positive, 0, guidance, 0)
graph.link(positive, 0, zero_out, 0)
graph.link(checkpoint, 0, sampler, 0)
graph.link(guidance, 0, sampler, 1)
graph.link(zero_out, 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.
"""
# Seeds randomize: a fixed seed makes the second queue a silent cache
# no-op — deadly in a demo.
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",
models=[MODEL_ZIMAGE_UNET],
)
clip = graph.place(
"CLIPLoader",
0,
[MODEL_ZIMAGE_CLIP["name"], "lumina2", "default"],
title="Qwen text encoder",
models=[MODEL_ZIMAGE_CLIP],
)
vae = graph.place(
"VAELoader",
0,
[MODEL_ZIMAGE_VAE["name"]],
title="Z-Image VAE",
models=[MODEL_ZIMAGE_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."
)
_FLUX_STYLE_TIP = (
"Style not coming through strongly enough?\n"
"FLUX has a confident house look and will drift back to it. Two dials, in "
"the order worth trying:\n"
"1. `Flux guidance` (3.5 here, FLUX's default) is the blunt dial. 4–5 makes "
"the style bite harder; past that the subject starts losing to it, and a "
"dark style can go to pure black.\n"
"2. Narrow `sections` on StyleRef Apply to the parts that define this "
"style. For a period or painterly style that is usually "
"`artistic_mediums,references,surface_material,colors` — the sections "
"naming the medium, the era and the finish. Leaving it empty sends every "
"section, and the identity-carrying ones render last, so a long style can "
"read as generic.\n"
"Which sections matter depends on the style: for a geometric or layout-led "
"style keep `shape_language` and `spatial_hierarchy` instead. Queue the "
"Facets template to see what a style actually carries."
)
_FLUX_SETUP_NOTE = (
"FLUX.1 dev (fp8 all-in-one). The checkpoint downloads on first use "
"into checkpoints/ — one ~16 GB file containing the UNET, the T5 text "
"encoder and the VAE.\n"
"Sampler defaults: 20 steps, CFG 1.0, euler/simple. Prompt strength is "
"the FluxGuidance node (3.5), not CFG.\n"
"FLUX takes no negative prompt. It is guidance distilled and runs at "
"CFG 1.0, 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 `flux` target — FLUX reads prose with T5, so the "
"whole style fits instead of being truncated at ~77 tokens the way an "
"SDXL CLIP encoder would.\n\n"
+ _FLUX_STYLE_TIP
)
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(ZIMAGE_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(ZIMAGE_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(ZIMAGE_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. Queue — the FLUX checkpoint downloads on first run.\n"
"2. 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.\n\n"
+ _FLUX_SETUP_NOTE,
title="Quickstart",
)
load = g.place(
"StyleRefLoad",
0,
load_widgets(FLUX_STYLE_REF),
title="Load — swap this slug",
)
checkpoint = _flux_checkpoint(g)
apply_node = g.place(
"StyleRefApply", 1, apply_widgets(DEMO_SUBJECT, "flux")
)
_preview(g, apply_node, 1)
g.link(load, 0, apply_node, 0)
_flux_tail(g, apply_node, checkpoint, col=2)
return g.to_json(models=[MODEL_FLUX])
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(FLUX_STYLE_REF))
checkpoint = _flux_checkpoint(g)
subjects = GRID_SUBJECTS
for index, subject in enumerate(subjects):
apply_node = g.place(
"StyleRefApply", 1, apply_widgets(subject, "flux"), title=f"Subject {index + 1}"
)
g.link(load, 0, apply_node, 0)
_preview(g, apply_node, 1, suffix=f" {index + 1}")
_flux_tail(g, apply_node, checkpoint, col=2)
return g.to_json(models=[MODEL_FLUX])
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. Queue.\n\n" + _FLUX_SETUP_NOTE,
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 = _flux_checkpoint(g)
apply_node = g.place(
"StyleRefApply", 1, apply_widgets(DEMO_SUBJECT, "flux")
)
g.link(load, 0, apply_node, 0)
_preview(g, apply_node, 1)
_flux_tail(g, apply_node, checkpoint, col=2)
return g.to_json(models=[MODEL_FLUX])
_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_reference_images() -> dict[str, Any]:
"""FLUX render + the style's inspiration images side by side."""
g = Graph()
g.note(
0,
"Style prompt and reference images together.\n"
"StyleRef Apply drives the 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" + _FLUX_SETUP_NOTE,
title="FLUX + references",
)
load = g.place(
"StyleRefLoad", 0, load_widgets(FLUX_STYLE_REF), title="Load — a gallery slug"
)
checkpoint = _flux_checkpoint(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, "flux")
)
g.link(load, 0, apply_node, 0)
_preview(g, apply_node, 1)
_flux_tail(g, apply_node, checkpoint, col=2)
return g.to_json(models=[MODEL_FLUX])
def workflow_05_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(FLUX_STYLE_REF),
title="Load — swap this slug",
)
checkpoint = _flux_checkpoint(g)
_facet_board(g, load, col=1)
apply_node = g.place(
"StyleRefApply", 3, apply_widgets(DEMO_SUBJECT, "flux")
)
g.link(load, 0, apply_node, 0)
_preview(g, apply_node, 3)
_flux_tail(g, apply_node, checkpoint, col=4)
return g.to_json(models=[MODEL_FLUX])
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(ZIMAGE_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 FLUX 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-reference-images.json": workflow_04_reference_images,
"05-facets.json": workflow_05_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()