Spaces:
Running
A newer version of the Gradio SDK is available: 6.24.0
title: Workflow1111 Diffusion Studio
emoji: π¨
colorFrom: indigo
colorTo: purple
sdk: gradio
sdk_version: 6.22.0
app_file: app.py
pinned: false
license: mit
hf_oauth: true
hf_oauth_scopes:
- inference-api
short_description: Automatic1111-style studio on one gr.Workflow canvas
Sign in with Hugging Face (button at the top right) before running anything. This Space carries no token of its own β every
model,spaceand inference-callingfnnode runs on your token and your own inference quota.
05 Β· Workflow1111 β a Diffusion Studio built from gr.Workflow
An Automatic1111-shaped image studio expressed as one canvas graph instead
of a tabbed UI: 64 nodes, 10 pipelines, 18 independently runnable outputs,
all inside a single gr.Workflow.
pip install -r apps/05_workflow1111/requirements.txt
hf auth login # or: set HF_TOKEN=hf_xxx
python apps/05_workflow1111/app.py
Why it isn't tabs
gr.Workflow raises if you construct it inside a gr.Blocks context:
if Context.root_block is not None:
raise ValueError("gr.Workflow cannot be created inside another gr.Blocks context.")
So an A1111 clone genuinely cannot be a gr.Tabs layout here β the graph is
the UI. The analogue of a tab is a subject group: a connected cluster of
outputs, which gradio also publishes as its own REST endpoint. Ten pipelines sit
side by side on one canvas, and you run whichever output you want.
What's on the canvas
| # | Pipeline | A1111 equivalent | Nodes |
|---|---|---|---|
| 1 | txt2img | txt2img tab | prompt builder β negative builder β sampler β FLUX.1-schnell β post-processing β image + params |
| 2 | Hires fix | Hires. fix | txt2img result β prep β FLUX.1-Kontext re-render |
| 3 | img2img | img2img tab | upload β prep β FLUX.1-Kontext edit β post-processing |
| 4 | Prompt magic | β | idea β instruction β Qwen3-4B β cleanup |
| 5 | Interrogate | CLIP interrogate | image β Qwen2.5-VL β prompt; + ViT classification |
| 6 | Detect & mask | inpaint masking | DETR β annotated boxes β feathered inpaint mask |
| 7 | Prompt matrix | X/Y/Z plot | 4 variants β 4 parallel renders β contact sheet |
| 8 | Extras | Extras tab | local Lanczos upscale Β· AuraSR Γ4 Β· background removal |
| 9 | Annotators | ControlNet preprocessors | Canny Β· line art Β· sketch Β· luma-depth Β· posterize Β· threshold |
| 10 | PNG Info | PNG Info tab | read generation parameters back out of a file |
The txt2img node has the real control surface β negative prompt, sampling
steps, CFG scale, seed (with -1 = random), width/height with aspect presets,
and a model_id box that acts as the checkpoint selector β not just a prompt
box. Keeping that surface intact in the browser is exactly what gotcha #1 below
is about.
The generation-parameters loop closes. postprocess writes the A1111
parameter block into the PNG's parameters text chunk; the PNG Info pipeline
parses it back out. Images this app makes round-trip through a real A1111
install too.
Architecture
14 references β 32 operators β 18 subjects 73 edges
ββ 28 fn 19 pure-local Β· 9 calling InferenceClient
ββ 2 model HF Inference Providers
ββ 2 space Gradio Spaces on the Hub
19 of the 28 fn nodes are pure local Pillow/numpy β all the prompt logic,
post-processing, annotators, masking, grid composition and metadata parsing β
so most of the app keeps working with no token, no quota and no network. 13
nodes in total leave the machine.
Files
| File | What it is |
|---|---|
app.py |
Entry point β 12 lines of actual wiring |
nodes.py |
The 21 bound functions (the fn node library) |
build_workflow.py |
Generates + verifies workflow.json |
workflow.json |
The committed graph |
test_nodes.py |
53 offline unit tests (~2s) |
test_pipelines.py |
Runs all 18 outputs through the real WorkflowExecutor |
test_api.py |
Drives the 9 generated REST endpoints against a running app |
make_samples.py |
Regenerates the shipped sample images |
deploy_space.py |
Stages + uploads the Space |
layout.json |
The curated node positions |
samples/ |
Sample images used as reference-node defaults |
build_workflow.py derives each fn node's input ports from the bound
function's own signature via inspect, so port order can never drift from
the Python argument order β the executor passes fn arguments positionally,
in port order, and that mismatch is the easiest bug to introduce by hand. It
then refuses to write the file unless every edge resolves, every type matches,
every required input is wired or defaulted, no input has two incoming edges,
and no node is orphaned.
Six gotchas this app is built around
All six were found by probing gradio 6.22.0 / huggingface_hub 1.26.0 directly, not from the docs. They are the difference between "renders on the canvas" and "actually runs".
1. The canvas rewrites model node ports β silently
This is the big one, and it is invisible until you open the graph in a browser.
The canvas normalizes every model node's input ports to the endpoint's
canonical schema in _INFERENCE_ENDPOINT_SCHEMAS, saves the result back over
workflow.json, and leaves the now-dangling edges in place.
text_to_image's schema is just ["prompt"]. So a txt2img node carrying
negative_prompt, num_inference_steps, guidance_scale, seed, width and
height β which works perfectly through the headless executor and the REST API
β loses all six the instant a browser loads it. No error, no warning; the image
just quietly ignores every setting. chat_completion normalizes to
["image", "text"], which had the same effect on the prompt LLM (it started
replying "Hello! It seems like your message might be missing something").
The fix is to stop using model nodes wherever the control surface is richer
than the schema: txt2img, chat_llm and interrogate are fn nodes that
call InferenceClient themselves. fn ports are never rewritten. The two
remaining model nodes (image_to_image) have ports exactly equal to their
schema, and
build_workflow.py now refuses to build if that ever stops being true.
A useful side effect: an fn node can validate. interrogate requires its
image, because the model node version cheerfully described an image it was
never given β a fabricated result that looked entirely successful.
2. Image ports do not chain uniformly
model nodes emit {"path", "url": "/gradio_api/file=<abs path>", "is_file": true},
and _img_url() prefers the url key β which means nothing to a remote
provider:
| chain | works? |
|---|---|
model image β another model's image_to_image |
β File not found at \gradio_api\file=... |
data: URI β model task endpoint |
β |
data: URI β space |
β call_space only calls handle_file on dicts |
{"path": p} (no url) β model or space |
β |
model image β chat_completion VLM |
β
(_chat_image_url strips the prefix) |
| uploaded reference β space or model | β |
So: fn nodes emit {"path", "url"} carrying a real file and a data:
URI (see gotcha #5), prep_image sits between any two model nodes, and
space nodes only ever take an uploaded image.
3. Several obvious backends simply don't work
Probed live against this account's enabled providers:
guidance_scale=0on FLUX β 422, fal-ai requires>= 1. The sampler node clamps it.stable-diffusion-3.5-large-turboβ 504 after ~120s.Salesforce/blip-image-captioning-large,Qwen2.5-VL-7B,Llama-3.2-*,Mistral-7B-Instruct-v0.3β no enabled provider.- depth-estimation is dead:
InferenceClienthas nodepth_estimationmethod, and the positional fallback targetsapi-inference.huggingface.co, whose DNS no longer resolves. That's why the annotator's depth mode is an honest luminance approximation rather than a monocular depth model β the edge modes are the genuine article.
Working and fast: FLUX.1-schnell (3s), FLUX.1-dev (3.5s), FLUX.1-Kontext-dev
(6s), Qwen3-4B-Instruct (1.2s), Qwen2.5-VL-72B (7s), DETR + ViT (5s).
4. ImageSlider outputs are tuples
Both Spaces return a before/after pair, so the cutout / upscaled result is at
output_index: 1, verified by calling them rather than reading their docs.
5. One image value has to satisfy four different consumers
An fn node's image output is consumed four ways, and they disagree:
| consumer | wants |
|---|---|
REST endpoint (gr.Image component) |
a real file path β a data: URI gets treated as a filename and joined to the CWD, raising OSError: [Errno 22] |
| canvas | a url it can display |
chained model node (_img_url) |
something a remote provider can fetch |
chained fn node (_load_image) |
anything, prefers path |
So _emit returns both: {"path": <temp file>, "url": <data: URI>}.
_from_output takes path first (endpoint happy), the frontend and _img_url
take url first (canvas and providers happy).
6. A json port silently destroys its value in the canvas
The canvas serializes a json-typed port with JavaScript's String(obj)
instead of JSON.stringify, so the receiving node gets the literal six-word
string "[object Object]". Everything downstream then sees no data, with no
error anywhere:
- DETR detections reached
draw_detectionsas"[object Object]"β zero boxes β an annotated image identical to the input, andmask_from_detectionsfailing with "No detections matched" at everymin_score. - ViT labels reached
top_labelsthe same way β "No labels above the score threshold". png_info's field dict reached its output node as"[object Object]".
Both the executor and the REST API handle json ports perfectly, so this is
invisible to test_pipelines.py and test_api.py β only the canvas is
affected. The graph therefore contains no json ports at all: structured
data travels as JSON text, which survives, and _as_list parses it back.
detect_objects and classify_image are fn nodes calling InferenceClient
for the same reason a model node could not be used (their output port type is
fixed by the endpoint schema β gotcha #1).
Reference-node defaults are a separate case with the opposite answer: the
canvas strips path out of a graph default and keeps only url, so the sample
images are referenced by their public Hub URL β the one form that renders
and that handle_file / InferenceClient can fetch. That is also why the
Space must stay public.
Tests
python apps/05_workflow1111/test_nodes.py # 53 unit tests, offline, ~2s
python apps/05_workflow1111/test_pipelines.py # all 18 outputs, hits HF
python apps/05_workflow1111/test_pipelines.py local # only the offline outputs
python apps/05_workflow1111/test_api.py # the 9 REST endpoints
python apps/05_workflow1111/test_api.py --local # endpoints needing no token
All three layers matter, and they catch different things. test_pipelines.py
drives WorkflowExecutor directly, so it skips gradio's output component
postprocessing β which is precisely where an image port's value shape is
validated. test_api.py is the only layer that catches gotcha #5.
test_pipelines.py writes every result to _test_output/ so you can look at
it. Current status: 53/53 unit, 18/18 executor, 9/9 REST, plus a full run in
the live canvas covering fn, model, space, uploads and image chaining.
Using it as an API
Every subject group is a REST endpoint, so the studio is scriptable:
from gradio_client import Client
client = Client("http://127.0.0.1:7860")
image, params, hires = client.predict(
"a red fox in a snowy pine forest", # Prompt
"", # Negative prompt
"Cinematic", # Style preset
"enhance fine detail", # Hires refine instruction
api_name="/image",
)
Run python apps/05_workflow1111/test_pipelines.py local to print the full
endpoint list with parameter names and types.
Extending it
Change a checkpoint by editing the constants at the top of build_workflow.py
(T2I_MODEL, EDIT_MODEL, β¦) and re-running it. Add a node by writing the
function in nodes.py, registering it in BIND, and adding an fn(...) call
plus link(...)s β the verifier will tell you what you got wrong before the
file is written.
Re-running
build_workflow.pyoverwrites node positions, so any layout you drag around in the canvas is reset.