""" 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.()`, 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")