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.
- diffusers_workflow-0.4.0.dist-info/METADATA +318 -0
- diffusers_workflow-0.4.0.dist-info/RECORD +260 -0
- diffusers_workflow-0.4.0.dist-info/WHEEL +5 -0
- diffusers_workflow-0.4.0.dist-info/entry_points.txt +7 -0
- diffusers_workflow-0.4.0.dist-info/licenses/LICENSE +201 -0
- diffusers_workflow-0.4.0.dist-info/top_level.txt +2 -0
- dw/__init__.py +440 -0
- dw/adapter_compatibility.py +226 -0
- dw/arguments.py +1231 -0
- dw/assessment_rules.py +159 -0
- dw/assets.py +130 -0
- dw/cache_blocks.json +16 -0
- dw/cache_blocks.py +146 -0
- dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
- dw/content_types.py +150 -0
- dw/dissolve_frame_errors.py +121 -0
- dw/docs/ACCELERATION.md +352 -0
- dw/docs/AGENT_LOOP.md +95 -0
- dw/docs/DEPENDENCIES.md +91 -0
- dw/docs/IP_ADAPTER.md +109 -0
- dw/docs/LORAS.md +131 -0
- dw/docs/MCP.md +517 -0
- dw/docs/PROMPT_WEIGHTING.md +78 -0
- dw/docs/QUANTIZATION.md +230 -0
- dw/docs/RECIPES_24GB.md +201 -0
- dw/docs/RELEASING.md +195 -0
- dw/docs/REMOTE.md +140 -0
- dw/docs/REPL_COMMANDS.md +121 -0
- dw/docs/REPL_WORKER_GUIDE.md +51 -0
- dw/docs/SECURITY.md +272 -0
- dw/docs/SECURITY_QUICKREF.md +112 -0
- dw/docs/SERVER.md +679 -0
- dw/docs/TASKS.md +1741 -0
- dw/docs/TESTING.md +71 -0
- dw/docs/WORKFLOW_GUIDE.md +2038 -0
- dw/docs/WORKSPACES.md +316 -0
- dw/download_watch.py +335 -0
- dw/elision.py +306 -0
- dw/events.py +275 -0
- dw/for_each.py +409 -0
- dw/host_memory.py +258 -0
- dw/host_memory_projection.py +230 -0
- dw/hub_cache.py +432 -0
- dw/introspection.py +1228 -0
- dw/kernel_availability.py +208 -0
- dw/locations.py +599 -0
- dw/log_setup.py +45 -0
- dw/loudness.py +82 -0
- dw/media_audio.py +217 -0
- dw/media_frames.py +367 -0
- dw/media_info.py +297 -0
- dw/pipeline_processors/chain.py +821 -0
- dw/pipeline_processors/config_objects.py +237 -0
- dw/pipeline_processors/pipeline.py +2297 -0
- dw/pipeline_processors/remote.py +46 -0
- dw/plan.py +920 -0
- dw/previous_results.py +411 -0
- dw/probe_paths.py +59 -0
- dw/prompt_schema.json +48 -0
- dw/prompt_weighting.py +378 -0
- dw/prompts.py +159 -0
- dw/realize.py +250 -0
- dw/reference_limits.py +215 -0
- dw/reference_names.py +125 -0
- dw/repl.py +338 -0
- dw/repl_commands.py +836 -0
- dw/repl_worker.py +159 -0
- dw/result.py +1720 -0
- dw/result_fps.py +82 -0
- dw/run.py +162 -0
- dw/runs.py +768 -0
- dw/scalar_result_validation.py +97 -0
- dw/schema.py +283 -0
- dw/security.py +1038 -0
- dw/select_validation.py +115 -0
- dw/serve.py +277 -0
- dw/server/__init__.py +2 -0
- dw/server/app.py +4586 -0
- dw/server/assess.py +132 -0
- dw/server/catalog_shape.py +487 -0
- dw/server/enhancers.py +129 -0
- dw/server/exports.py +480 -0
- dw/server/guides.py +257 -0
- dw/server/jobs.py +1561 -0
- dw/server/mcp_mount.py +95 -0
- dw/server/netinfo.py +124 -0
- dw/server/observed_cost.py +379 -0
- dw/server/sysinfo.py +71 -0
- dw/server/ui/assets/abap-08VXUWAP.js +1 -0
- dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
- dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
- dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
- dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
- dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
- dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
- dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
- dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
- dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
- dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
- dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
- dw/server/ui/assets/css-DIMkf-bt.js +3 -0
- dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
- dw/server/ui/assets/cssMode-CPznxfY8.js +1 -0
- dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
- dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
- dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
- dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
- dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
- dw/server/ui/assets/editor.api-CpWcotrd.js +847 -0
- dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
- dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
- dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
- dw/server/ui/assets/freemarker2-CXtRM8N4.js +3 -0
- dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
- dw/server/ui/assets/go-C-y9NEjX.js +1 -0
- dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
- dw/server/ui/assets/handlebars-N7x-6NMY.js +1 -0
- dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
- dw/server/ui/assets/html-PhsdjHSr.js +1 -0
- dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
- dw/server/ui/assets/htmlMode-Dgj0SEok.js +1 -0
- dw/server/ui/assets/index-3Vw6WAPW.css +1 -0
- dw/server/ui/assets/index-DgrYhQd9.js +43 -0
- dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
- dw/server/ui/assets/java-BEtHBSE6.js +1 -0
- dw/server/ui/assets/javascript-BJqN9Qhv.js +1 -0
- dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
- dw/server/ui/assets/jsonMode-DbM4SWSv.js +7 -0
- dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
- dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
- dw/server/ui/assets/less-B9JPFI3C.js +2 -0
- dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
- dw/server/ui/assets/liquid-BWr8lEc4.js +1 -0
- dw/server/ui/assets/lspLanguageFeatures-C1iGuDyZ.js +4 -0
- dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
- dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
- dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
- dw/server/ui/assets/mdx-DAdMi_0p.js +1 -0
- dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
- dw/server/ui/assets/monaco--ixms01u.css +1 -0
- dw/server/ui/assets/monaco-BGCeEqaw.js +56 -0
- dw/server/ui/assets/msdax-DauUninz.js +1 -0
- dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
- dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
- dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
- dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
- dw/server/ui/assets/perl-oz_6vUea.js +1 -0
- dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
- dw/server/ui/assets/php-nr791fC2.js +1 -0
- dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
- dw/server/ui/assets/postiats-43DmfD33.js +1 -0
- dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
- dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
- dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
- dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
- dw/server/ui/assets/python-Bcn70HdC.js +1 -0
- dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
- dw/server/ui/assets/r-BwWrilGY.js +1 -0
- dw/server/ui/assets/razor-D1HmNnby.js +1 -0
- dw/server/ui/assets/redis-ClamHrr6.js +1 -0
- dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
- dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
- dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
- dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
- dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
- dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
- dw/server/ui/assets/scheme-BeGwcela.js +1 -0
- dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
- dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
- dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
- dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
- dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
- dw/server/ui/assets/sql-NEE52Syq.js +1 -0
- dw/server/ui/assets/st-DbInun42.js +1 -0
- dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
- dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
- dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
- dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
- dw/server/ui/assets/tsMode-D6u0XmOW.js +11 -0
- dw/server/ui/assets/twig-De2hgUGE.js +1 -0
- dw/server/ui/assets/typescript-BU6v-LMV.js +1 -0
- dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
- dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
- dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
- dw/server/ui/assets/workers-Cn7cTUKr.js +1 -0
- dw/server/ui/assets/xml--0LP2Lwk.js +1 -0
- dw/server/ui/assets/yaml-mpBg9jnt.js +1 -0
- dw/server/ui/index.html +17 -0
- dw/server/updater.py +192 -0
- dw/settings.py +98 -0
- dw/shot_span_preflight.py +116 -0
- dw/shots.py +359 -0
- dw/slice_preflight.py +148 -0
- dw/step.py +187 -0
- dw/step_cache.py +442 -0
- dw/subfolders.py +107 -0
- dw/task_domains.py +307 -0
- dw/tasks/assess.py +826 -0
- dw/tasks/audio_transcription.py +88 -0
- dw/tasks/audio_utils.py +1862 -0
- dw/tasks/background_remover.py +43 -0
- dw/tasks/borders.py +113 -0
- dw/tasks/compose_text.py +74 -0
- dw/tasks/concat_videos.py +300 -0
- dw/tasks/depth_estimator.py +54 -0
- dw/tasks/diffusion_upscale.py +109 -0
- dw/tasks/dissolve_videos.py +342 -0
- dw/tasks/format_messages.py +24 -0
- dw/tasks/gather.py +173 -0
- dw/tasks/grade.py +97 -0
- dw/tasks/image_to_text.py +43 -0
- dw/tasks/image_utils.py +764 -0
- dw/tasks/interpolate_frames.py +252 -0
- dw/tasks/judge.py +68 -0
- dw/tasks/model_cache.py +55 -0
- dw/tasks/pair_audio.py +268 -0
- dw/tasks/qr_code.py +19 -0
- dw/tasks/restore_faces.py +175 -0
- dw/tasks/rife_model.py +192 -0
- dw/tasks/segment.py +121 -0
- dw/tasks/select.py +111 -0
- dw/tasks/speech_generation.py +228 -0
- dw/tasks/stabilize.py +129 -0
- dw/tasks/task.py +920 -0
- dw/tasks/tensor_image.py +57 -0
- dw/tasks/text_generation.py +169 -0
- dw/tasks/text_sections.py +80 -0
- dw/tasks/upscale.py +203 -0
- dw/tasks/video_utils.py +624 -0
- dw/tasks/zoe_depth.py +71 -0
- dw/teacache.py +381 -0
- dw/teacache_models.json +99 -0
- dw/test.py +29 -0
- dw/type_helpers.py +231 -0
- dw/validate.py +68 -0
- dw/variable_constraints.py +444 -0
- dw/variables.py +443 -0
- dw/video_extensions.py +141 -0
- dw/vram_estimate.py +116 -0
- dw/worker.py +764 -0
- dw/workflow.py +2007 -0
- dw/workflow_schema.json +1346 -0
- dw/workflow_sources.py +383 -0
- dw/workflows/h3_context_ir.json +57 -0
- dw/workflows/test.json +31 -0
- dw/workspace.py +730 -0
- dw_mcp/__init__.py +6 -0
- dw_mcp/__main__.py +133 -0
- dw_mcp/assets.py +336 -0
- dw_mcp/authoring.py +114 -0
- dw_mcp/catalog.py +360 -0
- dw_mcp/client.py +486 -0
- dw_mcp/diagnose.py +371 -0
- dw_mcp/exports.py +84 -0
- dw_mcp/guides.py +35 -0
- dw_mcp/media.py +638 -0
- dw_mcp/models.py +97 -0
- dw_mcp/prompts.py +104 -0
- dw_mcp/server.py +1343 -0
- 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
|
+

|
|
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.
|