Spaces:
Running
Running
File size: 36,034 Bytes
db59f73 af888c6 db59f73 af888c6 db59f73 af888c6 db59f73 af888c6 db59f73 af888c6 db59f73 af888c6 db59f73 af888c6 db59f73 af888c6 db59f73 af888c6 db59f73 af888c6 db59f73 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 | """
Generate `workflow.json` for Workflow1111.
Hand-writing ~3000 lines of graph JSON is how wiring bugs get in, so the graph
is generated and then *verified* โ see `verify()` at the bottom. The single
most important guarantee: an `fn` operator's input ports are derived from the
bound function's own signature via `inspect`, so port order can never drift
away from the Python argument order (the executor passes `fn` args
positionally, in port order).
python apps/05_workflow1111/build_workflow.py
Re-run after editing `nodes.py`. Node positions come from `layout.json` (the
hand-arranged, overlap-checked layout), so rebuilding preserves the canvas
arrangement instead of resetting it. To adopt a new arrangement, drag nodes in
the canvas and re-snapshot `layout.json` from the saved `workflow.json`.
"""
import inspect
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from gradio.workflow import _is_injected_param # noqa: E402
import nodes as N # noqa: E402
def fn_params(func):
"""A bound function's real input parameters.
Skips gradio's injected parameters (`OAuthToken`, `OAuthProfile`,
`Request`) โ gradio supplies those itself, so they must not become ports.
"""
hints = getattr(func, "__annotations__", {})
try:
from typing import get_type_hints
hints = get_type_hints(func)
except Exception:
pass
return [p for p in inspect.signature(func).parameters
if not _is_injected_param(hints.get(p))]
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "workflow.json")
LAYOUT = os.path.join(HERE, "layout.json")
# Sample images ship in the Space repo and are referenced by their public Hub
# URL. That is the one default shape that satisfies everything at once: the
# canvas only renders a reference default that carries a `url` key (it strips
# `path` from graph defaults for safety), while `call_model`/`call_space` need
# something a remote provider can actually fetch โ which a relative
# `/gradio_api/file=` URL is not, but an absolute https one is. The same URLs
# therefore work identically when running locally and on the Space.
SPACE_ID = os.environ.get("WORKFLOW1111_SPACE", "ysharma/Workflow1111")
SAMPLE_BASE = f"https://huggingface.co/spaces/{SPACE_ID}/resolve/main/samples"
def sample(filename):
return {"url": f"{SAMPLE_BASE}/{filename}"}
# Verified working on HF Inference Providers โ see the probe results recorded
# in the README. Swapping these is the main "model checkpoint" knob.
T2I_MODEL = "black-forest-labs/FLUX.1-schnell"
T2I_QUALITY_MODEL = "black-forest-labs/FLUX.1-dev"
EDIT_MODEL = "black-forest-labs/FLUX.1-Kontext-dev"
LLM_MODEL = "Qwen/Qwen3-4B-Instruct-2507"
VLM_MODEL = "Qwen/Qwen2.5-VL-72B-Instruct"
DETECT_MODEL = "facebook/detr-resnet-50"
CLASSIFY_MODEL = "google/vit-base-patch16-224"
RMBG_SPACE = "briaai/BRIA-RMBG-2.0"
UPSCALE_SPACE = "gokaygokay/AuraSR-v2"
references, operators, subjects, edges = [], [], [], []
COL = [60, 420, 800, 1180, 1560, 1940] # x positions by pipeline stage
def _size(n_in, n_out, width, base=64, per_port=30):
return width, base + per_port * max(n_in, n_out, 1)
def ref(node_id, label, port_type, x, y, default=None, width=250):
"""A reference node โ a free input. These become the API parameters."""
references.append({
"id": node_id, "role": "reference", "label": label, "asset_type": port_type,
"inputs": [{"id": "in", "label": label, "type": port_type}],
"outputs": [{"id": "out", "label": label, "type": port_type}],
"data": {} if default is None else {"out": default},
"x": x, "y": y, "width": width,
"height": 200 if port_type in ("image", "audio", "video") else 96,
})
return node_id
def fn(node_id, fn_name, x, y, *, label=None, types=None, data=None,
required=(), outputs=None, width=290):
"""An `fn` operator. Input ports are generated from the bound function's
signature, so the positional call order is correct by construction."""
func = N.BIND[fn_name]
params = fn_params(func)
types = types or {}
data = data or {}
inputs = [{
"id": f"in_{p}",
"label": p,
"type": types.get(p, "text"),
**({"required": True} if p in required else {}),
} for p in params]
outs = outputs or [("out_0", "output", "text")]
out_ports = [{"id": oid, "label": olabel, "type": otype, "output_index": i}
for i, (oid, olabel, otype) in enumerate(outs)]
w, h = _size(len(inputs), len(out_ports), width)
operators.append({
"id": node_id, "role": "operator", "kind": "fn", "fn": fn_name,
"label": label or fn_name,
"inputs": inputs, "outputs": out_ports,
"data": {f"in_{k}": v for k, v in data.items()},
"x": x, "y": y, "width": w, "height": h,
})
return node_id
def model(node_id, model_id, endpoint, pipeline_tag, x, y, *, label=None,
inputs=(), outputs=None, data=None, width=290):
"""A `model` operator (HF Inference Providers).
Input port **ids** are forwarded verbatim as keyword arguments to
`InferenceClient.<endpoint>()`, which is what gives txt2img its real
negative-prompt / steps / CFG / seed / size controls.
"""
in_ports = [{"id": pid, "label": plabel, "type": ptype,
**({"required": True} if req else {})}
for pid, plabel, ptype, req in inputs]
outs = outputs or [("out_0", "Image", "image")]
out_ports = [{"id": oid, "label": olabel, "type": otype, "output_index": i}
for i, (oid, olabel, otype) in enumerate(outs)]
w, h = _size(len(in_ports), len(out_ports), width)
operators.append({
"id": node_id, "role": "operator", "kind": "model",
"model_id": model_id, "pipeline_tag": pipeline_tag, "endpoint": endpoint,
"label": label or model_id.split("/")[-1],
"inputs": in_ports, "outputs": out_ports, "data": data or {},
"x": x, "y": y, "width": w, "height": h,
})
return node_id
def space(node_id, space_id, endpoint, x, y, *, label=None, inputs=(),
outputs=None, data=None, width=290):
"""A `space` operator. Inputs are passed **positionally**, in port order."""
in_ports = [{"id": pid, "label": plabel, "type": ptype,
**({"required": True} if req else {})}
for pid, plabel, ptype, req in inputs]
out_ports = [{"id": oid, "label": olabel, "type": otype, "output_index": idx}
for oid, olabel, otype, idx in (outputs or [])]
w, h = _size(len(in_ports), len(out_ports), width)
operators.append({
"id": node_id, "role": "operator", "kind": "space",
"space_id": space_id, "endpoint": endpoint,
"label": label or space_id.split("/")[-1],
"inputs": in_ports, "outputs": out_ports, "data": data or {},
"x": x, "y": y, "width": w, "height": h,
})
return node_id
def out(node_id, label, port_type, x, y, width=300):
"""A subject node โ a workflow output, and an API endpoint result."""
subjects.append({
"id": node_id, "role": "subject", "label": label, "asset_type": port_type,
"inputs": [{"id": "in", "label": label, "type": port_type}],
"outputs": [{"id": "out", "label": label, "type": port_type}],
"data": {},
"x": x, "y": y, "width": width,
"height": 260 if port_type == "image" else 190,
})
return node_id
def link(a, b):
"""Wire "node.port" โ "node.port"."""
fnode, fport = a.split(".")
tnode, tport = b.split(".")
edges.append({
"id": f"e{len(edges) + 1}",
"from_node_id": fnode, "from_port_id": fport,
"to_node_id": tnode, "to_port_id": tport,
"type": None, # filled in by verify() from the source port
})
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# 1 ยท txt2img โ the flagship pipeline
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Y = 60
ref("ref_prompt", "Prompt", "text", COL[0], Y,
"a red fox standing in a snowy pine forest, looking at the camera")
ref("ref_negative", "Negative prompt", "text", COL[0], Y + 120, "")
ref("ref_style", "Style preset", "text", COL[0], Y + 240, "Cinematic")
fn("op_style", "apply_style", COL[1], Y, label="โ Prompt builder",
required=("prompt",),
data={"extra_tags": "", "quality_boost": True},
outputs=[("out_0", "prompt", "text")])
fn("op_negative", "build_negative", COL[1], Y + 190, label="โ Negative builder",
types={"use_base": "boolean", "safety_filter": "boolean"},
data={"negative": "", "use_base": True, "safety_filter": True},
outputs=[("out_0", "negative", "text")])
fn("op_sampler", "sampler_settings", COL[1], Y + 380, label="โก Sampler",
types={"steps": "number", "cfg_scale": "number", "seed": "number",
"width": "number", "height": "number"},
data={"steps": 4, "cfg_scale": 1.0, "seed": -1,
"aspect": "1:1 Square", "width": 1024, "height": 1024},
outputs=[("out_steps", "steps", "number"), ("out_cfg", "cfg", "number"),
("out_seed", "seed", "number"), ("out_width", "width", "number"),
("out_height", "height", "number")])
# txt2img is an `fn` node, not a `model` node, on purpose: the canvas rewrites
# a model node's ports to the endpoint's canonical schema (just `prompt` for
# text_to_image), which silently discarded the negative prompt, steps, CFG,
# seed and size. `fn` ports are left alone, so the control surface survives.
fn("op_txt2img", "txt2img", COL[2], Y, label="โข txt2img ยท FLUX.1-schnell",
types={"steps": "number", "cfg_scale": "number", "seed": "number",
"width": "number", "height": "number"},
required=("prompt",),
data={"negative_prompt": "", "steps": 4, "cfg_scale": 1.0, "seed": -1,
"width": 1024, "height": 1024, "model_id": T2I_MODEL},
outputs=[("out_0", "image", "image")])
fn("op_geninfo", "generation_info", COL[2], Y + 300, label="โฃ Generation params",
types={"steps": "number", "cfg_scale": "number", "seed": "number",
"width": "number", "height": "number"},
data={"model_id": T2I_MODEL},
outputs=[("out_0", "parameters", "text")])
fn("op_post", "postprocess", COL[3], Y, label="โค Post-processing",
types={"image": "image", "upscale": "number", "sharpen": "number",
"saturation": "number", "contrast": "number", "brightness": "number",
"vignette": "number", "grain": "number", "border": "number"},
required=("image",),
data={"upscale": 1.0, "upscale_method": "Lanczos", "sharpen": 0.35,
"saturation": 1.05, "contrast": 1.02, "brightness": 1.0,
"vignette": 0.12, "grain": 0.04, "border": 0.0, "watermark": ""},
outputs=[("out_0", "image", "image")])
out("sub_image", "๐ผ Image", "image", COL[4], Y)
out("sub_params", "๐ Generation parameters", "text", COL[4], Y + 300)
link("ref_prompt.out", "op_style.in_prompt")
link("ref_style.out", "op_style.in_style")
link("ref_style.out", "op_negative.in_style")
link("ref_negative.out", "op_negative.in_negative")
link("op_style.out_0", "op_txt2img.in_prompt")
link("op_negative.out_0", "op_txt2img.in_negative_prompt")
link("op_sampler.out_steps", "op_txt2img.in_steps")
link("op_sampler.out_cfg", "op_txt2img.in_cfg_scale")
link("op_sampler.out_seed", "op_txt2img.in_seed")
link("op_sampler.out_width", "op_txt2img.in_width")
link("op_sampler.out_height", "op_txt2img.in_height")
link("op_style.out_0", "op_geninfo.in_prompt")
link("op_negative.out_0", "op_geninfo.in_negative")
link("op_sampler.out_steps", "op_geninfo.in_steps")
link("op_sampler.out_cfg", "op_geninfo.in_cfg_scale")
link("op_sampler.out_seed", "op_geninfo.in_seed")
link("op_sampler.out_width", "op_geninfo.in_width")
link("op_sampler.out_height", "op_geninfo.in_height")
link("op_txt2img.out_0", "op_post.in_image")
link("op_geninfo.out_0", "op_post.in_embed_info")
link("op_post.out_0", "sub_image.in")
link("op_geninfo.out_0", "sub_params.in")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# 2 ยท Hires fix โ upscale the txt2img result, then refine it with img2img
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Y = 700
ref("ref_hires_instruction", "Hires refine instruction", "text", COL[2], Y,
"enhance fine detail and micro-texture, keep the composition identical")
fn("op_hires_prep", "prep_image", COL[3], Y + 130, label="โฅ Hires prep",
types={"image": "image", "max_side": "number", "strip_alpha": "boolean"},
required=("image",),
data={"max_side": 1024, "mode": "Fit", "strip_alpha": True},
outputs=[("out_0", "image", "image")])
model("op_hires", EDIT_MODEL, "image_to_image", "image-to-image", COL[4], Y + 130,
label="โฆ Hires fix ยท FLUX.1-Kontext",
inputs=[("image", "image", "image", True),
("prompt", "prompt", "text", True)])
out("sub_hires", "โจ Hires image", "image", COL[5], Y + 130)
link("op_post.out_0", "op_hires_prep.in_image")
link("op_hires_prep.out_0", "op_hires.image")
link("ref_hires_instruction.out", "op_hires.prompt")
link("op_hires.out_0", "sub_hires.in")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# 3 ยท img2img โ edit an uploaded image by instruction
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Y = 1080
ref("ref_init_image", "Init image", "image", COL[0], Y, sample("init_image.jpg"))
ref("ref_edit_instruction", "Edit instruction", "text", COL[0], Y + 240,
"make it a snowy winter scene at golden hour")
fn("op_i2i_prep", "prep_image", COL[1], Y, label="โ Prepare init image",
types={"image": "image", "max_side": "number", "strip_alpha": "boolean"},
required=("image",),
data={"max_side": 1024, "mode": "Fit", "strip_alpha": True},
outputs=[("out_0", "image", "image")])
model("op_i2i", EDIT_MODEL, "image_to_image", "image-to-image", COL[2], Y,
label="โก img2img ยท FLUX.1-Kontext",
inputs=[("image", "image", "image", True),
("prompt", "prompt", "text", True)])
fn("op_i2i_post", "postprocess", COL[3], Y, label="โข Post-processing",
types={"image": "image", "upscale": "number", "sharpen": "number",
"saturation": "number", "contrast": "number", "brightness": "number",
"vignette": "number", "grain": "number", "border": "number"},
required=("image",),
data={"upscale": 1.0, "upscale_method": "Lanczos", "sharpen": 0.3,
"saturation": 1.0, "contrast": 1.0, "brightness": 1.0,
"vignette": 0.0, "grain": 0.0, "border": 0.0, "watermark": ""},
outputs=[("out_0", "image", "image")])
out("sub_i2i", "๐จ Edited image", "image", COL[4], Y)
link("ref_init_image.out", "op_i2i_prep.in_image")
link("op_i2i_prep.out_0", "op_i2i.image")
link("ref_edit_instruction.out", "op_i2i.prompt")
link("op_i2i.out_0", "op_i2i_post.in_image")
link("op_i2i_post.out_0", "sub_i2i.in")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# 4 ยท Prompt magic โ an LLM writes the prompt for you
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Y = 1450
ref("ref_idea", "Rough idea", "text", COL[0], Y, "a lighthouse in a storm")
fn("op_magic", "magic_instruction", COL[1], Y, label="โ Build instruction",
required=("idea",),
data={"target_style": "Cinematic", "verbosity": "Detailed"},
outputs=[("out_0", "instruction", "text")])
fn("op_llm", "chat_llm", COL[2], Y, label="โก Prompt LLM ยท Qwen3-4B",
types={"max_tokens": "number"}, required=("prompt",),
data={"model_id": LLM_MODEL, "max_tokens": 512},
outputs=[("out_0", "Text", "text")])
fn("op_clean_magic", "clean_prompt", COL[3], Y, label="โข Tidy up",
types={"max_tags": "number"}, required=("raw",),
data={"max_tags": 40},
outputs=[("out_0", "prompt", "text")])
out("sub_magic", "๐ช Generated prompt", "text", COL[4], Y)
link("ref_idea.out", "op_magic.in_idea")
link("op_magic.out_0", "op_llm.in_prompt")
link("op_llm.out_0", "op_clean_magic.in_raw")
link("op_clean_magic.out_0", "sub_magic.in")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# 5 ยท Interrogate โ recover a prompt (and labels) from an image
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Y = 1780
ref("ref_interrogate_image", "Image to interrogate", "image", COL[0], Y,
sample("interrogate.jpg"))
fn("op_vlm", "interrogate", COL[1], Y, label="โ Interrogate ยท Qwen2.5-VL",
types={"image": "image", "max_tokens": "number"}, required=("image",),
data={"instruction": "Describe this image as a Stable Diffusion prompt: "
"comma-separated visual tags only, covering subject, setting, "
"composition, lighting, colour and medium. No sentences, "
"no preamble.",
"model_id": VLM_MODEL, "max_tokens": 512},
outputs=[("out_0", "Text", "text")])
fn("op_clean_interrogate", "clean_prompt", COL[2], Y, label="โก Tidy up",
types={"max_tags": "number"}, required=("raw",),
data={"max_tags": 45},
outputs=[("out_0", "prompt", "text")])
# `fn`, not `model`: a `json` output port reaches the canvas as the literal
# string "[object Object]" (JS String(obj) instead of JSON.stringify), so the
# labels never survive the edge. Text ports carrying JSON do.
fn("op_classify", "classify_image", COL[1], Y + 260, label="โข Classify ยท ViT",
types={"image": "image"}, required=("image",),
data={"model_id": CLASSIFY_MODEL},
outputs=[("out_0", "labels", "text")])
fn("op_labels", "top_labels", COL[2], Y + 260, label="โฃ Rank labels",
types={"labels": "text", "top_k": "number", "min_score": "number"},
required=("labels",),
data={"top_k": 5, "min_score": 0.01},
outputs=[("out_0", "table", "text"), ("out_1", "rows", "text")])
out("sub_interrogated", "๐ Recovered prompt", "text", COL[3], Y)
out("sub_labels", "๐ท Classification", "text", COL[3], Y + 260)
link("ref_interrogate_image.out", "op_vlm.in_image")
link("op_vlm.out_0", "op_clean_interrogate.in_raw")
link("op_clean_interrogate.out_0", "sub_interrogated.in")
link("ref_interrogate_image.out", "op_classify.in_image")
link("op_classify.out_0", "op_labels.in_labels")
link("op_labels.out_0", "sub_labels.in")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# 6 ยท Detect & mask โ object detection into an inpainting mask
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Y = 2200
ref("ref_detect_image", "Image to analyse", "image", COL[0], Y, sample("detect.jpg"))
fn("op_detect", "detect_objects", COL[1], Y, label="โ Detect ยท DETR",
types={"image": "image", "min_score": "number"}, required=("image",),
data={"model_id": DETECT_MODEL, "min_score": 0.0},
outputs=[("out_0", "detections", "text")])
fn("op_draw", "draw_detections", COL[2], Y, label="โก Annotate",
types={"image": "image", "detections": "text", "min_score": "number",
"show_labels": "boolean"},
required=("image", "detections"),
data={"min_score": 0.5, "show_labels": True},
outputs=[("out_0", "image", "image"), ("out_1", "summary", "text")])
fn("op_mask", "mask_from_detections", COL[2], Y + 300, label="โข Build inpaint mask",
types={"image": "image", "detections": "text", "min_score": "number",
"feather": "number", "invert": "boolean", "preview": "boolean"},
required=("image", "detections"),
data={"label_filter": "", "min_score": 0.5, "feather": 8,
"invert": False, "preview": False},
outputs=[("out_0", "mask", "image")])
out("sub_detected", "๐ฆ Detected objects", "image", COL[3], Y)
out("sub_detect_summary", "๐ Detection summary", "text", COL[3], Y + 300)
out("sub_mask", "๐ญ Inpaint mask", "image", COL[4], Y + 300)
link("ref_detect_image.out", "op_detect.in_image")
link("ref_detect_image.out", "op_draw.in_image")
link("op_detect.out_0", "op_draw.in_detections")
link("ref_detect_image.out", "op_mask.in_image")
link("op_detect.out_0", "op_mask.in_detections")
link("op_draw.out_0", "sub_detected.in")
link("op_draw.out_1", "sub_detect_summary.in")
link("op_mask.out_0", "sub_mask.in")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# 7 ยท Prompt matrix โ four variants rendered in parallel into an X/Y grid
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Y = 2700
ref("ref_matrix_base", "Matrix base prompt", "text", COL[0], Y, "a lone tree on a hill")
ref("ref_matrix_variants", "Variants (| separated)", "text", COL[0], Y + 120,
"at sunrise | in a thunderstorm | under the milky way | in autumn fog")
fn("op_matrix", "prompt_matrix", COL[1], Y, label="โ Expand matrix",
required=("base_prompt",),
data={"shared_tags": "cinematic, highly detailed, dramatic lighting"},
outputs=[("out_p1", "prompt 1", "text"), ("out_p2", "prompt 2", "text"),
("out_p3", "prompt 3", "text"), ("out_p4", "prompt 4", "text"),
("out_labels", "labels", "text")])
for i in range(4):
fn(f"op_grid_{i + 1}", "txt2img", COL[2], Y + i * 250,
label=f"โก Render {i + 1}",
types={"steps": "number", "cfg_scale": "number", "seed": "number",
"width": "number", "height": "number"},
required=("prompt",),
data={"negative_prompt": "", "steps": 4, "cfg_scale": 1.0,
"seed": 1000 + i * 111, "width": 768, "height": 768,
"model_id": T2I_MODEL},
outputs=[("out_0", "image", "image")])
link(f"op_matrix.out_p{i + 1}", f"op_grid_{i + 1}.in_prompt")
link(f"op_grid_{i + 1}.out_0", f"op_sheet.in_image_{i + 1}")
fn("op_sheet", "contact_sheet", COL[3], Y + 320, label="โข Contact sheet",
types={"image_1": "image", "image_2": "image", "image_3": "image",
"image_4": "image", "columns": "number", "gap": "number"},
data={"columns": 2, "gap": 16, "title": "Prompt matrix"},
outputs=[("out_0", "grid", "image")])
out("sub_grid", "๐งฉ X/Y grid", "image", COL[4], Y + 320)
link("ref_matrix_base.out", "op_matrix.in_base_prompt")
link("ref_matrix_variants.out", "op_matrix.in_variations")
link("op_matrix.out_labels", "op_sheet.in_labels")
link("op_sheet.out_0", "sub_grid.in")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# 8 ยท Extras โ one upload, three post-processors (two local, one remote)
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Y = 3560
ref("ref_extras_image", "Extras input image", "image", COL[0], Y, sample("extras.jpg"))
fn("op_extras", "extras_upscale", COL[1], Y, label="โ Upscale (local, instant)",
types={"image": "image", "factor": "number", "sharpen": "number",
"denoise": "boolean", "restore_contrast": "boolean"},
required=("image",),
data={"factor": 2.0, "method": "Lanczos", "sharpen": 0.45,
"denoise": False, "restore_contrast": True},
outputs=[("out_0", "image", "image"), ("out_1", "report", "text")])
space("op_aurasr", UPSCALE_SPACE, "/process_image", COL[1], Y + 300,
label="โก Upscale ร4 (AuraSR GAN)",
inputs=[("input_image", "image", "image", True)],
outputs=[("out_0", "Upscaled", "image", 1)])
space("op_rmbg", RMBG_SPACE, "/image", COL[1], Y + 500,
label="โข Remove background (BRIA)",
inputs=[("image", "image", "image", True)],
outputs=[("out_0", "Cutout", "image", 1)])
out("sub_upscaled", "๐ Upscaled (local)", "image", COL[2], Y)
out("sub_upscale_report", "๐ Upscale report", "text", COL[3], Y)
out("sub_aurasr", "๐ Upscaled ร4 (GAN)", "image", COL[2], Y + 300)
out("sub_cutout", "โ Background removed", "image", COL[2], Y + 620)
link("ref_extras_image.out", "op_extras.in_image")
link("op_extras.out_0", "sub_upscaled.in")
link("op_extras.out_1", "sub_upscale_report.in")
link("ref_extras_image.out", "op_aurasr.input_image")
link("op_aurasr.out_0", "sub_aurasr.in")
link("ref_extras_image.out", "op_rmbg.image")
link("op_rmbg.out_0", "sub_cutout.in")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# 9 ยท ControlNet-style annotator previews (local)
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Y = 4340
ref("ref_control_image", "Annotator input", "image", COL[0], Y, sample("control.jpg"))
fn("op_control", "controlnet_preprocess", COL[1], Y, label="Annotator",
types={"image": "image", "low_threshold": "number", "high_threshold": "number",
"invert": "boolean", "blur": "number"},
required=("image",),
data={"mode": "Canny edges", "low_threshold": 60, "high_threshold": 160,
"invert": False, "blur": 0.0},
outputs=[("out_0", "map", "image")])
out("sub_control", "๐ธ Annotator map", "image", COL[2], Y)
link("ref_control_image.out", "op_control.in_image")
link("op_control.out_0", "sub_control.in")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# 10 ยท PNG Info โ read generation parameters back out of a file
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Y = 4700
ref("ref_pnginfo_image", "PNG to inspect", "image", COL[0], Y,
sample("with_parameters.png"))
fn("op_pnginfo", "png_info", COL[1], Y, label="Read PNG metadata",
types={"image": "image"}, required=("image",),
outputs=[("out_0", "report", "text"), ("out_1", "fields", "text")])
out("sub_png_report", "๐งพ PNG info", "text", COL[2], Y)
out("sub_png_fields", "๐งฎ Parsed fields", "text", COL[3], Y)
link("ref_pnginfo_image.out", "op_pnginfo.in_image")
link("op_pnginfo.out_0", "sub_png_report.in")
link("op_pnginfo.out_1", "sub_png_fields.in")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Verification โ catch wiring mistakes here, not at runtime
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
from gradio.workflow import _INFERENCE_ENDPOINT_SCHEMAS # noqa: E402
def verify():
problems = []
nodes = references + operators + subjects
by_id = {}
for n in nodes:
if n["id"] in by_id:
problems.append(f"duplicate node id: {n['id']}")
by_id[n["id"]] = n
# fn nodes: ports must mirror the Python signature exactly (positional call)
for n in operators:
if n["kind"] != "fn":
continue
func = N.BIND.get(n["fn"])
if func is None:
problems.append(f"{n['id']}: fn '{n['fn']}' is not in nodes.BIND")
continue
params = fn_params(func)
labels = [p["label"] for p in n["inputs"]]
if labels != params:
problems.append(f"{n['id']}: port order {labels} != signature {params}")
for key in n["data"]:
if key not in {p["id"] for p in n["inputs"]}:
problems.append(f"{n['id']}: data key '{key}' is not an input port")
# model nodes: their ports must match the endpoint's canonical schema
# EXACTLY. The canvas rewrites any model node whose ports differ, silently
# dropping extra inputs (and orphaning the edges into them) the first time
# the graph is opened in a browser. Anything needing a richer control
# surface than the schema allows has to be an `fn` node calling
# InferenceClient itself โ that is why `txt2img` is one.
for n in operators:
if n["kind"] != "model":
continue
schema = _INFERENCE_ENDPOINT_SCHEMAS.get(n["endpoint"])
if schema is None:
problems.append(f"{n['id']}: unknown endpoint '{n['endpoint']}'")
continue
expected = [p["id"] for p in schema["inputs"]]
actual = [p["id"] for p in n["inputs"]]
if actual != expected:
problems.append(
f"{n['id']}: model ports {actual} != {n['endpoint']} schema "
f"{expected} โ the canvas would rewrite this node")
# edges: endpoints must exist, and types must agree
port_type = {}
for n in nodes:
for p in n.get("inputs", []):
port_type[(n["id"], p["id"], "in")] = p["type"]
for p in n.get("outputs", []):
port_type[(n["id"], p["id"], "out")] = p["type"]
fed = set()
for e in edges:
src = (e["from_node_id"], e["from_port_id"], "out")
dst = (e["to_node_id"], e["to_port_id"], "in")
if src not in port_type:
problems.append(f"edge {e['id']}: no output port {src[0]}.{src[1]}")
continue
if dst not in port_type:
problems.append(f"edge {e['id']}: no input port {dst[0]}.{dst[1]}")
continue
if dst in fed:
problems.append(f"edge {e['id']}: {dst[0]}.{dst[1]} has two incoming edges")
fed.add(dst)
stype, dtype = port_type[src], port_type[dst]
e["type"] = stype
compatible = stype == dtype or "text" in (stype, dtype) and {stype, dtype} <= {
"text", "number", "boolean", "json"}
if not compatible:
problems.append(
f"edge {e['id']}: type mismatch {src[0]}.{src[1]}({stype}) "
f"โ {dst[0]}.{dst[1]}({dtype})")
# every subject must be fed, and every required input must be satisfied
for s in subjects:
if (s["id"], "in", "in") not in fed:
problems.append(f"subject {s['id']} has no incoming edge")
for n in operators:
for p in n["inputs"]:
if not p.get("required"):
continue
if (n["id"], p["id"], "in") not in fed and p["id"] not in n["data"]:
problems.append(
f"{n['id']}: required input '{p['id']}' is neither wired nor defaulted")
# nothing may be orphaned
touched = {e["from_node_id"] for e in edges} | {e["to_node_id"] for e in edges}
for n in nodes:
if n["id"] not in touched:
problems.append(f"orphan node: {n['id']}")
return problems
if __name__ == "__main__":
issues = verify()
if issues:
print(f"REFUSING TO WRITE โ {len(issues)} problem(s):")
for p in issues:
print(" โข", p)
sys.exit(1)
# Apply the curated layout. Positions in `layout.json` are the hand-arranged
# ones (dragged in the canvas, then overlap-checked); the x/y computed above
# are only a fallback for nodes the layout doesn't know about yet.
placed = 0
if os.path.exists(LAYOUT):
with open(LAYOUT, encoding="utf-8") as f:
layout = json.load(f)
for node in references + operators + subjects:
pos = layout.get(node["id"])
if pos:
node["x"], node["y"] = pos["x"], pos["y"]
placed += 1
missing = [n["id"] for n in references + operators + subjects
if n["id"] not in layout]
if missing:
print(f" note: {len(missing)} node(s) not in layout.json, using "
f"generated positions: {', '.join(missing[:6])}")
graph = {
"schema_version": "2",
"name": "Workflow1111 ยท Diffusion Studio",
"references": references,
"operators": operators,
"subjects": subjects,
"edges": edges,
}
with open(OUT, "w", encoding="utf-8") as f:
json.dump(graph, f, indent=2, ensure_ascii=False)
kinds = {}
for o in operators:
kinds[o["kind"]] = kinds.get(o["kind"], 0) + 1
print(f"wrote {os.path.relpath(OUT, os.getcwd())}")
print(f" {len(references)} references, {len(operators)} operators "
f"({', '.join(f'{v} {k}' for k, v in sorted(kinds.items()))}), "
f"{len(subjects)} subjects, {len(edges)} edges")
print(f" {placed} node positions applied from layout.json")
|