diffusers-workflow 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. diffusers_workflow-0.4.0.dist-info/METADATA +318 -0
  2. diffusers_workflow-0.4.0.dist-info/RECORD +260 -0
  3. diffusers_workflow-0.4.0.dist-info/WHEEL +5 -0
  4. diffusers_workflow-0.4.0.dist-info/entry_points.txt +7 -0
  5. diffusers_workflow-0.4.0.dist-info/licenses/LICENSE +201 -0
  6. diffusers_workflow-0.4.0.dist-info/top_level.txt +2 -0
  7. dw/__init__.py +440 -0
  8. dw/adapter_compatibility.py +226 -0
  9. dw/arguments.py +1231 -0
  10. dw/assessment_rules.py +159 -0
  11. dw/assets.py +130 -0
  12. dw/cache_blocks.json +16 -0
  13. dw/cache_blocks.py +146 -0
  14. dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
  15. dw/content_types.py +150 -0
  16. dw/dissolve_frame_errors.py +121 -0
  17. dw/docs/ACCELERATION.md +352 -0
  18. dw/docs/AGENT_LOOP.md +95 -0
  19. dw/docs/DEPENDENCIES.md +91 -0
  20. dw/docs/IP_ADAPTER.md +109 -0
  21. dw/docs/LORAS.md +131 -0
  22. dw/docs/MCP.md +517 -0
  23. dw/docs/PROMPT_WEIGHTING.md +78 -0
  24. dw/docs/QUANTIZATION.md +230 -0
  25. dw/docs/RECIPES_24GB.md +201 -0
  26. dw/docs/RELEASING.md +195 -0
  27. dw/docs/REMOTE.md +140 -0
  28. dw/docs/REPL_COMMANDS.md +121 -0
  29. dw/docs/REPL_WORKER_GUIDE.md +51 -0
  30. dw/docs/SECURITY.md +272 -0
  31. dw/docs/SECURITY_QUICKREF.md +112 -0
  32. dw/docs/SERVER.md +679 -0
  33. dw/docs/TASKS.md +1741 -0
  34. dw/docs/TESTING.md +71 -0
  35. dw/docs/WORKFLOW_GUIDE.md +2038 -0
  36. dw/docs/WORKSPACES.md +316 -0
  37. dw/download_watch.py +335 -0
  38. dw/elision.py +306 -0
  39. dw/events.py +275 -0
  40. dw/for_each.py +409 -0
  41. dw/host_memory.py +258 -0
  42. dw/host_memory_projection.py +230 -0
  43. dw/hub_cache.py +432 -0
  44. dw/introspection.py +1228 -0
  45. dw/kernel_availability.py +208 -0
  46. dw/locations.py +599 -0
  47. dw/log_setup.py +45 -0
  48. dw/loudness.py +82 -0
  49. dw/media_audio.py +217 -0
  50. dw/media_frames.py +367 -0
  51. dw/media_info.py +297 -0
  52. dw/pipeline_processors/chain.py +821 -0
  53. dw/pipeline_processors/config_objects.py +237 -0
  54. dw/pipeline_processors/pipeline.py +2297 -0
  55. dw/pipeline_processors/remote.py +46 -0
  56. dw/plan.py +920 -0
  57. dw/previous_results.py +411 -0
  58. dw/probe_paths.py +59 -0
  59. dw/prompt_schema.json +48 -0
  60. dw/prompt_weighting.py +378 -0
  61. dw/prompts.py +159 -0
  62. dw/realize.py +250 -0
  63. dw/reference_limits.py +215 -0
  64. dw/reference_names.py +125 -0
  65. dw/repl.py +338 -0
  66. dw/repl_commands.py +836 -0
  67. dw/repl_worker.py +159 -0
  68. dw/result.py +1720 -0
  69. dw/result_fps.py +82 -0
  70. dw/run.py +162 -0
  71. dw/runs.py +768 -0
  72. dw/scalar_result_validation.py +97 -0
  73. dw/schema.py +283 -0
  74. dw/security.py +1038 -0
  75. dw/select_validation.py +115 -0
  76. dw/serve.py +277 -0
  77. dw/server/__init__.py +2 -0
  78. dw/server/app.py +4586 -0
  79. dw/server/assess.py +132 -0
  80. dw/server/catalog_shape.py +487 -0
  81. dw/server/enhancers.py +129 -0
  82. dw/server/exports.py +480 -0
  83. dw/server/guides.py +257 -0
  84. dw/server/jobs.py +1561 -0
  85. dw/server/mcp_mount.py +95 -0
  86. dw/server/netinfo.py +124 -0
  87. dw/server/observed_cost.py +379 -0
  88. dw/server/sysinfo.py +71 -0
  89. dw/server/ui/assets/abap-08VXUWAP.js +1 -0
  90. dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
  91. dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  92. dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
  93. dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
  94. dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
  95. dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
  96. dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
  97. dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
  98. dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
  99. dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
  100. dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
  101. dw/server/ui/assets/css-DIMkf-bt.js +3 -0
  102. dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
  103. dw/server/ui/assets/cssMode-CPznxfY8.js +1 -0
  104. dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
  105. dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
  106. dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  107. dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
  108. dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
  109. dw/server/ui/assets/editor.api-CpWcotrd.js +847 -0
  110. dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
  111. dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
  112. dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
  113. dw/server/ui/assets/freemarker2-CXtRM8N4.js +3 -0
  114. dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  115. dw/server/ui/assets/go-C-y9NEjX.js +1 -0
  116. dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
  117. dw/server/ui/assets/handlebars-N7x-6NMY.js +1 -0
  118. dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
  119. dw/server/ui/assets/html-PhsdjHSr.js +1 -0
  120. dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
  121. dw/server/ui/assets/htmlMode-Dgj0SEok.js +1 -0
  122. dw/server/ui/assets/index-3Vw6WAPW.css +1 -0
  123. dw/server/ui/assets/index-DgrYhQd9.js +43 -0
  124. dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
  125. dw/server/ui/assets/java-BEtHBSE6.js +1 -0
  126. dw/server/ui/assets/javascript-BJqN9Qhv.js +1 -0
  127. dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
  128. dw/server/ui/assets/jsonMode-DbM4SWSv.js +7 -0
  129. dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
  130. dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
  131. dw/server/ui/assets/less-B9JPFI3C.js +2 -0
  132. dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
  133. dw/server/ui/assets/liquid-BWr8lEc4.js +1 -0
  134. dw/server/ui/assets/lspLanguageFeatures-C1iGuDyZ.js +4 -0
  135. dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
  136. dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
  137. dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
  138. dw/server/ui/assets/mdx-DAdMi_0p.js +1 -0
  139. dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
  140. dw/server/ui/assets/monaco--ixms01u.css +1 -0
  141. dw/server/ui/assets/monaco-BGCeEqaw.js +56 -0
  142. dw/server/ui/assets/msdax-DauUninz.js +1 -0
  143. dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
  144. dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  145. dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
  146. dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  147. dw/server/ui/assets/perl-oz_6vUea.js +1 -0
  148. dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
  149. dw/server/ui/assets/php-nr791fC2.js +1 -0
  150. dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
  151. dw/server/ui/assets/postiats-43DmfD33.js +1 -0
  152. dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
  153. dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
  154. dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
  155. dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
  156. dw/server/ui/assets/python-Bcn70HdC.js +1 -0
  157. dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
  158. dw/server/ui/assets/r-BwWrilGY.js +1 -0
  159. dw/server/ui/assets/razor-D1HmNnby.js +1 -0
  160. dw/server/ui/assets/redis-ClamHrr6.js +1 -0
  161. dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
  162. dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  163. dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
  164. dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
  165. dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
  166. dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
  167. dw/server/ui/assets/scheme-BeGwcela.js +1 -0
  168. dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
  169. dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
  170. dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
  171. dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
  172. dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  173. dw/server/ui/assets/sql-NEE52Syq.js +1 -0
  174. dw/server/ui/assets/st-DbInun42.js +1 -0
  175. dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
  176. dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  177. dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
  178. dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
  179. dw/server/ui/assets/tsMode-D6u0XmOW.js +11 -0
  180. dw/server/ui/assets/twig-De2hgUGE.js +1 -0
  181. dw/server/ui/assets/typescript-BU6v-LMV.js +1 -0
  182. dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
  183. dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
  184. dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
  185. dw/server/ui/assets/workers-Cn7cTUKr.js +1 -0
  186. dw/server/ui/assets/xml--0LP2Lwk.js +1 -0
  187. dw/server/ui/assets/yaml-mpBg9jnt.js +1 -0
  188. dw/server/ui/index.html +17 -0
  189. dw/server/updater.py +192 -0
  190. dw/settings.py +98 -0
  191. dw/shot_span_preflight.py +116 -0
  192. dw/shots.py +359 -0
  193. dw/slice_preflight.py +148 -0
  194. dw/step.py +187 -0
  195. dw/step_cache.py +442 -0
  196. dw/subfolders.py +107 -0
  197. dw/task_domains.py +307 -0
  198. dw/tasks/assess.py +826 -0
  199. dw/tasks/audio_transcription.py +88 -0
  200. dw/tasks/audio_utils.py +1862 -0
  201. dw/tasks/background_remover.py +43 -0
  202. dw/tasks/borders.py +113 -0
  203. dw/tasks/compose_text.py +74 -0
  204. dw/tasks/concat_videos.py +300 -0
  205. dw/tasks/depth_estimator.py +54 -0
  206. dw/tasks/diffusion_upscale.py +109 -0
  207. dw/tasks/dissolve_videos.py +342 -0
  208. dw/tasks/format_messages.py +24 -0
  209. dw/tasks/gather.py +173 -0
  210. dw/tasks/grade.py +97 -0
  211. dw/tasks/image_to_text.py +43 -0
  212. dw/tasks/image_utils.py +764 -0
  213. dw/tasks/interpolate_frames.py +252 -0
  214. dw/tasks/judge.py +68 -0
  215. dw/tasks/model_cache.py +55 -0
  216. dw/tasks/pair_audio.py +268 -0
  217. dw/tasks/qr_code.py +19 -0
  218. dw/tasks/restore_faces.py +175 -0
  219. dw/tasks/rife_model.py +192 -0
  220. dw/tasks/segment.py +121 -0
  221. dw/tasks/select.py +111 -0
  222. dw/tasks/speech_generation.py +228 -0
  223. dw/tasks/stabilize.py +129 -0
  224. dw/tasks/task.py +920 -0
  225. dw/tasks/tensor_image.py +57 -0
  226. dw/tasks/text_generation.py +169 -0
  227. dw/tasks/text_sections.py +80 -0
  228. dw/tasks/upscale.py +203 -0
  229. dw/tasks/video_utils.py +624 -0
  230. dw/tasks/zoe_depth.py +71 -0
  231. dw/teacache.py +381 -0
  232. dw/teacache_models.json +99 -0
  233. dw/test.py +29 -0
  234. dw/type_helpers.py +231 -0
  235. dw/validate.py +68 -0
  236. dw/variable_constraints.py +444 -0
  237. dw/variables.py +443 -0
  238. dw/video_extensions.py +141 -0
  239. dw/vram_estimate.py +116 -0
  240. dw/worker.py +764 -0
  241. dw/workflow.py +2007 -0
  242. dw/workflow_schema.json +1346 -0
  243. dw/workflow_sources.py +383 -0
  244. dw/workflows/h3_context_ir.json +57 -0
  245. dw/workflows/test.json +31 -0
  246. dw/workspace.py +730 -0
  247. dw_mcp/__init__.py +6 -0
  248. dw_mcp/__main__.py +133 -0
  249. dw_mcp/assets.py +336 -0
  250. dw_mcp/authoring.py +114 -0
  251. dw_mcp/catalog.py +360 -0
  252. dw_mcp/client.py +486 -0
  253. dw_mcp/diagnose.py +371 -0
  254. dw_mcp/exports.py +84 -0
  255. dw_mcp/guides.py +35 -0
  256. dw_mcp/media.py +638 -0
  257. dw_mcp/models.py +97 -0
  258. dw_mcp/prompts.py +104 -0
  259. dw_mcp/server.py +1343 -0
  260. dw_mcp/workspaces.py +212 -0
