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
@@ -0,0 +1,115 @@
1
+ """Static validation for select's arguments (docs/proposals/score-and-select.md).
2
+
3
+ select's own signature carries no domain - `rule` is any string, and
4
+ `threshold`/`index` are only meaningful for some rules - so a step whose
5
+ rule is misspelled, or whose threshold is missing, or whose index rule has
6
+ none, validates clean today and dies on select's own run-time ValueError
7
+ after the fan-out ahead of it has already generated. Checked here in
8
+ `validation_errors` (free), and unchanged as select's own run-time check
9
+ for a value arriving from a `variable:` or an earlier step, which this
10
+ static pass can never see.
11
+ """
12
+
13
+ from .for_each import MEMBER_SEPARATOR, render_path
14
+
15
+ _RULES = {"argmax", "argmin", "first_above", "first_below", "index"}
16
+ _THRESHOLD_RULES = {"first_above", "first_below"}
17
+
18
+
19
+ def _where(name):
20
+ return (
21
+ f" in member '{name}'"
22
+ if isinstance(name, str) and MEMBER_SEPARATOR in name
23
+ else ""
24
+ )
25
+
26
+
27
+ def _is_gather(value):
28
+ return isinstance(value, str) and value.startswith("gather:")
29
+
30
+
31
+ def _is_expanded_gather(value):
32
+ """A gather: reference after for_each expansion has replaced it with the
33
+ list of previous_result: references it drew from - the only form
34
+ select_errors ever actually sees once validation_errors expands the
35
+ definition before calling it."""
36
+ return (
37
+ isinstance(value, list)
38
+ and len(value) > 0
39
+ and all(isinstance(v, str) and v.startswith("previous_result:") for v in value)
40
+ )
41
+
42
+
43
+ def select_errors(workflow_definition, source_indices=None):
44
+ """Every select step whose arguments cannot be right, as [{path, message}]."""
45
+ steps = workflow_definition.get("steps")
46
+ if not isinstance(steps, list):
47
+ return []
48
+
49
+ errors = []
50
+ for index, step in enumerate(steps):
51
+ if not isinstance(step, dict):
52
+ continue
53
+ task = step.get("task")
54
+ if not isinstance(task, dict) or task.get("command") != "select":
55
+ continue
56
+ arguments = task.get("arguments")
57
+ if not isinstance(arguments, dict):
58
+ continue
59
+
60
+ source = (
61
+ source_indices[index]
62
+ if source_indices is not None and index < len(source_indices)
63
+ else index
64
+ )
65
+ name = step.get("name")
66
+ where = _where(name)
67
+
68
+ def add(key, message):
69
+ path = render_path(("steps", source, "task", "arguments", key))
70
+ errors.append({"path": path, "message": f"{message}{where}."})
71
+
72
+ rule = arguments.get("rule")
73
+ if isinstance(rule, str) and rule not in _RULES:
74
+ add("rule", f"select: unknown rule: {rule!r}")
75
+ elif isinstance(rule, str):
76
+ has_threshold = "threshold" in arguments
77
+ if rule in _THRESHOLD_RULES and not has_threshold:
78
+ add("threshold", f"select rule '{rule}' requires a threshold")
79
+ elif rule not in _THRESHOLD_RULES and has_threshold:
80
+ add(
81
+ "threshold",
82
+ f"select: 'threshold' is only meaningful for rule "
83
+ f"'first_above'/'first_below', not {rule!r}",
84
+ )
85
+
86
+ has_index = "index" in arguments
87
+ if rule == "index" and not has_index:
88
+ add("index", "select rule 'index' requires an index")
89
+ elif rule != "index" and has_index:
90
+ add(
91
+ "index",
92
+ f"select: 'index' is only meaningful for rule 'index', "
93
+ f"not {rule!r}",
94
+ )
95
+
96
+ candidates = arguments.get("candidates")
97
+ scores = arguments.get("scores")
98
+ if "candidates" in arguments and "scores" in arguments:
99
+ if _is_gather(candidates) != _is_gather(scores):
100
+ add(
101
+ "candidates",
102
+ "select: 'candidates' and 'scores' must both be "
103
+ "'gather:' references or both plain lists, got "
104
+ f"candidates={candidates!r} scores={scores!r}",
105
+ )
106
+ elif _is_expanded_gather(candidates) and _is_expanded_gather(scores):
107
+ if len(candidates) != len(scores):
108
+ add(
109
+ "scores",
110
+ "select: 'candidates' and 'scores' gather from "
111
+ f"for_each groups of different sizes: candidates has "
112
+ f"{len(candidates)} entries, scores has {len(scores)}",
113
+ )
114
+
115
+ return errors
dw/serve.py ADDED
@@ -0,0 +1,277 @@
1
+ """Run the workflow engine as a local HTTP server.
2
+
3
+ python -m dw.serve
4
+ python -m dw.serve --port 8765 --workflow-dir ./workflows
5
+
6
+ Binds to localhost by default - this serves your GPU to your own tools,
7
+ not to the network. Interactive API docs at http://127.0.0.1:8765/docs
8
+ """
9
+
10
+ import argparse
11
+ import multiprocessing
12
+ import os
13
+ import sys
14
+
15
+ # Spawn start method before anything touches multiprocessing (CUDA/MPS)
16
+ if multiprocessing.get_start_method(allow_none=True) != "spawn":
17
+ try:
18
+ multiprocessing.set_start_method("spawn", force=True)
19
+ except RuntimeError:
20
+ pass
21
+
22
+
23
+ def main():
24
+ parser = argparse.ArgumentParser(description="Serve diffusers workflows over HTTP.")
25
+ parser.add_argument(
26
+ "--host", default="127.0.0.1", help="Bind address (default: 127.0.0.1)"
27
+ )
28
+ parser.add_argument("--port", type=int, default=8765, help="Port (default: 8765)")
29
+ parser.add_argument(
30
+ "--workspace",
31
+ default=None,
32
+ help="Directory holding the workflows, prompts, assets and outputs "
33
+ "this server serves (default: DW_WORKSPACE, else the 'workspace' "
34
+ "setting, else the working directory when it looks like a "
35
+ "workspace, else ~/diffusers-workspace). --workflow-dir, "
36
+ "--output-dir and --prompt-dir each override one of its folders",
37
+ )
38
+ parser.add_argument(
39
+ "--workflow-dir",
40
+ default=None,
41
+ help="Directory of workflow JSON files (default: the workspace's workflows/)",
42
+ )
43
+ parser.add_argument(
44
+ "--output-dir",
45
+ default=None,
46
+ help="Directory results are written to (default: the workspace's outputs/)",
47
+ )
48
+ parser.add_argument(
49
+ "--prompt-dir",
50
+ default=None,
51
+ help="Directory of stored prompt files (default: discovered the way "
52
+ "a CLI run discovers it - DW_PROMPT_DIR, else ./prompts if it "
53
+ "exists, else the nearest prompts/ above the workflow directory)",
54
+ )
55
+ parser.add_argument(
56
+ "--examples-dir",
57
+ action="append",
58
+ default=None,
59
+ dest="examples_dirs",
60
+ metavar="DIR",
61
+ help="A read-only directory of workflows to offer alongside the "
62
+ "workspace's own - a checkout's workflows/ tree, say. Repeatable. "
63
+ "Saves never go here: they always land in --workflow-dir. The "
64
+ "prompts/ and assets/ beside such a tree come with it, read-only, "
65
+ "at the back of the prompt and asset libraries, so an example runs "
66
+ "with the prompts and media it references",
67
+ )
68
+ parser.add_argument(
69
+ "--asset-dir",
70
+ default=None,
71
+ help="Directory of input media 'asset:' references resolve against, "
72
+ "and where browser uploads are saved (default: the workspace's "
73
+ "assets/)",
74
+ )
75
+ parser.add_argument(
76
+ "--output-layout",
77
+ choices=("run", "flat"),
78
+ default=None,
79
+ help="'run' (default) gives each job its own directory under "
80
+ "<output_dir>/<workflow>/, with a manifest.json beside its files; "
81
+ "'flat' writes the way it did before run directories",
82
+ )
83
+ parser.add_argument(
84
+ "-l",
85
+ "--log_level",
86
+ default="INFO",
87
+ help="DEBUG, INFO, WARNING, ERROR, CRITICAL",
88
+ )
89
+ parser.add_argument(
90
+ "--token",
91
+ default=None,
92
+ help="Static bearer token required on every /api/ request "
93
+ "(default: DW_API_TOKEN env var, else no authentication)",
94
+ )
95
+ parser.add_argument(
96
+ "--trust-workflows",
97
+ action="store_true",
98
+ default=False,
99
+ help="Trust every workflow this server loads - inline (from a "
100
+ "POST /api/jobs body, e.g. MCP-authored), a stored one, or one at "
101
+ "an arbitrary path - to execute arbitrary Python: allow "
102
+ "pre_load_modules and any dotted *_type/*_dtype/dtype/config_type "
103
+ "value, not just ones inside the diffusers/torch/transformers/"
104
+ "quantization-backend ecosystem the tool already depends on. Off "
105
+ "by default - see docs/SECURITY.md's Trust model. A server "
106
+ "anything can submit jobs to (including an MCP client) should "
107
+ "stay untrusted.",
108
+ )
109
+ parser.add_argument(
110
+ "--mcp",
111
+ action="store_true",
112
+ default=False,
113
+ help="Also serve the MCP tool surface at /mcp over Streamable HTTP, "
114
+ "behind the same token as /api, so an agent on another machine "
115
+ "needs no local install (`claude mcp add --transport http dw "
116
+ "http://<host>:<port>/mcp --header 'Authorization: Bearer <token>'`). "
117
+ "Refused on a non-loopback --host without a token.",
118
+ )
119
+ args = parser.parse_args()
120
+
121
+ # Resolved and pinned before anything derives a directory from it - the
122
+ # spawned worker inherits the environment variable, the way it inherits
123
+ # the prompt directory and the trust flag below
124
+ from .workspace import PROMPTS_SUBDIR, resolve_workspace, set_workspace
125
+
126
+ workspace = set_workspace(resolve_workspace(args.workspace))
127
+ workflow_dir = args.workflow_dir or workspace.workflows
128
+ output_dir = args.output_dir or workspace.outputs
129
+ asset_dir = os.path.abspath(args.asset_dir or workspace.assets)
130
+
131
+ # A workspace's workflow folder is where the UI and MCP clients save, so
132
+ # it has to exist for a first run in a fresh workspace. Only the folder
133
+ # actually defaulted to is created - an explicit --workflow-dir that does
134
+ # not exist stays the operator's typo, not a new empty directory. The
135
+ # output folder is created by the job manager on the same reasoning
136
+ if not args.workflow_dir:
137
+ os.makedirs(workflow_dir, exist_ok=True)
138
+
139
+ # Created either way: it is the upload destination and a static mount,
140
+ # both of which need it to exist before the first request
141
+ os.makedirs(asset_dir, exist_ok=True)
142
+
143
+ # Pinned like the prompt directory, so 'asset:' resolves to the same
144
+ # library in the worker that the upload route writes into
145
+ os.environ["DW_ASSET_DIR"] = asset_dir
146
+
147
+ if args.output_layout:
148
+ from .runs import set_output_layout
149
+
150
+ set_output_layout(args.output_layout)
151
+
152
+ token = args.token or os.environ.get("DW_API_TOKEN") or None
153
+
154
+ from .server.app import LOOPBACK_HOSTS
155
+
156
+ # A hard error, where the REST-only case below is a warning: an MCP
157
+ # endpoint can author and run workflows, and unlike the web UI there is
158
+ # no page to paste a token into. Raised before startup() and before the
159
+ # worker subprocess is ever spawned.
160
+ if args.mcp and args.host not in LOOPBACK_HOSTS and not token:
161
+ print(
162
+ f"dw-serve: --mcp on {args.host} needs a token. An MCP endpoint "
163
+ "can author and run workflows, and unlike the web UI there is "
164
+ "no page to type a token into - pass --token or set "
165
+ "DW_API_TOKEN, or bind to 127.0.0.1.",
166
+ file=sys.stderr,
167
+ )
168
+ raise SystemExit(2)
169
+
170
+ # Set before create_app / before the worker subprocess is ever spawned -
171
+ # 'spawn' launches a fresh interpreter that inherits this environment
172
+ # variable, so the job runner sees the same trust choice the API does
173
+ from .security import set_trust_workflows
174
+
175
+ set_trust_workflows(args.trust_workflows)
176
+
177
+ # Resolve the prompt directory once, with the same discovery a CLI run
178
+ # uses, anchored at the workflow directory - then pin it, so the library
179
+ # the Prompts page edits and the one 'prompt:' references resolve against
180
+ # are always the same directory. The spawned worker inherits the variable.
181
+ from .prompts import get_prompt_dir
182
+
183
+ prompt_dir = os.path.abspath(
184
+ args.prompt_dir or get_prompt_dir(base_dir=os.path.abspath(workflow_dir))
185
+ )
186
+ os.environ["DW_PROMPT_DIR"] = prompt_dir
187
+
188
+ # An --examples-dir tree brings the prompts and assets its workflows
189
+ # reference along with it, and those live beside the tree rather than in
190
+ # this workspace. Putting them on the read-only end of each library's
191
+ # search path is what lets an example run as it shipped: the workspace's
192
+ # own library is still searched first and is still the only one written
193
+ # to. Pinned in the environment, so the worker resolves as the API does
194
+ from .workspace import (
195
+ ASSETS_SUBDIR,
196
+ WORKFLOWS_SUBDIR,
197
+ example_libraries,
198
+ set_library_fallbacks,
199
+ )
200
+
201
+ example_dirs = example_libraries(args.examples_dirs)
202
+ set_library_fallbacks(PROMPTS_SUBDIR, example_dirs[PROMPTS_SUBDIR])
203
+ # The workflow trees themselves, so a sub-workflow step can compose a
204
+ # stored template by the name list_workflows reports rather than a copy
205
+ # of it in this workspace (#90)
206
+ set_library_fallbacks(WORKFLOWS_SUBDIR, args.examples_dirs)
207
+ # The shared library goes ahead of the examples and behind the
208
+ # workspace's own, which is the order 'asset:' resolves in: a workspace
209
+ # name shadows a shared one, and a shared one shadows an example's.
210
+ # Created here rather than on first use so it is on the path the worker
211
+ # inherited at startup, whenever the first shared asset lands
212
+ common_assets = workspace.common_assets
213
+ if common_assets:
214
+ os.makedirs(common_assets, exist_ok=True)
215
+ set_library_fallbacks(
216
+ ASSETS_SUBDIR,
217
+ ([common_assets] if common_assets else []) + example_dirs[ASSETS_SUBDIR],
218
+ )
219
+
220
+ try:
221
+ import uvicorn
222
+ except ImportError:
223
+ print(
224
+ "The server needs fastapi and uvicorn: pip install fastapi 'uvicorn[standard]'"
225
+ )
226
+ raise SystemExit(1)
227
+
228
+ from . import startup
229
+
230
+ startup(args.log_level)
231
+
232
+ import logging
233
+
234
+ logger = logging.getLogger("dw")
235
+
236
+ if args.host not in LOOPBACK_HOSTS and not token:
237
+ logger.warning(
238
+ "Binding to %s with no API token configured (--token or "
239
+ "DW_API_TOKEN) - anything that can reach this address can "
240
+ "queue jobs, read and write workflows/prompts, and browse "
241
+ "generated output. Set a token, or bind to 127.0.0.1 if this "
242
+ "server does not need to be reachable off this machine.",
243
+ args.host,
244
+ )
245
+
246
+ from .server.app import create_app
247
+
248
+ from .server.app import default_ui_dir
249
+
250
+ app = create_app(
251
+ # absolute, so the path the UI hands back on submit is unambiguous
252
+ workflow_dir=os.path.abspath(workflow_dir),
253
+ output_dir=output_dir,
254
+ log_level=args.log_level,
255
+ prompt_dir=prompt_dir,
256
+ asset_dir=asset_dir,
257
+ examples_dirs=args.examples_dirs,
258
+ workspace=workspace.root,
259
+ host=args.host,
260
+ token=token,
261
+ mcp=args.mcp,
262
+ port=args.port,
263
+ )
264
+ ui = " - UI at /" if default_ui_dir() else ""
265
+ mcp = " - MCP at /mcp" if args.mcp else ""
266
+ print(
267
+ f"diffusers-workflow server on http://{args.host}:{args.port}"
268
+ f" (docs at /docs{ui}{mcp})",
269
+ # stdout is a pipe under systemd or nohup, where block buffering
270
+ # would otherwise hold this line back until shutdown
271
+ flush=True,
272
+ )
273
+ uvicorn.run(app, host=args.host, port=args.port, log_level=args.log_level.lower())
274
+
275
+
276
+ if __name__ == "__main__":
277
+ main()
dw/server/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """HTTP server exposing the workflow engine: submit jobs, stream progress,
2
+ cancel, browse workflows and outputs. See dw/serve.py for the entry point."""