Spaces:
Running
Running
File size: 13,245 Bytes
db59f73 af888c6 db59f73 af888c6 db59f73 af888c6 db59f73 6fe13ea db59f73 af888c6 db59f73 af888c6 db59f73 af888c6 db59f73 6fe13ea db59f73 6fe13ea af888c6 6fe13ea db59f73 6fe13ea db59f73 6fe13ea db59f73 6fe13ea 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 | ---
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`, `space` and
> inference-calling `fn` node 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`.
```bash
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:
```python
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=0` on 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**: `InferenceClient` has no `depth_estimation`
method, and the positional fallback targets `api-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_detections` as `"[object Object]"` β zero boxes
β an annotated image identical to the input, and
`mask_from_detections` failing with "No detections matched" at **every**
`min_score`.
- ViT labels reached `top_labels` the 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
```bash
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:
```python
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.py` overwrites node positions, so any layout you
> drag around in the canvas is reset.
|