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/select_validation.py
ADDED
|
@@ -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