diffusers-workflow 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. diffusers_workflow-0.4.0.dist-info/METADATA +318 -0
  2. diffusers_workflow-0.4.0.dist-info/RECORD +260 -0
  3. diffusers_workflow-0.4.0.dist-info/WHEEL +5 -0
  4. diffusers_workflow-0.4.0.dist-info/entry_points.txt +7 -0
  5. diffusers_workflow-0.4.0.dist-info/licenses/LICENSE +201 -0
  6. diffusers_workflow-0.4.0.dist-info/top_level.txt +2 -0
  7. dw/__init__.py +440 -0
  8. dw/adapter_compatibility.py +226 -0
  9. dw/arguments.py +1231 -0
  10. dw/assessment_rules.py +159 -0
  11. dw/assets.py +130 -0
  12. dw/cache_blocks.json +16 -0
  13. dw/cache_blocks.py +146 -0
  14. dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
  15. dw/content_types.py +150 -0
  16. dw/dissolve_frame_errors.py +121 -0
  17. dw/docs/ACCELERATION.md +352 -0
  18. dw/docs/AGENT_LOOP.md +95 -0
  19. dw/docs/DEPENDENCIES.md +91 -0
  20. dw/docs/IP_ADAPTER.md +109 -0
  21. dw/docs/LORAS.md +131 -0
  22. dw/docs/MCP.md +517 -0
  23. dw/docs/PROMPT_WEIGHTING.md +78 -0
  24. dw/docs/QUANTIZATION.md +230 -0
  25. dw/docs/RECIPES_24GB.md +201 -0
  26. dw/docs/RELEASING.md +195 -0
  27. dw/docs/REMOTE.md +140 -0
  28. dw/docs/REPL_COMMANDS.md +121 -0
  29. dw/docs/REPL_WORKER_GUIDE.md +51 -0
  30. dw/docs/SECURITY.md +272 -0
  31. dw/docs/SECURITY_QUICKREF.md +112 -0
  32. dw/docs/SERVER.md +679 -0
  33. dw/docs/TASKS.md +1741 -0
  34. dw/docs/TESTING.md +71 -0
  35. dw/docs/WORKFLOW_GUIDE.md +2038 -0
  36. dw/docs/WORKSPACES.md +316 -0
  37. dw/download_watch.py +335 -0
  38. dw/elision.py +306 -0
  39. dw/events.py +275 -0
  40. dw/for_each.py +409 -0
  41. dw/host_memory.py +258 -0
  42. dw/host_memory_projection.py +230 -0
  43. dw/hub_cache.py +432 -0
  44. dw/introspection.py +1228 -0
  45. dw/kernel_availability.py +208 -0
  46. dw/locations.py +599 -0
  47. dw/log_setup.py +45 -0
  48. dw/loudness.py +82 -0
  49. dw/media_audio.py +217 -0
  50. dw/media_frames.py +367 -0
  51. dw/media_info.py +297 -0
  52. dw/pipeline_processors/chain.py +821 -0
  53. dw/pipeline_processors/config_objects.py +237 -0
  54. dw/pipeline_processors/pipeline.py +2297 -0
  55. dw/pipeline_processors/remote.py +46 -0
  56. dw/plan.py +920 -0
  57. dw/previous_results.py +411 -0
  58. dw/probe_paths.py +59 -0
  59. dw/prompt_schema.json +48 -0
  60. dw/prompt_weighting.py +378 -0
  61. dw/prompts.py +159 -0
  62. dw/realize.py +250 -0
  63. dw/reference_limits.py +215 -0
  64. dw/reference_names.py +125 -0
  65. dw/repl.py +338 -0
  66. dw/repl_commands.py +836 -0
  67. dw/repl_worker.py +159 -0
  68. dw/result.py +1720 -0
  69. dw/result_fps.py +82 -0
  70. dw/run.py +162 -0
  71. dw/runs.py +768 -0
  72. dw/scalar_result_validation.py +97 -0
  73. dw/schema.py +283 -0
  74. dw/security.py +1038 -0
  75. dw/select_validation.py +115 -0
  76. dw/serve.py +277 -0
  77. dw/server/__init__.py +2 -0
  78. dw/server/app.py +4586 -0
  79. dw/server/assess.py +132 -0
  80. dw/server/catalog_shape.py +487 -0
  81. dw/server/enhancers.py +129 -0
  82. dw/server/exports.py +480 -0
  83. dw/server/guides.py +257 -0
  84. dw/server/jobs.py +1561 -0
  85. dw/server/mcp_mount.py +95 -0
  86. dw/server/netinfo.py +124 -0
  87. dw/server/observed_cost.py +379 -0
  88. dw/server/sysinfo.py +71 -0
  89. dw/server/ui/assets/abap-08VXUWAP.js +1 -0
  90. dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
  91. dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  92. dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
  93. dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
  94. dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
  95. dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
  96. dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
  97. dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
  98. dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
  99. dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
  100. dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
  101. dw/server/ui/assets/css-DIMkf-bt.js +3 -0
  102. dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
  103. dw/server/ui/assets/cssMode-CPznxfY8.js +1 -0
  104. dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
  105. dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
  106. dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  107. dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
  108. dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
  109. dw/server/ui/assets/editor.api-CpWcotrd.js +847 -0
  110. dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
  111. dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
  112. dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
  113. dw/server/ui/assets/freemarker2-CXtRM8N4.js +3 -0
  114. dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  115. dw/server/ui/assets/go-C-y9NEjX.js +1 -0
  116. dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
  117. dw/server/ui/assets/handlebars-N7x-6NMY.js +1 -0
  118. dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
  119. dw/server/ui/assets/html-PhsdjHSr.js +1 -0
  120. dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
  121. dw/server/ui/assets/htmlMode-Dgj0SEok.js +1 -0
  122. dw/server/ui/assets/index-3Vw6WAPW.css +1 -0
  123. dw/server/ui/assets/index-DgrYhQd9.js +43 -0
  124. dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
  125. dw/server/ui/assets/java-BEtHBSE6.js +1 -0
  126. dw/server/ui/assets/javascript-BJqN9Qhv.js +1 -0
  127. dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
  128. dw/server/ui/assets/jsonMode-DbM4SWSv.js +7 -0
  129. dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
  130. dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
  131. dw/server/ui/assets/less-B9JPFI3C.js +2 -0
  132. dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
  133. dw/server/ui/assets/liquid-BWr8lEc4.js +1 -0
  134. dw/server/ui/assets/lspLanguageFeatures-C1iGuDyZ.js +4 -0
  135. dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
  136. dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
  137. dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
  138. dw/server/ui/assets/mdx-DAdMi_0p.js +1 -0
  139. dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
  140. dw/server/ui/assets/monaco--ixms01u.css +1 -0
  141. dw/server/ui/assets/monaco-BGCeEqaw.js +56 -0
  142. dw/server/ui/assets/msdax-DauUninz.js +1 -0
  143. dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
  144. dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  145. dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
  146. dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  147. dw/server/ui/assets/perl-oz_6vUea.js +1 -0
  148. dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
  149. dw/server/ui/assets/php-nr791fC2.js +1 -0
  150. dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
  151. dw/server/ui/assets/postiats-43DmfD33.js +1 -0
  152. dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
  153. dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
  154. dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
  155. dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
  156. dw/server/ui/assets/python-Bcn70HdC.js +1 -0
  157. dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
  158. dw/server/ui/assets/r-BwWrilGY.js +1 -0
  159. dw/server/ui/assets/razor-D1HmNnby.js +1 -0
  160. dw/server/ui/assets/redis-ClamHrr6.js +1 -0
  161. dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
  162. dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  163. dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
  164. dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
  165. dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
  166. dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
  167. dw/server/ui/assets/scheme-BeGwcela.js +1 -0
  168. dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
  169. dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
  170. dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
  171. dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
  172. dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  173. dw/server/ui/assets/sql-NEE52Syq.js +1 -0
  174. dw/server/ui/assets/st-DbInun42.js +1 -0
  175. dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
  176. dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  177. dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
  178. dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
  179. dw/server/ui/assets/tsMode-D6u0XmOW.js +11 -0
  180. dw/server/ui/assets/twig-De2hgUGE.js +1 -0
  181. dw/server/ui/assets/typescript-BU6v-LMV.js +1 -0
  182. dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
  183. dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
  184. dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
  185. dw/server/ui/assets/workers-Cn7cTUKr.js +1 -0
  186. dw/server/ui/assets/xml--0LP2Lwk.js +1 -0
  187. dw/server/ui/assets/yaml-mpBg9jnt.js +1 -0
  188. dw/server/ui/index.html +17 -0
  189. dw/server/updater.py +192 -0
  190. dw/settings.py +98 -0
  191. dw/shot_span_preflight.py +116 -0
  192. dw/shots.py +359 -0
  193. dw/slice_preflight.py +148 -0
  194. dw/step.py +187 -0
  195. dw/step_cache.py +442 -0
  196. dw/subfolders.py +107 -0
  197. dw/task_domains.py +307 -0
  198. dw/tasks/assess.py +826 -0
  199. dw/tasks/audio_transcription.py +88 -0
  200. dw/tasks/audio_utils.py +1862 -0
  201. dw/tasks/background_remover.py +43 -0
  202. dw/tasks/borders.py +113 -0
  203. dw/tasks/compose_text.py +74 -0
  204. dw/tasks/concat_videos.py +300 -0
  205. dw/tasks/depth_estimator.py +54 -0
  206. dw/tasks/diffusion_upscale.py +109 -0
  207. dw/tasks/dissolve_videos.py +342 -0
  208. dw/tasks/format_messages.py +24 -0
  209. dw/tasks/gather.py +173 -0
  210. dw/tasks/grade.py +97 -0
  211. dw/tasks/image_to_text.py +43 -0
  212. dw/tasks/image_utils.py +764 -0
  213. dw/tasks/interpolate_frames.py +252 -0
  214. dw/tasks/judge.py +68 -0
  215. dw/tasks/model_cache.py +55 -0
  216. dw/tasks/pair_audio.py +268 -0
  217. dw/tasks/qr_code.py +19 -0
  218. dw/tasks/restore_faces.py +175 -0
  219. dw/tasks/rife_model.py +192 -0
  220. dw/tasks/segment.py +121 -0
  221. dw/tasks/select.py +111 -0
  222. dw/tasks/speech_generation.py +228 -0
  223. dw/tasks/stabilize.py +129 -0
  224. dw/tasks/task.py +920 -0
  225. dw/tasks/tensor_image.py +57 -0
  226. dw/tasks/text_generation.py +169 -0
  227. dw/tasks/text_sections.py +80 -0
  228. dw/tasks/upscale.py +203 -0
  229. dw/tasks/video_utils.py +624 -0
  230. dw/tasks/zoe_depth.py +71 -0
  231. dw/teacache.py +381 -0
  232. dw/teacache_models.json +99 -0
  233. dw/test.py +29 -0
  234. dw/type_helpers.py +231 -0
  235. dw/validate.py +68 -0
  236. dw/variable_constraints.py +444 -0
  237. dw/variables.py +443 -0
  238. dw/video_extensions.py +141 -0
  239. dw/vram_estimate.py +116 -0
  240. dw/worker.py +764 -0
  241. dw/workflow.py +2007 -0
  242. dw/workflow_schema.json +1346 -0
  243. dw/workflow_sources.py +383 -0
  244. dw/workflows/h3_context_ir.json +57 -0
  245. dw/workflows/test.json +31 -0
  246. dw/workspace.py +730 -0
  247. dw_mcp/__init__.py +6 -0
  248. dw_mcp/__main__.py +133 -0
  249. dw_mcp/assets.py +336 -0
  250. dw_mcp/authoring.py +114 -0
  251. dw_mcp/catalog.py +360 -0
  252. dw_mcp/client.py +486 -0
  253. dw_mcp/diagnose.py +371 -0
  254. dw_mcp/exports.py +84 -0
  255. dw_mcp/guides.py +35 -0
  256. dw_mcp/media.py +638 -0
  257. dw_mcp/models.py +97 -0
  258. dw_mcp/prompts.py +104 -0
  259. dw_mcp/server.py +1343 -0
  260. dw_mcp/workspaces.py +212 -0