dw/docs/SERVER.md ADDED
@@ -0,0 +1,679 @@
1
+ # Server & Web UI
2
+
3
+ `dw.serve` runs the workflow engine as a local HTTP server with a full web
4
+ UI: browse and run workflows, build them in a form-based editor with
5
+ introspection-driven autocomplete, watch jobs stream live progress, review
6
+ past generations in a gallery, and manage the models on disk.
7
+
8
+ ```bash
9
+ python -m dw.serve # http://127.0.0.1:8765
10
+ python -m dw.serve --port 8000 --workflow-dir ./workflows --output-dir ./outputs --prompt-dir ./prompts
11
+
12
+ # or point it at a workspace, which supplies all four directories
13
+ python -m dw.serve --workspace ~/studio
14
+
15
+ # your own workflows, with a checkout's examples alongside them read-only
16
+ python -m dw.serve --workspace ~/studio --examples-dir ~/src/diffusers-workflow/workflows
17
+ python -m dw.serve --host 0.0.0.0 --token "some-long-random-string" # reachable off this machine
18
+ python -m dw.serve --host 0.0.0.0 --token "..." --mcp # ...and drivable by an agent on another machine
19
+ python -m dw.serve --trust-workflows # only if nothing untrusted can reach POST /api/jobs - see Security model
20
+ ```
21
+
22
+ Installed as a package, the same server is `dw-serve`. Interactive API docs
23
+ (OpenAPI) are at `/docs`.
24
+
25
+ The server keeps the REPL's persistent GPU worker underneath: models stay
26
+ loaded between runs, so re-running a workflow with a new prompt skips the
27
+ load entirely.
28
+
29
+ ## The pages
30
+
31
+ The UI is organised by workspace. A sidebar lists every workspace on the
32
+ server; the selected one opens into **Overview, Workflows, Jobs, Gallery,
33
+ Assets, Editor**, and the hash carries the workspace (`#/ws/studio/gallery`),
34
+ so a link names where it points and an old `#/gallery` bookmark redirects to
35
+ the last workspace you were in. Below the workspaces, **Shared** holds what
36
+ every workspace sees — the prompt library, the `common` asset library and
37
+ the read-only example workflows — and **Server** holds Models, Schema and
38
+ Status.
39
+
40
+ - **Overview** — a glance at the workspace: its recent outputs, its recent
41
+ jobs, its workflows (ones with a proof first, plus a filled **New
42
+ workflow** button), its asset count, and disk usage. Each panel loads and
43
+ fails independently, so a slow gallery does not hold the jobs list back
44
+ and a panel's own error shows in place of its content rather than reading
45
+ as an empty workspace. **Manage** on this page is where a workspace is
46
+ deleted (disabled for `default`); a workspace is created from the sidebar.
47
+ - **Workflows** — every workflow on the workspace's own search path
48
+ (`--workflow-dir`), as cards with descriptions, output kinds, and variable
49
+ counts; the read-only ones from an `--examples-dir` are under Shared →
50
+ Examples instead, and opening one's Editor there saves a copy into the
51
+ current workspace. Folders one level deep become sections. Click through
52
+ to a run form generated from the workflow's variables, with the raw JSON
53
+ alongside.
54
+ - **Prompts** — under Shared: the prompt library under `--prompt-dir`
55
+ (default: discovered
56
+ the way a CLI run discovers it, then pinned for every job, so the page and
57
+ `prompt:` resolution always agree), plus the read-only `prompts/` beside
58
+ each `--examples-dir`, so an example's `prompt:` references resolve: stored
59
+ prompts as
60
+ cards with descriptions, intended-model badges, and tags, foldered the
61
+ same way workflows are. Each opens in an editor with form, split, and
62
+ schema-aware JSON views, and an **Enhance with AI** panel that expands an
63
+ idea into a full prompt with a local language model (a preset per target
64
+ model family; the model runs as an ordinary queued job). A workflow
65
+ argument written as `prompt:name` loads the stored text at run time,
66
+ and deleting a prompt warns which workflows reference it.
67
+ - **Jobs** — the queue and full run history (persisted in
68
+ `~/.diffusers_helper/jobs.sqlite`). A workspace's Jobs page lists its own;
69
+ Server → Status lists every workspace's with a filter. A running job streams step-by-step
70
+ progress, per-step denoising ticks, what each step is doing when it is not
71
+ denoising (loading a model, decoding, saving), and its result files as
72
+ they land.
73
+ A run whose steps chose a `result.subfolder` shows its results under
74
+ `final/` and `intermediate/` headings (or whatever the step named), the
75
+ deliverable first; one that chose none shows them as before.
76
+ Jobs can be cancelled mid-denoise and re-run with one click. A finished
77
+ job's **Export** button gathers the run into the workspace's `exports/`
78
+ on the server (`POST /api/jobs/{id}/export`) and downloads it as one zip -
79
+ the same bundle MCP's `export_job` makes; an export that already exists
80
+ asks before it is replaced.
81
+ - **Editor** — build or modify workflows without writing JSON by hand.
82
+ Forms are generated from the live pipeline signatures (see
83
+ [introspection](#introspection)), references
84
+ (`variable:` / `previous_result:`) autocomplete from the workflow itself,
85
+ and three views — form, split, and raw JSON — edit the same definition.
86
+ The split view puts the form beside the JSON with both sides editable;
87
+ changes apply when a side loses focus. Validate, save, and run from the
88
+ same screen; a valid verdict is followed by the run's plan - the step
89
+ and list counts, the minutes from the workflow's `cost` block with its
90
+ basis, how many steps the step cache would serve, and the weights this
91
+ server would download first (`describePlan`, `ui/src/lib/plan.ts`,
92
+ reading `POST /api/validate`'s `plan`). A Monaco editor with the workflow JSON schema backs the
93
+ JSON views. A fourth view, **flow**, renders the workflow's data-flow
94
+ graph read-only: one box per step, arrows for each `previous_result`
95
+ reference labeled with the argument it feeds, entry-point steps marked
96
+ apart from steps that depend on earlier ones, and fan-in points -
97
+ steps combining more than one upstream producer - flagged with the
98
+ cartesian-product multiplier where it's known statically (e.g. a
99
+ literal `num_images_per_prompt` on both producers). It's a diagram of
100
+ the JSON, not a second way to edit it; clicking a step jumps to it in
101
+ the form view.
102
+ - **Gallery** — everything in the selected workspace's output directory,
103
+ which the engine lays out as `<workflow>/<run id>/`. The folder filter groups a workflow's runs
104
+ together rather than listing each run separately, and a **subfolder** pick
105
+ beside the text filter - offered once any output landed in one - narrows
106
+ the grid to `final/`, `intermediate/` or whatever a step's
107
+ `result.subfolder` named; each run directory
108
+ also holds a `manifest.json` describing what produced it (see
109
+ [Workspaces](WORKSPACES.md#runs)). Images generated with
110
+ `embed_metadata` carry their full workflow definition and seed; **open as
111
+ workflow** loads that definition into the editor with the seed pinned, so
112
+ any image can be reproduced or riffed on. Each tile carries a checkbox
113
+ (shift-click extends a range, **Select all** takes whatever the filter
114
+ leaves showing); a selection can be downloaded as one zip or deleted in
115
+ bulk, which is how a directory that fills up over a few hundred runs gets
116
+ cleared out. Anything that fails to delete stays selected. **Keep as
117
+ asset** promotes one generated file into the workspace's asset library
118
+ under a stable name, so a later workflow can reference it as
119
+ `asset:<name>` instead of a run id that pruning would break.
120
+ - **Models** — the Hugging Face hub cache: every cached repo with sizes,
121
+ revisions, and last-used dates, plus free disk space. Download a repo by
122
+ id with live progress (cancellable; partial files resume on retry), and
123
+ delete to free disk (refused while a job is running; the next workflow
124
+ that needs the model downloads it again). The page also shows the
125
+ installed diffusers version (with its commit for a git install) and can
126
+ upgrade it to GitHub HEAD - new model pipelines usually land there
127
+ before a PyPI release. The idle worker restarts on success so the next
128
+ job imports the new version; the upgrade is refused while a job runs.
129
+ - **Schema** — the workflow JSON schema the running server validates
130
+ against, as a browsable tree: the document root plus every definition,
131
+ with types, required markers, defaults, enums, and descriptions.
132
+ `$ref` labels jump to their definition; a filter narrows the list.
133
+ - **Server → Status** — what this server is and how to reach it: device,
134
+ version, bind address and LAN addresses, whether a token is required,
135
+ whether `/mcp` is mounted (with the `claude mcp add` line to connect to
136
+ it), and the directories in use — and, below, the queue across every
137
+ workspace. Workspaces are created from the sidebar and deleted from their
138
+ Overview.
139
+
140
+ ## Workspaces
141
+
142
+ One server can hold several workspaces — each with its own `workflows/`,
143
+ `assets/` and `outputs/`, all sharing the root's one prompt library. The
144
+ root's own folders are the workspace named `default`.
145
+
146
+ Every scoped route takes an optional `?workspace=<name>`; omitting it means
147
+ `default`, so nothing written against a single-workspace server changes
148
+ meaning. `POST /api/jobs` also accepts `"workspace"` in the body, and a job
149
+ holds onto the directories it was submitted with — through the run, a rerun,
150
+ and when history serves its files back. `GET /api/jobs` spans every workspace
151
+ unless one is named.
152
+
153
+ A workspace is a namespace, not a security boundary: the API token is
154
+ all-or-nothing. See [Workspaces](WORKSPACES.md#several-workspaces-on-one-server).
155
+
156
+ The sidebar lists every workspace, and `+ new` there creates one; a
157
+ workspace's own Overview page is where it is deleted. Server → Status shows
158
+ the resolved directories and the `claude mcp add` line for connecting an
159
+ agent from another machine, plus the queue across every workspace:
160
+
161
+ ![The Server page before the sidebar: address picker, generated claude mcp add line, resolved directories — the workspace list it shows now lives in the sidebar](img/ui-server-dark.png)
162
+
163
+ ## Jobs API
164
+
165
+ | Route | What it does |
166
+ | --- | --- |
167
+ | `POST /api/jobs` | Queue a run: `{"workflow_path": ...}` or an inline `{"workflow": {...}, "base_dir": ...}`, plus `arguments` for variable overrides. `workflow_path` accepts a stored workflow name as listed by `/api/workflows` (with or without `.json`, nested names included), or a relative/absolute path that still resolves under `--workflow-dir` - confined the same way the `/api/workflows` CRUD routes are; a path that names a real file outside that directory is rejected with 400, not opened. Answers with argument warnings from signature checking. Takes an optional `acknowledged_cost`: `true` is recorded as `acknowledged: boolean`; the object `{fingerprint, minutes, downloads}` from a validate answer's `plan` is `bound` - the server re-plans the run for the arguments given and answers **409** when the fingerprint differs or a repo in `downloads_required` is not in `downloads` (a download that has since vanished is not a refusal); the body is `{"detail": {message, reason: "fingerprint" \| "downloads" \| "unplannable", acknowledged, plan}}` with the current plan, so the caller re-quotes from it. `minutes` is recorded, never compared. Nothing is required: the web UI and every caller that sends nothing are `acknowledged: none`, and every job answer and history row carries `acknowledged` (and `acknowledged_cost` when bound). `POST /api/jobs/{id}/rerun` takes the same field and checks against the stored spec; a fresh seed does not change a fingerprint. |
168
+ | `GET /api/jobs?workspace=&status=&limit=` | Queue + history summaries, oldest first, with `total` beside them. `status` narrows to one state or a comma-separated set (`queued`, `running`, `succeeded`, `failed`, `cancelled`; anything else is a 400); `limit` keeps the newest N, and `total` still reports how many matched, so a bounded answer cannot be mistaken for a complete one. No parameters means every job, which is what the web UI polls |
169
+ | `GET /api/jobs/{id}` | Full detail: spec, events, manifest, error. A manifest entry for a step served from the step cache carries `reused: true`. Every entry carries `subfolder` - the in-run subfolder the step's `result.subfolder` chose, `''` for none. A `for_each` step appears in the manifest as its members (`shot@wide_open`, `shot@closeup`), because the manifest records what ran; the run's `workflow.json` keeps the `for_each` form, because it records what was asked |
170
+ | `GET /api/jobs/{id}/workflow` | The workflow the job ran: `{id, definition, realized, seed_variable}`. `seed_variable` names the variable a `new_seed` rerun would draw into (null when the workflow has none), read from the workflow as written rather than the realized copy, whose seed is pinned. `realized: true` is the copy the run itself wrote (`workflow.json` in its run directory), with arguments, seed, prompts and `output:latest` pinned; `false` falls back to the submitted definition, which is what a job from before run tracking has. 404 means neither is readable - the job itself still is. The equivalent MCP tool is `get_job_workflow` (see [MCP.md](MCP.md#diagnose)) |
171
+ | `POST /api/jobs/{id}/export?workspace=&overwrite=` | Gather one finished job into `<workspace>/exports/<job id>/`: `workflow.json`, `manifest.json`, `job.json`, `README.md`, `assets/`, `inputs/`, `outputs/`. 201 with the file list, total bytes, anything it could not find, a `zip_url`, and the three JSON files inline. 404 unknown job, 409 for a job still running or an existing export without `overwrite` |
172
+ | `GET /exports/{id}.zip?workspace=` | The same tree as one archive, built on request rather than kept as a second copy. Entries are named `<job id>/<relative path>`. Ungated exactly as `/outputs` is |
173
+ | `GET /api/jobs/{id}/events` | Server-sent events stream; `?after=N` / `Last-Event-ID` replay missed events, so reconnects are lossless. Every event carries `seq` and `at` - seconds since the job started (since it was queued, for the events before that) - so a step's cost is a subtraction: `step_start` to `generating` is what a reused pipeline still pays before it runs, `generating` to the first `pipeline_step` is the prompt and reference encoding |
174
+ | `GET /api/jobs/{id}/event-log?after=-1&limit=200` | The same events as the SSE stream, as one JSON page: `{id, status, events, last_seq, truncated, note}`. `after` is exclusive; page by passing back the previous `last_seq`. A job restored from history serves the bounded event tail persisted with it; a job that finished before events were retained returns an empty list and a `note` saying so. |
175
+ | `POST /api/jobs/{id}/cancel` | Cooperative cancel (takes effect at the next step boundary or denoise step) |
176
+ | `POST /api/jobs/{id}/rerun` | Re-queue a finished job's spec. Body `{"new_seed": true}` draws a fresh seed into the workflow's seed variable instead of repeating the original arguments; 400 when the workflow pins its seed to a literal or names none. A plain rerun of a seeded workflow is served whole from the step cache — the earlier run's files, republished with `reused: true`, generating nothing |
177
+ | `POST /api/jobs/{id}/move` | Reorder a queued job: `{"direction": "up"\|"down"\|"front"\|"back"}`. Job listings carry each waiting job's `queue_position`. |
178
+
179
+ One job runs at a time (it is one GPU); submissions queue in order, and
180
+ the waiting portion of the queue can be reordered.
181
+
182
+ ### Progress events
183
+
184
+ Every event in the stream carries a `seq` and an `event` name:
185
+
186
+ | event | when | payload |
187
+ | --- | --- | --- |
188
+ | `job_status` | queued/running/terminal transitions | `status` |
189
+ | `log` | worker output lines; each top-level block of a `ModularPipeline` as it starts (`MiniMaxAI/MiniMax-H3: vae_encoder`) - the lead-in before the denoise loop is where a reference encode's minutes go, and the block name is what says which one it is in; and each file the `saving` phase writes, named as it starts (`writing shot.mp4 (121 frames)`) and costed as it finishes (`wrote shot.mp4 in 1.3s (1.4 MB)`), which is the other stretch a step spends with its denoise counter frozen | `message`, and for a file `file` plus `seconds` on the closing one |
190
+ | `memory` | device memory after a run | `info` |
191
+ | `run_start` | the run directory is chosen, before the first step | `run_id`, `identity`, `run_dir` |
192
+ | `workflow_start` | the run begins | `workflow`, `total_steps`, `steps`, `seed` |
193
+ | `step_start` / `step_end` | each step | `step`, `index`, `total_steps`; `files` and `subfolder` at the end. A step served from the step cache adds `reused: true` to `step_end`, and its `files` are the earlier run's files rather than newly written ones |
194
+ | `iteration_start` | each argument combination in a step | `step`, `iteration`, `total_iterations` |
195
+ | `pipeline_step` | each denoise step | `step`, `total_steps`. Emitted for a pipeline that takes a `callback_on_step_end`, and for a `ModularPipeline` (H3, LTX-2, Qwen-Image), which takes none - there the denoise block's own progress bar is what reports |
196
+ | `phase` | the step changes what it is doing | `phase`, `detail` |
197
+ | `pipeline_released` | a step with `release_pipeline` drops its pipeline | `step`, `index`, `gpu_memory_allocated_mb` and `gpu_memory_allocated_before_mb` (both `null` where the backend cannot say). Emitted between the step's generation and its files being written, which is where the release happens - so the ordering is readable off the event stream rather than by trying to poll memory through a sub-second write |
198
+ | `warning` | a step finds something wrong with what it is about to write | `message`, plus a `kind` and the figures behind it (`level_spread`: `spread_db`, `measure`, `command`; `fps_mismatch`: `declared_fps`, `source_fps`; `audio_no_headroom`: `file`, `peak_dbfs` - a deliverable at or above -0.5 dBFS, which an mp3 or AAC encode decodes over full scale; `step_elided`: `step`, `overridden_by` when a supplied argument is what made it unreferenced). Also appended to the job's `warnings`, prefixed with the step it fired in - the event keeps the moment, `warnings` keeps it where a caller polling the finished job will look, since a warning about the artifact outlives the run that noticed it |
199
+ | `workflow_end` | the run finishes | `manifest` |
200
+
201
+ A step spends most of its wall clock outside the denoise loop, and
202
+ `pipeline_step` cannot see any of it. `phase` is what fills that silence:
203
+ `loading` (with the model or component in `detail`), `cached` (the same
204
+ pipeline as a previous run - milliseconds, not minutes), `generating`
205
+ (the denoise loop, or a chain's `segment N/M` - which is why the counter
206
+ restarts), `decoding` (latents, after the last denoise step), `saving`
207
+ (writing files, including video encode - it names each file on the `log`
208
+ stream rather than running silent, since the denoise counter is frozen at its
209
+ last step throughout) and `task` (a task step, named in
210
+ `detail`). Emits are a handful per step, not per denoise tick.
211
+
212
+ ### Progress on a running job
213
+
214
+ `GET /api/jobs/{id}` carries a `progress` block while a job is running
215
+ (`null` before it starts and once it is terminal, where the manifest is the
216
+ better answer). It is the same information the event log holds, kept as the
217
+ events arrive so a caller does not have to page back through a trimmed log
218
+ to learn where a long render is:
219
+
220
+ | field | |
221
+ | --- | --- |
222
+ | `step`, `step_index`, `total_steps` | the workflow step being run |
223
+ | `phase`, `phase_detail` | the latest phase and what it named |
224
+ | `seconds_in_phase` | how long it has been in it |
225
+ | `seconds_since_event` | how long since anything at all happened - the number that separates a slow run from a stuck one |
226
+ | `denoise_step`, `denoise_total_steps` | the denoise loop's counter, `null` until it starts |
227
+
228
+ A null `denoise_step` under `generating` is the pipeline's lead-in - encoding
229
+ the prompt and every reference - which emits nothing and runs well over a
230
+ minute on a large video model. How long it runs follows what it has to
231
+ encode: on MiniMax H3, ~90 s for a prompt with an image or audio reference,
232
+ but ~10 min once a *video* reference is among them - a measured run encoding
233
+ one 5 s 960x544 clip on an RTX 3090 sat silent from 94 s to 723 s. The keys
234
+ are always present so that lead-in can be told from a loop that has stopped
235
+ advancing: `seconds_since_event` is a stall signal once `denoise_step` is a
236
+ number, or in any phase other than `generating`.
237
+
238
+ The lead-in is no longer silent, though: each of a modular pipeline's
239
+ top-level blocks emits a `log` naming it as it starts (`before_encode`,
240
+ `text_encoder`, `vae_encoder`, `denoise`, `decode` on H3), so the last event
241
+ says which one the run is inside. Only a `SequentialPipelineBlocks` is
242
+ narrated this way - a conditional container picks one branch rather than
243
+ running them all, and walking its sub-blocks would be a wrong answer bought
244
+ with a progress message.
245
+
246
+ It is a coarse one even then. A step's cost is not uniform when the pipeline
247
+ configures a transformer block cache (`"cache": {"type": "first_block"}`):
248
+ most steps are served from it in seconds and every few steps one is computed
249
+ in full, so the same healthy run emits four `pipeline_step` events in 20 s
250
+ and then nothing for 133 s. Liveness is `denoise_step` having moved between
251
+ polls minutes apart, not silence measured against a fixed threshold - on H3
252
+ that threshold would have to exceed ~140 s to mean anything.
253
+
254
+ ## Introspection API
255
+
256
+ The editor's forms come from these; they are just as usable from scripts:
257
+
258
+ - `GET /api/pipelines`, `GET /api/pipelines/{name}` — diffusers pipeline
259
+ classes and their call signatures
260
+ - `GET /api/classes?kind=...`, `GET /api/classes/{name}?target=call|init|load` —
261
+ any allowed class (diffusers + registered extension modules), described
262
+ for calling, constructing, or `from_pretrained` loading
263
+ - `GET /api/tasks` — the task commands and processors
264
+ - `GET /api/tasks/{command}` — a task's argument schema, read from its
265
+ registered implementation's real signature
266
+ - `GET /api/schema` — the workflow JSON schema. `?section=` answers one
267
+ part of it - `steps`, `pipelines`, `tasks`, `result`, `variables` or
268
+ `configuration` - as `{section, sections, elsewhere, schema}`, where
269
+ `elsewhere` names the section holding each definition the fragment still
270
+ `$ref`s; the no-argument call is the whole schema, unchanged
271
+ - `GET /api/guides` — the documentation that bears on choosing a
272
+ capability: each guide's name, what it covers, and its section headings
273
+ - `GET /api/guides/{name}?section=` — one section of a guide; section names
274
+ match loosely. Without a `section` the answer is the guide's index - its
275
+ opening, its first section, and `sections`/`withheld` naming the rest -
276
+ rather than the whole file, which for WORKFLOW_GUIDE.md is ~19.6k tokens
277
+ in one call (#101). Served by the engine so an MCP client
278
+ at another version reads the guides for the server it is driving, not
279
+ its own. A checkout serves the repo's `docs/`; an install the copy
280
+ `build_dist.sh` puts under `dw/docs/`
281
+ - `POST /api/validate` — schema validation plus signature-level argument
282
+ warnings for pipeline and task steps (catches the typo before the model
283
+ loads); `warnings` also names an entry key of a list-driven variable that
284
+ no step reads, at the entry's path (`variables.shots[0]` or, when the
285
+ caller's own `arguments` supplied the list, `arguments.shots[0]`).
286
+ Accepts `workflow_path` (same resolution and confinement as
287
+ `/api/jobs`, above) as an alternative to inline `workflow` - exactly one
288
+ of the two, or a 400. Every schema violation is returned in `errors`
289
+ (`[{path, message}]`, sorted by path, capped at 25), and joined one per
290
+ line in `error`. It also takes the `arguments` a caller is about to run
291
+ with, and checks them the way the run would: a name the workflow does not
292
+ declare, a value that will not coerce to the declared type, and an
293
+ `asset:`, `prompt:` or `output:` reference that names nothing this
294
+ workspace can reach - each reported at `arguments.<name>`. `POST /api/jobs`
295
+ makes the same check and answers 400 rather than queuing a job that would
296
+ fail on its first step; `checked_arguments` on a valid answer names what
297
+ was covered, since without arguments the verdict is about the stored
298
+ defaults only. A reference set a pipeline would refuse is an error here
299
+ too - too many images, videos or audio clips for the family, or, for
300
+ MiniMax-H3, audio as the only reference - because the pipeline enforces
301
+ those only once its checkpoint is loaded, minutes into an acknowledged
302
+ run (`dw/reference_limits.py`, which reads each limit off the diffusers
303
+ block that enforces it rather than restating it). A LoRA loaded onto the
304
+ wrong checkpoint partition is an error for the opposite reason - the
305
+ pipeline accepts it: MiniMax-H3's `ref2va` holds `transformer_ref` alone,
306
+ so an FL2VA-trained adapter loads onto it, the run succeeds and only the
307
+ identity retention is worse (`dw/adapter_compatibility.py`, #155). A
308
+ `weight_name` carrying neither `ref2v` nor `fl2v` cannot be placed, so it
309
+ is a `warnings` entry naming the rule rather than a refusal - a
310
+ reference-trained checkpoint nobody has named yet still gets through.
311
+
312
+ A valid answer also carries `plan`, what the run will execute for those
313
+ arguments: `fingerprint` (`sha256:…` over the realized, expanded
314
+ definition with the seed and the documentation keys removed and
315
+ `output:…/latest/…` left unpinned - the same work hashes the same, a
316
+ longer list or an edited stored prompt does not); `steps`, the expanded
317
+ member count; `list_entries`, `{variable: length}` for each `for_each`
318
+ over a list variable; `cached_steps`, how many of those steps the
319
+ worker's step cache would serve (`0` for an unseeded workflow, `null`
320
+ when the worker is busy or did not answer - a workflow with no `seed`
321
+ also gets a warning saying so, since `0` alone does not distinguish a
322
+ disabled cache from an empty one);
323
+ `downloads_required`, each `model_name` the hub cache does not hold as
324
+ `{repo, gb, gated, access_blocked}` (`gb` from the hub, `null` when it
325
+ could not be asked - `?sizes=false` skips the hub, and then `gated` and
326
+ `access_blocked` are `null` too); `gated` is the hub's own field for the
327
+ repo (`false`, `"auto"` or `"manual"`) or `null` when the lookup itself
328
+ failed for a reason other than the gate; `access_blocked` is `true`
329
+ when this box's Hugging Face token specifically has not been granted
330
+ access to a gated repo, `false` when the repo isn't gated or the token is
331
+ accepted, and `null` when it could not be determined either way. A gated
332
+ repo's own metadata is served by the hub regardless of this token's
333
+ access, so `model_info` succeeding proves nothing about the gate; a
334
+ gated entry gets a second, real check - a HEAD request against one of the
335
+ repo's own files - and it is *that* request's `GatedRepoError` that sets
336
+ `access_blocked: true` (the pre-flight signal for what would otherwise be
337
+ a 403 partway into a run, #186). `access_blocked` is `null` when there is
338
+ no file to probe or the probe itself fails for an unrelated reason (e.g.
339
+ offline) - "unknown" is not "not blocked". `validate_workflow`'s
340
+ `warnings` carries one line per entry with `access_blocked: true`. Each
341
+ `from_single_file` URL is `{repo: null, url, gb: null, gated: null,
342
+ access_blocked: null}`, since a direct file URL is never gated; and
343
+ `estimate`, `{minutes, basis,
344
+ device, measured_on, partial, runs}` from this box's own history when it
345
+ has one and otherwise from the workflow's `cost` block -
346
+ `basis` is `observed` (the cold median of this server's own finished runs
347
+ of this shape, with `runs` saying how many; preferred over a curated
348
+ figure, and quoted only for the bucket the caller's arguments fall in),
349
+ `catalog` (the stored total, for a run whose lists are the
350
+ ones it was measured with), `per_entry` (re-priced from a measured
351
+ per-entry rate, when the entry carries `per_entry`), `derived` (the
352
+ stored total extrapolated linearly over a list whose length the caller
353
+ changed - an estimate, not a measurement), `other_device` (no entry for
354
+ the serving backend; the first entry's figure, which is a warning rather
355
+ than a quote) or `unknown` (no cost block, or more than one list changed
356
+ so there is nothing honest to extrapolate along); a composed child's
357
+ cost is added to a curated figure and `partial` is true when a child has
358
+ none - an `observed` figure already measured the whole run, children
359
+ included, so nothing is added to it and `partial` is false. `plan` is `null` when
360
+ it could not be built; an invalid answer carries no `plan` key.
361
+
362
+ ## Files and models
363
+
364
+ - `GET /api/workflows` — the stored workflow names, plus a `details` entry
365
+ per workflow: `description`, `kinds` (the output content types' top-level
366
+ halves), `steps`, `variables` (a count) and `variable_names`, and
367
+ `prompt_refs` naming the stored prompts it leans on, and `configures` - for
368
+ a workflow under `models/`, the `templates/` name it is a tuned
369
+ configuration of, empty when it is a template itself or when the name does
370
+ not resolve (then `configures_missing` carries what was written). Four more
371
+ say what the workflow makes, read off its definition (a top-level `shape`,
372
+ `traits` or `summary` in the file overrides): `shape`, one of `image`,
373
+ `image-set`, `image-edit`, `shot`, `sequence`, `audio`, `text`, `utility`;
374
+ `traits`, a sorted subset of `has-audio`, `chained`, `image-conditioned`,
375
+ `identity-referenced`, `needs-input-media`, `composes-workflows`; `summary`,
376
+ the first sentence of the description, capped at 120 characters; and `cost`,
377
+ the maintainer-measured `{device, name, vram_gb, minutes}` runs, or `null`
378
+ when nobody has measured it - a list-driven workflow's `cost` entry may
379
+ also carry a measured `per_entry` (`{variable, minutes, entries}`), the
380
+ cost of one entry of the list it was measured against. The response's
381
+ `cost_basis` says what that is - `curated`: figures a maintainer measured
382
+ once and wrote into the workflow, never derived from this server's own job
383
+ history, so `null` means nobody wrote one down rather than "this box has
384
+ never run it". Beside it, `observed` is the derived figure the same
385
+ listing is allowed to carry (#93): what *this* box's own finished runs of
386
+ that workflow took, as `cold_minutes`/`cold_runs` (model load included,
387
+ so comparable to a curated `cost`) and `warm_minutes`/`warm_runs` (model
388
+ already resident), with the `drivers` the figure is for, `since`, and
389
+ `unclassified_runs` when a run's persisted events were trimmed past its
390
+ `loading` phase. Runs are bucketed by the workflow's declared
391
+ `cost_drivers` - the variables that move its cost - so a 345-frame run
392
+ never informs a 124-frame figure; a list driver buckets on its length. A
393
+ workflow declaring no drivers falls back to runs that overrode nothing at
394
+ all, and a run whose every step was a step-cache hit is excluded. The
395
+ compact view carries only `observed_minutes` (cold) and `observed_runs`;
396
+ `GET /api/workflows/{name}/variables` carries the whole block beside the
397
+ defaults. Derived from the job rows in one query - so the figures outlive
398
+ a pruned run directory - and cached against the jobs table's high-water
399
+ mark rather than a file mtime, because a job landing changes every figure
400
+ and changes no file. `observed` never replaces `cost`: a maintainer's
401
+ claim on a named card and this machine's last week are different things.
402
+ A `models/` entry
403
+ takes its `shape` and `traits` from the template it configures and keeps
404
+ its own `cost`. A list-driven workflow (one with a `for_each` step) also
405
+ carries `lists`: per list variable, the fields an entry takes, the steps
406
+ run over it and the default's length. Enough to choose a workflow and know
407
+ what to pass it without reading each one; the variable defaults are
408
+ deliberately left out, being an order of magnitude more payload on a
409
+ listing the UI reloads. Cached by file mtime
410
+
411
+ Optional query params narrow and shrink it:
412
+ `?shape=&traits=&configures=&include_models=&view=compact`. `shape` keeps
413
+ entries of that shape and `traits` (comma-separated) those carrying all of
414
+ them - an unknown value in either is a 400 whose `detail` lists the
415
+ vocabulary. `configures=<template>` keeps that template's model configs.
416
+ `view=compact` is the agent's projection: it drops `description`, `origin`,
417
+ `writable`, `prompt_refs`, `steps` and `variables`, keeps `summary`,
418
+ `shape`, `traits`, `cost`, `kinds`, `variable_names` and `lists` (carried
419
+ only when the workflow has a list-driven step, like `configures`), and
420
+ lists templates only unless `include_models=true` or a `configures` asks
421
+ otherwise. With no params the response is what it always was, plus the new
422
+ fields
423
+ - `GET/PUT/DELETE /api/workflows/{name}` — read, save, delete workflow files
424
+ (confined to `--workflow-dir`)
425
+ - `GET /api/workflows/{name:path}/download` — download a workflow file as JSON
426
+ - `GET /api/workflows/{name:path}/variables` — a workflow's variables and what
427
+ they default to, without the definition around them. Long string defaults are
428
+ cut to 200 characters and named in `truncated`, including strings inside a
429
+ list default, named like `shots[0].prompt`; `full=true` returns them whole
430
+ - `GET /api/prompts`, `GET/PUT/DELETE /api/prompts/{name}` — the prompt
431
+ library (confined to `--prompt-dir`, names held to what a `prompt:`
432
+ reference can load); saves are validated against the prompt schema,
433
+ served at `GET /api/prompt-schema`
434
+ - `GET /api/prompts/{name:path}/download` — download a prompt file as text
435
+ - `GET /api/enhancers`, `POST /api/enhance` — prompt-enhancement presets,
436
+ and `{"idea": ..., "preset": ..., "model_name": ..., "device": ...}` to
437
+ queue an enhancement as an ordinary job whose saved text file is the
438
+ result
439
+ - `GET /api/gallery`, `GET /api/gallery/{name}/metadata`,
440
+ `DELETE /api/gallery/{name}` — outputs and their embedded metadata. Each
441
+ gallery entry carries `folder` (the workflow identity, the run id dropped)
442
+ and `subfolder` (what followed the run id - the `final`/`intermediate` a
443
+ step's `result.subfolder` chose, `''` when it chose none); `?folder=` and
444
+ `?subfolder=` filter independently (`?version=` too - with `?folder=`,
445
+ the one run the gallery labels `v4`), and the reply's `folders` and
446
+ `subfolders` list every distinct value over the whole tree, `''` always a
447
+ member of each so root-level files stay selectable
448
+ - `GET /api/gallery/{name:path}/download` — download an output file
449
+ - `POST /api/gallery/archive` — `{"names": [...]}` (1-1000) bundles a
450
+ multi-file selection into one zip, named by each file's gallery-relative
451
+ path so output subfolders survive. A browser cannot zip on its own and
452
+ throttles a burst of single downloads, so the gallery's bulk download
453
+ goes through here; an unknown or out-of-directory name 404s the whole
454
+ request rather than yielding a partial archive
455
+ - `GET /api/workspaces`, `POST /api/workspaces` (`{"name": ...}`),
456
+ `DELETE /api/workspaces/{name}?acknowledged=true` — the workspaces on this
457
+ server. The workspace root's own `workflows/assets/outputs` are the
458
+ `default` workspace and a named one is a subdirectory beside them, sharing
459
+ the root's one prompt library. Delete answers with what it would remove and
460
+ refuses until acknowledged, refuses the default, and refuses a workspace
461
+ with jobs still queued. Each listed workspace carries a `usage`
462
+ (`{files, bytes}`) — roughly how much disk its own folders hold, walked at
463
+ most once a minute per workspace and deliberately approximate; the shared
464
+ prompt library counts against the `default` workspace alone rather than
465
+ once per workspace. A workspace is a namespace, **not** a security
466
+ boundary: the API token is all-or-nothing
467
+
468
+ `exports/` sits beside the workspace's own folders, holding one directory per
469
+ exported job. It is a reserved name: no workspace can be called `exports`, and
470
+ the folder is never listed as one.
471
+ - `GET /api/assets` — the asset library: input media, each with the
472
+ `asset:` reference a workflow carries rather than a path, since a path
473
+ only means something on the server's own machine. Empty rather than an
474
+ error when no library is configured. `libraries` lists the roots searched,
475
+ in order, each `{origin, dir, writable}` — what `asset_dirs` names without
476
+ saying which of them an upload or delete can actually reach. `shadowed`
477
+ lists the entries a nearer library hides: same shape as an `assets` entry
478
+ but without `url` (that URL would serve the shadowing file, not this one),
479
+ plus `shadowed_by` naming the origin that won
480
+ - `POST /api/assets/keep` (`{"name": ..., "asset_name": ..., "overwrite": false, "shared": false}`)
481
+ — keep a generated file as an input asset under a stable name, returning
482
+ its `asset:` reference. A run's files are named by the run that made them,
483
+ which is the wrong thing for a later workflow to depend on: `latest` moves
484
+ and a pinned run id breaks when outputs are pruned. The copy happens inside
485
+ the workspace and is a hard link where the filesystem allows one, so
486
+ keeping one frame of a large render costs no second copy of it. Refuses an
487
+ existing name unless `overwrite`. `asset_name` may name a folder and takes
488
+ the kept file's extension when it carries none (a contradicting one is a
489
+ 400) — the same rule the upload route follows, and what keeps a kept asset
490
+ from landing under an extensionless name the library listing never shows.
491
+ `"shared": true` keeps it in
492
+ `<root>/common/assets` instead — the library every workspace under the root
493
+ shares, which is where a recurring cast belongs
494
+ - `DELETE /api/assets/{name}` — remove one file from the asset library,
495
+ deleting from whichever library on the search path holds it (the
496
+ workspace's own before the shared one, the order `asset:` resolves in).
497
+ An asset from a read-only examples library answers 403, the same as a
498
+ read-only prompt or workflow; a name nothing holds answers 404
499
+ - `POST /api/assets/archive` — `{"names": [...]}` (1-1000) bundles a
500
+ multi-file asset selection into one zip, named by each file's
501
+ library-relative path, which is the name its `asset:` reference carries.
502
+ The gallery archive's counterpart on the input side; it resolves down the
503
+ same search path a run does, so a selection spanning this workspace's
504
+ library, the shared one and an examples tree downloads as one archive, and
505
+ an unknown or out-of-library name 404s the whole request rather than
506
+ yielding a partial one. A duplicate name (repeated in the selection, or
507
+ differing only by leading/trailing whitespace) collapses onto the one zip
508
+ entry. Media (image/video/audio) stores rather than deflates, unless it's
509
+ a raw format that still compresses (`.bmp`, `.wav`) - everything else the
510
+ libraries hold is an already-compressed container, and the response does
511
+ not start until the archive is complete, so deflating it is latency the
512
+ caller waits through for nothing. Everything else - `.json`, `.md`,
513
+ `.txt`, an unrecognized extension - deflates; so does the export zip's
514
+ text files (`workflow.json`, `manifest.json`, `job.json`, the README)
515
+ - `POST /api/uploads?filename=...` — the raw bytes of one image, video or audio file
516
+ (200MB ceiling, checked from `Content-Length` before a byte is read, and
517
+ again on the body; extension held to the allowed image/video list), saved
518
+ into the asset library's `uploads/` subfolder - the shared library at
519
+ `<root>/common/assets` when `shared=true`, this workspace's own otherwise -
520
+ under a generated name, or under `asset_name` when one is given (`cast/priya-voice.wav`, folders allowed,
521
+ the uploaded file's extension assumed, confined to the library the way
522
+ `keep_output`'s name is).
523
+ Answers 201 with `path` - `asset:uploads/<name>`, the reference a saved
524
+ workflow can carry and still resolve on a later run - and `url`, the same
525
+ file under the `/inputs` mount, for the editor's preview. A server started
526
+ without an asset library falls back to the output directory's `uploads/`
527
+ and an absolute path. This is how the UI's file pickers get a local file
528
+ onto the machine that will run the workflow. The body is the file itself,
529
+ so no multipart parser is needed for a single-file upload
530
+ - `GET /api/models`, `DELETE /api/models?repo={repo_id}` — hub cache
531
+ inventory and deletion
532
+ - `POST /api/models/download` (`{"repo_id": ...}`), `GET /api/models/downloads`,
533
+ `POST /api/models/downloads/{id}/cancel` — background snapshot downloads
534
+ with byte-level progress
535
+ - `GET /api/system/diffusers`, `POST /api/system/diffusers/update` —
536
+ installed diffusers version/commit, and a background diffusers install/
537
+ update (refused while a job is running or queued). The POST body is
538
+ optional JSON, `{"commit": ..., "revert": ...}`: with neither, it
539
+ `pip install --upgrade`s from GitHub HEAD; `commit` (7-40 hex characters,
540
+ validated before it reaches the command line) pins the git install to
541
+ that commit instead of HEAD; `revert: true` pins back to the known-good
542
+ published release instead of installing from git - the diffusers floor
543
+ version read from `pyproject.toml` (`pip install diffusers==<floor>`).
544
+ `commit` and `revert` are mutually exclusive. The status response
545
+ includes `before` (the version/commit that was installed when the update
546
+ started) alongside the live `version`/`commit`, so a revert has a
547
+ concrete before/after to compare
548
+ - `GET /api/memory`, `GET /api/health` — worker VRAM/RAM stats and liveness;
549
+ memory answers `live` (whether `info` was measured by this call), `stale`,
550
+ `reason` (`job_running`, `worker_stopped`, `worker_busy`,
551
+ `worker_unreachable`) and `age_seconds`, so a cached reading is never
552
+ mistaken for the worker's memory now - `info: null` means nothing has been
553
+ measured because nothing is resident. health also reports `hostname`,
554
+ `device` and whether `mcp` is mounted, so a remote client can tell which
555
+ machine answered
556
+ - `POST /api/memory/clear` (#221) — drops every loaded pipeline and the step
557
+ cache, the same mechanism as the REPL's `memory clear`, and returns the
558
+ memory reading taken right after. Refused with 409 while a job is running
559
+ or queued - the queue is FIFO, so the caller retries once it finishes
560
+ rather than this call blocking until it does
561
+ - `GET /api/server` — connection details for the Server page: `hostname`,
562
+ `version`, `device`, the `bind_host`/`port`/`wildcard_bind` the server was
563
+ started with, `auth_required` (whether a token is configured - never the
564
+ token itself), `mcp` (`mounted` plus its `path`), the `directories` in use,
565
+ and `runtime` (#222) - Python version, torch version and the CUDA version
566
+ torch was built against, the NVIDIA driver version (via `nvidia-smi`, when
567
+ it's on PATH), and the installed versions of diffusers, transformers,
568
+ accelerate, bitsandbytes, peft, safetensors and sentencepiece (`null` for
569
+ one not installed) - for diagnosing an environment mismatch between boxes
570
+ without shelling in; the machine's non-loopback `addresses`; a client
571
+ composes its URLs from an address, the port and the MCP path
572
+
573
+ ## Security model
574
+
575
+ The server is built to serve **your own GPU to your own browser**, not the
576
+ network:
577
+
578
+ - Binds to `127.0.0.1` by default. `--host 0.0.0.0` (or any other
579
+ non-loopback address) is possible; without a token configured (see
580
+ Authentication, below) the server logs a startup warning, since anything
581
+ that can reach that address can queue jobs and browse/delete files.
582
+ - Requests carrying an `Origin` header are rejected (403) unless its
583
+ hostname is a loopback name, the configured `--host`, or the hostname
584
+ the request itself was addressed to (`Host`). The last clause lets a
585
+ browser on another machine use a `--host 0.0.0.0` server by LAN IP or
586
+ hostname; it still blocks cross-site pages and DNS rebinding, where the
587
+ attacker's page carries its own `Origin` while `Host` is whatever
588
+ resolved. Scheme and port are ignored, so a TLS-terminating proxy that
589
+ forwards `Host` unchanged needs no configuration.
590
+ An `Origin` that cannot be parsed is refused the same way (403), not
591
+ answered with a 500.
592
+ - Every response carries `X-Content-Type-Options: nosniff` and
593
+ `X-Frame-Options: DENY`: a browser renders nothing as a type the server
594
+ did not declare, and no page elsewhere can frame the UI. The UI itself
595
+ carries no Content-Security-Policy yet.
596
+ - `/outputs` and `/inputs` share the UI's origin, where the API token lives
597
+ in localStorage, so a file served as an active document type
598
+ (`text/html`, `application/xhtml+xml`, `text/xml`, `application/xml`,
599
+ `image/svg+xml`) carries `Content-Security-Policy: sandbox`: it opens
600
+ under an opaque origin with no script. Range and ETag answers are
601
+ unchanged. The engine does not write `text/html` or `text/xml` results
602
+ at all (below), so such a file is one planted on disk.
603
+ - Requests carrying a `Host` header that names neither a loopback address
604
+ nor the configured `--host` are rejected (400). A wildcard bind
605
+ (`--host 0.0.0.0` or `::`) skips this check - clients reach such a
606
+ server by the machine's LAN IP or hostname, never by the bind address,
607
+ so there is no allowlist to build from it. This is defense-in-depth,
608
+ not the DNS-rebinding fix by itself - the `Origin` check above already
609
+ covers browser requests, since a browser's `Origin` reflects the real
610
+ requesting origin regardless of what DNS name resolved to this address.
611
+ The `Host` check closes the remaining gap: a non-browser client (curl, a
612
+ script, the MCP client) that never sends `Origin` at all.
613
+ - Every path from HTTP input goes through `dw/security.py` validation;
614
+ workflow files (both the `/api/workflows` CRUD routes and a
615
+ `workflow_path` given to `/api/jobs` or `/api/validate`) are confined to
616
+ the workflow directory, prompt files to the prompt directory, outputs to
617
+ the output directory, and traversal (`../`) is blocked throughout.
618
+ - Inline workflow definitions are schema-validated before queueing, and
619
+ their `base_dir` is validated like any other path input.
620
+ - A workflow JSON file can execute arbitrary Python (`pre_load_modules`,
621
+ dotted `*_type`/`config_type` values - see [Trust
622
+ model](SECURITY.md#trust-model)). `dw-serve` refuses that surface by
623
+ default for every job it runs, inline or from a file, MCP-submitted or
624
+ not; `--trust-workflows` lifts the refusal for the whole server and
625
+ should only be passed when nothing untrusted can reach `POST /api/jobs`.
626
+ - `--mcp` mounts the MCP tool surface at `/mcp` (Streamable HTTP) behind
627
+ the same token as `/api`, for an agent on another machine with no local
628
+ install. Both `/mcp` and `/mcp/` are answered, and the token is accepted
629
+ only as an `Authorization: Bearer` header there - never as `?token=`.
630
+ It is refused on a non-loopback bind without a token. See
631
+ [REMOTE.md](REMOTE.md).
632
+
633
+ ### Authentication
634
+
635
+ There is no authentication by default - the checks above assume a trusted
636
+ local machine or LAN. An optional static bearer token closes that gap:
637
+
638
+ ```bash
639
+ python -m dw.serve --token "some-long-random-string"
640
+ # or
641
+ export DW_API_TOKEN="some-long-random-string"
642
+ python -m dw.serve
643
+ ```
644
+
645
+ When a token is configured, every `/api/*` request must carry
646
+ `Authorization: Bearer <token>` or gets a 401. The UI's own static files and
647
+ `/outputs` (generated media) stay reachable without it - the page has to
648
+ load far enough for a user to enter the token, and an `<img>`/`<script>`
649
+ tag cannot attach a header anyway. That is why an active document served
650
+ from `/outputs` or `/inputs` is sandboxed (Security model, above). A few GET API routes additionally accept the
651
+ token as a `?token=...` query parameter, because the browser loads them
652
+ without being able to set headers: the SSE stream,
653
+ `GET /api/jobs/{id}/events` (`EventSource`), and the gallery grid's
654
+ `GET /api/gallery/{name}/thumbnail` (an `<img>` tag). The three `/download`
655
+ routes (gallery output, workflow, prompt) accept `?token=...` the same way,
656
+ since a download button is a plain `<a href download>` navigation that
657
+ cannot set a header either. That is a deliberate,
658
+ narrower trade-off (a token that can leak into logs or browser history for
659
+ those URLs) rather than a general alternative to the header - every other
660
+ route accepts the header only.
661
+
662
+ The web UI has a one-time token field (next to the theme toggle) that
663
+ stores the token in `localStorage` and attaches it to every API call,
664
+ including the two query-parameter routes above. The MCP server reads the
665
+ same `DW_API_TOKEN` variable (or `dw-mcp --token`), so one export
666
+ configures both ends - see [MCP.md](MCP.md). It is a convenience, not a
667
+ credential vault - anyone with access to the browser profile can read it
668
+ back out of `localStorage`.
669
+
670
+ A token configured this way is a single shared static secret, not a login
671
+ system: there is one token, checked with a constant-time comparison, and no
672
+ notion of separate users or sessions. It raises the bar for exposing the
673
+ server on a LAN or beyond; it is not a substitute for a real network
674
+ boundary (a firewall, a VPN, or simply binding to `127.0.0.1`) for anything
675
+ more exposed than that.
676
+
677
+ Running on another machine: [REMOTE.md](REMOTE.md) is the end-to-end recipe
678
+ - token, firewall, systemd unit, the browser, `--mcp`, and what to do beyond
679
+ the LAN.