dw/realize.py ADDED
@@ -0,0 +1,250 @@
1
+ """Realizing a workflow: a copy of a definition with every mutable input
2
+ pinned, so the file left beside a run's manifest reproduces that run however
3
+ the library, the catalog or the output tree change afterwards.
4
+
5
+ What "mutable" means here is precisely the set of things that can differ
6
+ between two runs of the same file: the arguments a caller passed, the seed a
7
+ seedless workflow drew, the text a stored prompt held at the time, and which
8
+ run 'output:.../latest/...' picked. Everything else - 'asset:', 'constant:',
9
+ 'previous_result:', 'builtin:' and a sub-workflow's path - is a name whose
10
+ meaning is pinned by something already recorded (the asset library, the
11
+ manifest's dw_version, the file itself), so it is kept as written and, for a
12
+ local sub-workflow, digested into the manifest instead.
13
+
14
+ Two rules hold this module together. It never mutates its input: the caller
15
+ hands it the definition the run is about to work from. And it never fails a
16
+ run: a reference that will not resolve is left exactly as written, so the
17
+ engine raises its own error at the point it would have raised anyway.
18
+ """
19
+
20
+ import copy
21
+ import hashlib
22
+ import logging
23
+ import os
24
+
25
+ from .prompts import PROMPT_PREFIX, fetch_prompt
26
+ from .runs import (
27
+ LATEST,
28
+ OUTPUT_PREFIX,
29
+ is_output_reference,
30
+ output_root as default_output_root,
31
+ resolve_output_reference,
32
+ version_selector,
33
+ )
34
+ from .security import SecurityError, validate_workflow_path
35
+ from .workflow_sources import resolve_sub_workflow, SubWorkflowNotFound
36
+ from .variables import set_variables
37
+
38
+ logger = logging.getLogger("dw")
39
+
40
+ BUILTIN_PREFIX = "builtin:"
41
+ VARIABLE_PREFIX = "variable:"
42
+
43
+
44
+ def realize_workflow(
45
+ definition,
46
+ arguments,
47
+ seed,
48
+ base_dir=None,
49
+ prompt_dir=None,
50
+ output_root=None,
51
+ workflow_dir=None,
52
+ pin_outputs=True,
53
+ ):
54
+ """A copy of `definition` with every mutable input pinned.
55
+
56
+ Args:
57
+ definition: The workflow as loaded, before Workflow.run's deep copy.
58
+ Never mutated.
59
+ arguments: The run's argument dict, folded into the variable defaults
60
+ exactly as `set_variables` folds them for the run itself.
61
+ seed: The seed the run resolved - an integer, never None, because
62
+ `Workflow.run` draws one when the workflow names none.
63
+ base_dir: The workflow file's directory, anchoring prompt discovery
64
+ and a sub-workflow's relative path.
65
+ prompt_dir: The prompt library, for `prompt:` inlining.
66
+ output_root: The output directory `output:` names resolve against.
67
+ workflow_dir: The root a sub-workflow path is confined to, as
68
+ `Workflow` confines it; None for an unconfined CLI run.
69
+ pin_outputs: Whether an 'output:.../latest/...' reference is rewritten
70
+ to the run it resolves to. The run leaves this True; the planner
71
+ (dw/plan.py) passes False so a run finishing between a validate
72
+ call and the queue call does not change the fingerprint of
73
+ identical work.
74
+
75
+ Returns:
76
+ (realized, annotations) - the pinned copy, and
77
+ {"prompts": [name, ...], "sub_workflows": {path: sha256 or None}}
78
+ for the manifest to carry, since the schema has nowhere to put them.
79
+ """
80
+ annotations = {"prompts": [], "sub_workflows": {}}
81
+ realized = copy.deepcopy(definition)
82
+
83
+ variables = realized.get("variables")
84
+ if isinstance(variables, dict):
85
+ # Exactly what the run computed: set_variables coerces each value to
86
+ # the type of the declared default and rejects an undeclared name
87
+ set_variables(arguments or {}, variables)
88
+
89
+ realized["seed"] = seed
90
+ # A definition can point its top-level seed at a declared variable
91
+ # ('"seed": "variable:seed_arg"') rather than an integer, so the run's
92
+ # resolved seed can also be read wherever else the workflow names that
93
+ # variable. Pinning the top-level field alone would leave the variable's
94
+ # own default whatever it was written as (typically none) - and a rerun
95
+ # of this realized copy with no seed argument would put that null
96
+ # default back over the pinned integer everywhere but the top level.
97
+ definition_seed = definition.get("seed")
98
+ if isinstance(definition_seed, str) and definition_seed.startswith(VARIABLE_PREFIX):
99
+ seed_variable = definition_seed.removeprefix(VARIABLE_PREFIX)
100
+ if isinstance(variables, dict) and seed_variable in variables:
101
+ variables[seed_variable] = seed
102
+ realized = _pin(
103
+ realized, annotations, base_dir, prompt_dir, output_root, pin_outputs
104
+ )
105
+ _record_sub_workflows(realized.get("steps"), annotations, base_dir, workflow_dir)
106
+ return realized, annotations
107
+
108
+
109
+ def strings_with_prefix(tree, prefix):
110
+ """Every string in a nested dict/list tree that starts with `prefix`, in
111
+ first-seen order, deduplicated."""
112
+ found = []
113
+
114
+ def collect(value):
115
+ if value.startswith(prefix) and value not in found:
116
+ found.append(value)
117
+ return value
118
+
119
+ _map_strings(tree, collect)
120
+ return found
121
+
122
+
123
+ def _map_strings(value, transform):
124
+ """Rebuild `value`, replacing every string it contains with
125
+ `transform(string)`. One recursive walk over dicts, lists and strings -
126
+ the shape `referenced_result_names` in dw/step_cache.py walks - shared by
127
+ `_pin` (which rewrites matching references) and `strings_with_prefix`
128
+ (which only collects them), so a reference is found wherever it sits: a
129
+ pipeline argument, a task argument, a sub-workflow's argument map, an
130
+ element of a list.
131
+ """
132
+ if isinstance(value, str):
133
+ return transform(value)
134
+ if isinstance(value, dict):
135
+ return {key: _map_strings(item, transform) for key, item in value.items()}
136
+ if isinstance(value, list):
137
+ return [_map_strings(item, transform) for item in value]
138
+ return value
139
+
140
+
141
+ def _pin(value, annotations, base_dir, prompt_dir, output_root, pin_outputs=True):
142
+ """Rebuild a value with prompt references inlined and, when
143
+ `pin_outputs`, output references pinned."""
144
+
145
+ def transform(string):
146
+ if string.startswith(PROMPT_PREFIX):
147
+ return _inline_prompt(string, annotations, prompt_dir, base_dir)
148
+ if pin_outputs and is_output_reference(string):
149
+ return _pin_output(string, output_root)
150
+ return string
151
+
152
+ return _map_strings(value, transform)
153
+
154
+
155
+ def _inline_prompt(reference, annotations, prompt_dir, base_dir):
156
+ """The stored text, and the name recorded for the manifest.
157
+
158
+ A stored prompt's text may not itself begin with a reference prefix (an
159
+ engine rule `fetch_prompt` enforces), so inlining cannot introduce a
160
+ second resolution.
161
+ """
162
+ try:
163
+ text = fetch_prompt(reference, prompt_dir, base_dir)
164
+ except (SecurityError, OSError, ValueError) as e:
165
+ logger.warning(f"Realization kept {reference} as written: {e}")
166
+ return reference
167
+ name = reference.removeprefix(PROMPT_PREFIX).strip()
168
+ if name not in annotations["prompts"]:
169
+ annotations["prompts"].append(name)
170
+ return text
171
+
172
+
173
+ def _pin_output(reference, output_root):
174
+ """'output:<identity>/latest/<file>' - or '/v4/' - rewritten to the run it
175
+ resolved to.
176
+
177
+ A version is stable, but deleting the newest run frees its number for
178
+ reuse, so the realized copy names the run id either way. An explicit run
179
+ id is already pinned, so it is returned untouched without touching the
180
+ disk - realizing must not fail on a reference the run has not reached
181
+ yet.
182
+ """
183
+ name = reference.removeprefix(OUTPUT_PREFIX).strip()
184
+ if not any(
185
+ part == LATEST or version_selector(part) is not None for part in name.split("/")
186
+ ):
187
+ return reference
188
+ root = output_root or default_output_root()
189
+ try:
190
+ resolved = resolve_output_reference(reference, root)
191
+ relative = os.path.relpath(resolved, root).replace(os.sep, "/")
192
+ except (SecurityError, OSError, ValueError) as e:
193
+ logger.warning(f"Realization kept {reference} as written: {e}")
194
+ return reference
195
+ return f"{OUTPUT_PREFIX}{relative}"
196
+
197
+
198
+ def _record_sub_workflows(steps, annotations, base_dir, workflow_dir):
199
+ """Digest every sub-workflow a step names by local path.
200
+
201
+ The schema's 'workflow' step takes a path, not a definition, so the
202
+ realized file keeps the path and the manifest records what the file held.
203
+ A builtin is packaged with the engine and pinned by the manifest's
204
+ dw_version, so it is not digested.
205
+ """
206
+
207
+ def scan(value):
208
+ if isinstance(value, dict):
209
+ reference = value.get("workflow")
210
+ if isinstance(reference, dict):
211
+ path = reference.get("path")
212
+ if isinstance(path, str) and not path.startswith(BUILTIN_PREFIX):
213
+ annotations["sub_workflows"][path] = _digest(
214
+ path, base_dir, workflow_dir
215
+ )
216
+ for item in value.values():
217
+ scan(item)
218
+ elif isinstance(value, list):
219
+ for item in value:
220
+ scan(item)
221
+
222
+ for step in steps or []:
223
+ scan(step)
224
+
225
+
226
+ def read_sub_workflow(path, base_dir, workflow_dir):
227
+ """The bytes of the sub-workflow file a step's `path` names, or None
228
+ when it cannot be read.
229
+
230
+ Resolved the way `Workflow.create_step_action` resolves it - beside the
231
+ referencing file, then across the workflow search path, then through
232
+ `validate_workflow_path` confined to the root it came from - so a path
233
+ this run could not have loaded is not one realization (or the planner)
234
+ reads either, and a catalog name the run composed is read rather than
235
+ recorded as unreadable (#90).
236
+ """
237
+ try:
238
+ candidate, root = resolve_sub_workflow(path, base_dir or ".", workflow_dir)
239
+ validated = validate_workflow_path(candidate, root)
240
+ with open(validated, "rb") as file:
241
+ return file.read()
242
+ except (SecurityError, OSError, ValueError, SubWorkflowNotFound) as e:
243
+ logger.debug(f"Sub-workflow {path} could not be read: {e}")
244
+ return None
245
+
246
+
247
+ def _digest(path, base_dir, workflow_dir):
248
+ """The SHA-256 of a sub-workflow file, or None when it cannot be read."""
249
+ raw = read_sub_workflow(path, base_dir, workflow_dir)
250
+ return hashlib.sha256(raw).hexdigest() if raw is not None else None
dw/reference_limits.py ADDED
@@ -0,0 +1,215 @@
1
+ """How many references of each kind a pipeline's request may carry.
2
+
3
+ A pipeline that conditions on reference media - MiniMax-H3's `ref2va` is the
4
+ one in the catalog - bounds what it accepts: so many images, so many videos,
5
+ so many audio clips, so many in total, and for H3 an audio reference may never
6
+ be the only one. Those bounds are enforced by the pipeline itself, which means
7
+ they are enforced *after* the checkpoint is loaded: a caller who ran the free
8
+ `validate_workflow`, was quoted eight minutes and acknowledged the cost found
9
+ out minutes in, from a failed job, what a millisecond of arithmetic could have
10
+ told them (#136).
11
+
12
+ The numbers are not written here. They live on the diffusers block that
13
+ enforces them, as its constructor's defaults, and this module reads them off
14
+ that block - so a diffusers release that raises a limit raises it here too,
15
+ and the engine holds no model knowledge but the name of the class that
16
+ declares the limits (see REFERENCE_LIMIT_BLOCKS). A family diffusers has no
17
+ such block for is simply not checked: this pass only ever refuses a request
18
+ the pipeline itself would refuse.
19
+ """
20
+
21
+ import importlib
22
+ import inspect
23
+ import logging
24
+
25
+ from .for_each import MEMBER_SEPARATOR, render_path
26
+
27
+ logger = logging.getLogger("dw")
28
+
29
+ # The module a family's reference classes live in -> the block whose
30
+ # __init__ defaults declare that family's limits. A pointer, not a number:
31
+ # what a limit *is* stays diffusers', which is the only place it can stay
32
+ # correct across a release
33
+ REFERENCE_LIMIT_BLOCKS = {
34
+ "diffusers.modular_pipelines.minimax_h3": (
35
+ "diffusers.modular_pipelines.minimax_h3.before_encoder",
36
+ "MiniMaxH3Ref2VASetupStep",
37
+ ),
38
+ }
39
+
40
+ # Families where an audio reference may not stand alone - H3 conditions a
41
+ # soundtrack on a picture, so audio by itself has nothing to speak over
42
+ AUDIO_NEEDS_A_PICTURE = frozenset(REFERENCE_LIMIT_BLOCKS)
43
+
44
+ REFERENCE_TYPE_KEY = "reference_type"
45
+
46
+ # Values substitution resolves before this pass runs; one still spelled out
47
+ # is another pass's complaint, not this one's
48
+ _UNRESOLVED_PREFIXES = ("variable:", "item:", "previous_result:", "gather:")
49
+
50
+
51
+ def _family(module_name):
52
+ """The REFERENCE_LIMIT_BLOCKS key a class's module belongs to, or None."""
53
+ for family in REFERENCE_LIMIT_BLOCKS:
54
+ if module_name == family or module_name.startswith(family + "."):
55
+ return family
56
+ return None
57
+
58
+
59
+ def _limits(family):
60
+ """The (model name, per-kind limits, total) a family declares.
61
+
62
+ Read from the block's constructor signature rather than from an instance:
63
+ constructing one is cheap but not free, and a default is exactly what the
64
+ signature holds. The model name is the block's own, so even the family's
65
+ name in the message is diffusers'.
66
+ """
67
+ module_path, class_name = REFERENCE_LIMIT_BLOCKS[family]
68
+ try:
69
+ block = getattr(importlib.import_module(module_path), class_name)
70
+ parameters = inspect.signature(block.__init__).parameters
71
+ except Exception:
72
+ # A diffusers that renamed or dropped the block - checking nothing is
73
+ # the right failure here, since the pipeline still enforces its own
74
+ logger.debug(f"No reference limits available from {family}", exc_info=True)
75
+ return None, None, None
76
+
77
+ per_kind = {}
78
+ total = None
79
+ for name, parameter in parameters.items():
80
+ if parameter.default is inspect.Parameter.empty:
81
+ continue
82
+ if name == "max_references":
83
+ total = parameter.default
84
+ elif name.startswith("max_") and name.endswith("s"):
85
+ per_kind[name[len("max_") : -1]] = parameter.default
86
+ model_name = getattr(block, "model_name", family.rsplit(".", 1)[-1])
87
+ return model_name, (per_kind or None), total
88
+
89
+
90
+ def _reference_class(value):
91
+ """The class a reference entry's '*_type' names, or None.
92
+
93
+ Anything that does not resolve, or that the untrusted gate refuses, is
94
+ left alone: realize_args reports a type it cannot load, and the type
95
+ reference check a refused one, with a better message than this pass
96
+ could give.
97
+ """
98
+ if not isinstance(value, dict):
99
+ return None
100
+ name = value.get(REFERENCE_TYPE_KEY)
101
+ if not isinstance(name, str) or name.startswith(_UNRESOLVED_PREFIXES):
102
+ return None
103
+ if "." not in name:
104
+ return None
105
+ # Through the run's own resolver, so an untrusted workflow's name meets
106
+ # the same trust gate here, at validate time, before anything imports
107
+ from .type_helpers import load_type_from_full_name
108
+
109
+ try:
110
+ return load_type_from_full_name(name, REFERENCE_TYPE_KEY)
111
+ except Exception:
112
+ return None
113
+
114
+
115
+ def _kinds(entries):
116
+ """(family, [kind, ...]) for a list of reference entries, or None.
117
+
118
+ A family is the one whose limits this pass knows how to read; an entry
119
+ whose class carries no 'kind', or a list mixing two families, is not a
120
+ reference set this pass understands.
121
+ """
122
+ if not isinstance(entries, list) or not entries:
123
+ return None
124
+ found = None
125
+ kinds = []
126
+ for entry in entries:
127
+ reference = _reference_class(entry)
128
+ kind = getattr(reference, "kind", None)
129
+ if not isinstance(kind, str):
130
+ return None
131
+ family = _family(getattr(reference, "__module__", ""))
132
+ if family is None:
133
+ return None
134
+ if found is None:
135
+ found = family
136
+ elif found != family:
137
+ return None
138
+ kinds.append(kind)
139
+ return (found, kinds) if found else None
140
+
141
+
142
+ def _errors_for(kinds, family):
143
+ """Every limit a set of reference kinds breaks, as messages."""
144
+ model_name, per_kind, total = _limits(family)
145
+ if per_kind is None and total is None:
146
+ return []
147
+
148
+ messages = []
149
+ for kind, limit in sorted((per_kind or {}).items()):
150
+ count = kinds.count(kind)
151
+ if count > limit:
152
+ messages.append(
153
+ f"{model_name} accepts at most {limit} "
154
+ f"{kind} reference{'s' if limit != 1 else ''}, got {count}."
155
+ )
156
+ if total is not None and len(kinds) > total:
157
+ messages.append(
158
+ f"{model_name} accepts at most {total} references in total, "
159
+ f"got {len(kinds)}."
160
+ )
161
+ if family in AUDIO_NEEDS_A_PICTURE and set(kinds) == {"audio"}:
162
+ messages.append(
163
+ "An audio reference has to be paired with at least one image or "
164
+ "video reference and cannot be used on its own."
165
+ )
166
+ return messages
167
+
168
+
169
+ def reference_limit_errors(workflow_definition, source_indices=None):
170
+ """Every reference list a pipeline would refuse, as [{path, message}].
171
+
172
+ The definition handed here has already been substituted and expanded, so a
173
+ `for_each` member's own references are checked as they will run;
174
+ `source_indices` maps each expanded step back to the step the author
175
+ wrote, and the member is named in the message - the same convention
176
+ subfolder_errors uses.
177
+ """
178
+ steps = workflow_definition.get("steps")
179
+ if not isinstance(steps, list):
180
+ return []
181
+
182
+ errors = []
183
+ for index, step in enumerate(steps):
184
+ if not isinstance(step, dict):
185
+ continue
186
+ pipeline = step.get("pipeline")
187
+ arguments = pipeline.get("arguments") if isinstance(pipeline, dict) else None
188
+ if not isinstance(arguments, dict):
189
+ continue
190
+ source = (
191
+ source_indices[index]
192
+ if source_indices is not None and index < len(source_indices)
193
+ else index
194
+ )
195
+ name = step.get("name")
196
+ where = (
197
+ f" in member '{name}'"
198
+ if isinstance(name, str) and MEMBER_SEPARATOR in name
199
+ else ""
200
+ )
201
+ for key, value in arguments.items():
202
+ found = _kinds(value)
203
+ if found is None:
204
+ continue
205
+ family, kinds = found
206
+ for message in _errors_for(kinds, family):
207
+ errors.append(
208
+ {
209
+ "path": render_path(
210
+ ("steps", source, "pipeline", "arguments", key)
211
+ ),
212
+ "message": f"{message.rstrip('.')}{where}.",
213
+ }
214
+ )
215
+ return errors
dw/reference_names.py ADDED
@@ -0,0 +1,125 @@
1
+ """The shape of an `asset:` / `prompt:` / `output:` name, checked for free.
2
+
3
+ An `output:` reference naming a file a `for_each` step wrote - the `@` in
4
+ `shot@opening_statement` - validated clean and then failed the job at run
5
+ time, after the queue, with a message that described a *valid* name and said
6
+ nothing about what it had objected to (#162). Two things were wrong with
7
+ that, and only one of them was the `@`: a name that can never resolve, in
8
+ any workspace, is not a run-time discovery. Its shape is a property of the
9
+ string alone.
10
+
11
+ So the shape is checked here, in `validation_errors`, which is what
12
+ `POST /api/validate`, `validate_workflow` and the pre-queue check all run -
13
+ while *existence* stays where it was. Whether a run id is still on disk
14
+ depends on the workspace and on what pruning has taken, and the definition's
15
+ own references (a template's `prompt:ltx2/hummingbird_garden`) are not the
16
+ caller's to answer for; the caller's `arguments` are separately resolved
17
+ against the workspace by the validate route.
18
+ """
19
+
20
+ from .assets import ASSET_PREFIX
21
+ from .for_each import MEMBER_SEPARATOR, render_path
22
+ from .prompts import PROMPT_PREFIX
23
+ from .runs import OUTPUT_PREFIX
24
+ from .security import (
25
+ InvalidInputError,
26
+ validate_asset_reference,
27
+ validate_output_reference,
28
+ validate_prompt_reference,
29
+ )
30
+
31
+ # Substitution and expansion run before this pass, so every string reaching
32
+ # it is literal. One still spelled with a deferred prefix is nothing this
33
+ # pass resolved, and the undeclared-variable pass owns that complaint
34
+ _UNRESOLVED_PREFIXES = ("variable:", "item:", "previous_result:", "gather:")
35
+
36
+
37
+ def _output_name(reference):
38
+ """The part of an `output:` reference the name rule applies to.
39
+
40
+ `latest` in the run-id position is expanded before the path is joined,
41
+ so it is checked as the ordinary segment it looks like.
42
+ """
43
+ return reference.removeprefix(OUTPUT_PREFIX).strip()
44
+
45
+
46
+ _KINDS = (
47
+ (OUTPUT_PREFIX, validate_output_reference, _output_name),
48
+ (ASSET_PREFIX, validate_asset_reference, None),
49
+ (PROMPT_PREFIX, validate_prompt_reference, None),
50
+ )
51
+
52
+
53
+ def reference_name_errors(workflow_definition, source_indices=None):
54
+ """Every reference in the definition whose *name* is malformed, as
55
+ [{path, message}].
56
+
57
+ Walks the substituted, expanded definition. `source_indices`, when
58
+ given, maps each expanded step back to the step the author wrote, so an
59
+ error inside a `for_each` member carries a path in their file and names
60
+ the member.
61
+ """
62
+ steps = workflow_definition.get("steps")
63
+ if not isinstance(steps, list):
64
+ return []
65
+
66
+ errors = []
67
+ for index, step in enumerate(steps):
68
+ if not isinstance(step, dict):
69
+ continue
70
+ source = (
71
+ source_indices[index]
72
+ if source_indices is not None and index < len(source_indices)
73
+ else index
74
+ )
75
+ name = step.get("name")
76
+ where = (
77
+ f" in member '{name}'"
78
+ if isinstance(name, str) and MEMBER_SEPARATOR in name
79
+ else ""
80
+ )
81
+ for path, value in _strings(step, ("steps", source)):
82
+ problem = reference_fault(value)
83
+ if problem is not None:
84
+ errors.append(
85
+ {"path": render_path(path), "message": f"{problem}{where}"}
86
+ )
87
+ return errors
88
+
89
+
90
+ def reference_fault(value):
91
+ """Why this string's reference name is malformed, or None.
92
+
93
+ None for anything that is not a reference, and for one still spelled
94
+ with a deferred prefix behind the reference prefix - `output:` on a
95
+ value substitution has not reached yet is not this pass's complaint.
96
+ """
97
+ if not isinstance(value, str):
98
+ return None
99
+ for prefix, check, extract in _KINDS:
100
+ if not value.startswith(prefix):
101
+ continue
102
+ rest = value[len(prefix) :].strip()
103
+ if not rest or rest.startswith(_UNRESOLVED_PREFIXES):
104
+ return None
105
+ try:
106
+ check(extract(value) if extract else rest)
107
+ except InvalidInputError as e:
108
+ return str(e)
109
+ return None
110
+ return None
111
+
112
+
113
+ def _strings(value, path):
114
+ """Every string inside a step, paired with the path it sits at."""
115
+ if isinstance(value, str):
116
+ yield path, value
117
+ elif isinstance(value, list):
118
+ for index, item in enumerate(value):
119
+ yield from _strings(item, path + (index,))
120
+ elif isinstance(value, dict):
121
+ for key, item in value.items():
122
+ yield from _strings(item, path + (key,))
123
+
124
+
125
+ __all__ = ["reference_fault", "reference_name_errors"]