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/variables.py ADDED
@@ -0,0 +1,443 @@
1
+ import copy
2
+ import logging
3
+ from .arguments import (
4
+ FROM_ARGUMENTS_KEY,
5
+ FROM_FILE_KEY,
6
+ FROM_PREVIOUS_RESULT_KEY,
7
+ )
8
+ from .security import (
9
+ validate_variable_name,
10
+ validate_string_input,
11
+ SecurityError,
12
+ MAX_VARIABLE_VALUE_LENGTH,
13
+ )
14
+
15
+ logger = logging.getLogger("dw")
16
+
17
+ # The keys an object description names its media with. A "variable:" under one
18
+ # of these that resolves to null has to stay present-and-null for
19
+ # realize_object to read it as an omitted optional reference
20
+ MEDIA_SOURCE_KEYS = (FROM_FILE_KEY, FROM_PREVIOUS_RESULT_KEY, FROM_ARGUMENTS_KEY)
21
+
22
+
23
+ class VariableNotFoundError(ValueError):
24
+ """Raised when a workflow references a "variable:name" that isn't declared."""
25
+
26
+
27
+ class VariableCycleError(ValueError):
28
+ """Raised when a list- or dict-valued variable references itself, directly
29
+ or through others, inside `resolve_variable_values`."""
30
+
31
+
32
+ def _resolve_variable_reference(value, variables):
33
+ """
34
+ If value is a "variable:name" reference, look it up and return (True, resolved).
35
+ Otherwise return (False, None) so the caller knows to recurse instead.
36
+
37
+ Raises:
38
+ VariableNotFoundError: if the referenced name isn't in variables, naming the
39
+ variables that are actually available.
40
+ """
41
+ if isinstance(value, str) and value.startswith("variable:"):
42
+ variable_name = value.removeprefix("variable:")
43
+ logger.debug(f"Replacing variable reference: {variable_name}")
44
+ if variable_name not in variables:
45
+ available = ", ".join(sorted(variables.keys())) or "<none>"
46
+ message = f"Variable <{variable_name}> not found; available variables: {available}"
47
+ raise VariableNotFoundError(message)
48
+ return True, variables[variable_name]
49
+ return False, None
50
+
51
+
52
+ def replace_variables(data, variables):
53
+ """
54
+ Recursively replaces variable references in data structures with their actual values.
55
+
56
+ Does not mutate its input - a new structure is returned and `data` is left as it
57
+ was passed in, so callers don't have to deep-copy defensively before calling.
58
+
59
+ Args:
60
+ data: The data structure (dict or list) containing variable references
61
+ variables: Dictionary of variable names and their values
62
+ Returns:
63
+ A new structure with "variable:name" references replaced. Any part of `data`
64
+ that isn't a dict/list/reference string is returned as-is.
65
+ """
66
+ if variables is None:
67
+ return data
68
+
69
+ logger.debug(f"Processing variables: {list(variables.keys())}")
70
+
71
+ # Handle lists - replace any "variable:name" strings with their values
72
+ if isinstance(data, list):
73
+ logger.debug(f"Processing list of length {len(data)}")
74
+ result = []
75
+ for item in data:
76
+ matched, resolved = _resolve_variable_reference(item, variables)
77
+ if matched:
78
+ result.append(resolved)
79
+ else:
80
+ # Recursively process nested structures
81
+ result.append(replace_variables(item, variables))
82
+ return result
83
+
84
+ # Handle dictionaries - replace values that are variable references
85
+ if isinstance(data, dict):
86
+ logger.debug(f"Processing dictionary with keys: {list(data.keys())}")
87
+ result = {}
88
+ for k, v in data.items():
89
+ matched, resolved = _resolve_variable_reference(v, variables)
90
+ if matched:
91
+ # A variable: reference that resolves to null is dropped as
92
+ # if the key had never been passed - an optional argument
93
+ # left at its unset default (e.g. select's threshold/index)
94
+ # shouldn't count as "present" just because a variable named
95
+ # it. A literal null written inline in the workflow JSON
96
+ # never goes through this branch, so its meaning elsewhere
97
+ # (a declared-but-unset default) is untouched.
98
+ #
99
+ # Except where the key itself is how an object description
100
+ # names its media: realize_object reads a present-and-null
101
+ # 'from_file'/'from_previous_result'/'from_arguments' as
102
+ # OMITTED - an optional reference this run was given nothing
103
+ # for (_names_no_media in arguments.py). Dropping the key
104
+ # there turns that into a media-less stub that reaches the
105
+ # pipeline instead.
106
+ if resolved is None and k not in MEDIA_SOURCE_KEYS:
107
+ continue
108
+ result[k] = resolved
109
+ else:
110
+ # Recursively process nested structures in dictionary values
111
+ result[k] = replace_variables(v, variables)
112
+ return result
113
+
114
+ # Scalars (and anything else) pass through unchanged. copy.deepcopy guards
115
+ # against a caller mutating a returned mutable leaf (e.g. a PIL.Image or a
116
+ # list-typed variable's value) and having that reach back into `variables`.
117
+ return copy.deepcopy(data)
118
+
119
+
120
+ def resolve_variable_values(variables):
121
+ """A copy of `variables` in which every "variable:name" inside a list-
122
+ or dict-valued variable is replaced by that variable's value.
123
+
124
+ A list-driven step reads its entries from a variable, and an entry that
125
+ says "from_file": "variable:character_a_voice" is how one variable sets
126
+ a voice in every shot the character speaks in. replace_variables only
127
+ walks the definition, so those references would reach the step as the
128
+ literal strings; this resolves them once, before realize_args, so a
129
+ reference type inside an entry is a type name by the time it is loaded.
130
+
131
+ Only list and dict values are walked. A scalar value that begins with
132
+ "variable:" is passed through as it always was.
133
+
134
+ Raises:
135
+ VariableNotFoundError: a reference names nothing declared
136
+ VariableCycleError: a value references itself, directly or through others
137
+ """
138
+ resolved = {}
139
+
140
+ def resolve(name, chain):
141
+ if name in resolved:
142
+ return resolved[name]
143
+ if name in chain:
144
+ loop = " -> ".join(chain[chain.index(name) :] + [name])
145
+ raise VariableCycleError(
146
+ f"Variable '{name}' references itself through: {loop}"
147
+ )
148
+ value = variables[name]
149
+ if isinstance(value, (list, dict)):
150
+ value = walk(value, chain + [name])
151
+ else:
152
+ value = copy.deepcopy(value)
153
+ resolved[name] = value
154
+ return value
155
+
156
+ def walk(node, chain):
157
+ matched, _ = _resolve_variable_reference(node, variables)
158
+ if matched:
159
+ return resolve(node.removeprefix("variable:"), chain)
160
+ if isinstance(node, list):
161
+ return [walk(item, chain) for item in node]
162
+ if isinstance(node, dict):
163
+ return {key: walk(item, chain) for key, item in node.items()}
164
+ return copy.deepcopy(node)
165
+
166
+ for name in variables:
167
+ resolve(name, [])
168
+ return resolved
169
+
170
+
171
+ def undeclared_variable_references(definition):
172
+ """The "variable:name" references in a workflow definition that name no
173
+ entry of its `variables` - the ones `replace_variables` will refuse at run
174
+ time, found before anything loads.
175
+
176
+ Walks everything but `variables` itself, plus the inside of every list-
177
+ or dict-valued variable, the way `resolve_variable_values` and
178
+ `replace_variables` together do. Returns a list of (path, name) pairs,
179
+ path being where the reference sits (`steps[0].pipeline.arguments.prompt`)
180
+ and name what it asked for - which is the whole remainder of the string,
181
+ since a reference is the entire value and nothing is interpolated
182
+ around it.
183
+ """
184
+ declared = definition.get("variables") or {}
185
+ found = []
186
+
187
+ def walk(node, path):
188
+ if isinstance(node, str) and node.startswith("variable:"):
189
+ name = node.removeprefix("variable:")
190
+ if name not in declared:
191
+ found.append((path, name))
192
+ elif isinstance(node, dict):
193
+ for k, v in node.items():
194
+ walk(v, f"{path}.{k}" if path else k)
195
+ elif isinstance(node, list):
196
+ for i, v in enumerate(node):
197
+ walk(v, f"{path}[{i}]")
198
+
199
+ for key, value in definition.items():
200
+ if key != "variables":
201
+ walk(value, key)
202
+ if isinstance(declared, dict):
203
+ for name, value in declared.items():
204
+ if isinstance(value, (list, dict)):
205
+ walk(value, f"variables.{name}")
206
+ return found
207
+
208
+
209
+ def _validated_strings(value):
210
+ """A copy of a list- or dict-valued caller argument with every string
211
+ leaf passed through `validate_string_input` - the same check a
212
+ top-level string argument gets in `set_variables` below. A for_each
213
+ entry's `prompt` or `from_file` is exactly as reachable to an attacker
214
+ as a top-level variable, and `isinstance(v, str)` alone would skip it.
215
+
216
+ Non-string leaves (numbers, bools, None, nested lists/dicts) pass
217
+ through unchanged; only str is validated.
218
+ """
219
+ if isinstance(value, str):
220
+ return validate_string_input(
221
+ value, max_length=MAX_VARIABLE_VALUE_LENGTH, allow_empty=True
222
+ )
223
+ if isinstance(value, list):
224
+ return [_validated_strings(item) for item in value]
225
+ if isinstance(value, dict):
226
+ return {key: _validated_strings(item) for key, item in value.items()}
227
+ return value
228
+
229
+
230
+ def set_variables(values, variables):
231
+ """
232
+ Sets the values of variables from a dictionary of new values with validation
233
+ Args:
234
+ values: Dictionary of new values to set
235
+ variables: Dictionary of existing variables with their default values/types
236
+ """
237
+ logger.debug(f"Setting variables: {list(values.keys())}")
238
+
239
+ if not isinstance(values, dict) or not isinstance(variables, dict):
240
+ logger.error("Both values and variables must be dictionaries")
241
+ raise TypeError("Both values and variables must be dictionaries")
242
+
243
+ for k, v in values.items():
244
+ try:
245
+ # Validate variable name
246
+ validated_name = validate_variable_name(k)
247
+
248
+ # The workflow must have already declared this variable (with a default
249
+ # value/type) - reject unknown names instead of raising a bare KeyError
250
+ if validated_name not in variables:
251
+ declared = ", ".join(sorted(variables.keys()))
252
+ logger.error(
253
+ f"Unknown variable '{validated_name}'; declared variables: {declared}"
254
+ )
255
+ raise ValueError(
256
+ f"Unknown variable '{validated_name}'; declared variables: {declared}"
257
+ )
258
+
259
+ # Validate string values - including strings nested inside a
260
+ # list- or dict-valued argument, which a for_each entry's
261
+ # prompt or from_file always is
262
+ if isinstance(v, str):
263
+ validated_value = validate_string_input(
264
+ v, max_length=MAX_VARIABLE_VALUE_LENGTH, allow_empty=True
265
+ )
266
+ elif isinstance(v, (list, dict)):
267
+ validated_value = _validated_strings(v)
268
+ else:
269
+ validated_value = v
270
+
271
+ logger.debug(
272
+ f"Setting variable {validated_name} to value: {validated_value}"
273
+ )
274
+ # Use the type of the existing variable to convert the new value
275
+ variables[validated_name] = get_value(
276
+ validated_value, type(variables[validated_name]), validated_name
277
+ )
278
+
279
+ except SecurityError as e:
280
+ logger.error(f"Security validation failed for variable {k}: {e}")
281
+ raise
282
+
283
+
284
+ def get_value(v, desired_type, name=None):
285
+ """
286
+ Converts a value to the desired type, with special handling for booleans
287
+ Args:
288
+ v: Value to convert
289
+ desired_type: Target type for conversion
290
+ name: Name of the variable being converted, used for error messages
291
+ Returns:
292
+ Converted value
293
+
294
+ Raises:
295
+ ValueError: if v cannot be converted to desired_type, naming the variable,
296
+ its target type, and the offending value.
297
+ """
298
+ logger.debug(f"Converting value {v} to type {desired_type}")
299
+
300
+ # A variable declared null is an optional one the workflow states no type
301
+ # for - passing a value to it is the expected case, not a suspicious one
302
+ if desired_type is None or desired_type is type(None):
303
+ logger.debug("Variable has no declared type, using the value as given")
304
+ return v
305
+
306
+ # A value already realized by an earlier step (an AudioTrack, an
307
+ # AudioVideo, a PIL.Image, ...) is a live object, not a JSON literal -
308
+ # 'previous_result:' resolves it to this before set_variables ever sees
309
+ # it, so a sibling step's task argument receives it unchanged. A
310
+ # sub-workflow's declared variable must too, rather than being coerced
311
+ # through the type of the variable's own default (usually a string
312
+ # 'asset:'/'output:' reference): desired_type(v) on one of these called
313
+ # str() on an AudioTrack and got its Python repr, which then reached
314
+ # slice_audio looking like a bogus path (#404)
315
+ if not isinstance(v, (str, int, float, bool, list, dict)):
316
+ logger.debug(
317
+ f"Value for variable '{name}' is an already-realized {type(v).__name__}; "
318
+ "using it as given"
319
+ )
320
+ return v
321
+
322
+ # Special handling for boolean string values - bool("0") and bool("no") are
323
+ # both truthy in Python, which would silently invert the user's intent, so
324
+ # only a known set of true/false spellings is accepted here
325
+ if isinstance(v, str) and desired_type is bool:
326
+ lowered = v.lower()
327
+ if lowered in ("true", "1", "yes", "on"):
328
+ return True
329
+ if lowered in ("false", "0", "no", "off"):
330
+ return False
331
+ var_label = name if name is not None else "<unknown>"
332
+ message = f"Cannot interpret '{v}' as true/false for variable '{var_label}'"
333
+ logger.error(message)
334
+ raise ValueError(message)
335
+
336
+ # Special handling for list string values - list("cat") would mangle the
337
+ # string into ['c', 'a', 't'], so a comma-separated string is split instead
338
+ if isinstance(v, str) and desired_type is list:
339
+ return [item.strip() for item in v.split(",")]
340
+
341
+ # A variable typed int by its default (e.g. `"score_gain": 1`) silently
342
+ # truncates a fractional override - int(0.3) == 0, with no error - which
343
+ # reads as a valid, if small, argument rather than the wrong type. Refuse
344
+ # it instead of truncating; the fix is to declare the default as a float
345
+ # (`1.0`) if the variable is meant to accept fractions
346
+ if desired_type is int and isinstance(v, float) and not v.is_integer():
347
+ var_label = name if name is not None else "<unknown>"
348
+ message = (
349
+ f"{var_label} {v!r} would be realized as {int(v)}: this variable "
350
+ f"is typed integer by its default; declare its default as a float "
351
+ f"(e.g. {float(int(v))!r}) to accept fractional values"
352
+ )
353
+ logger.error(message)
354
+ raise ValueError(message)
355
+
356
+ # A string cannot be coerced into a dict or a None - dict('/a/b.png') is
357
+ # nonsense, NoneType('x') a TypeError. Those defaults are how media
358
+ # variables ({'location': ...}) and optional inputs (null) are declared,
359
+ # and a string override is a path or a reference that realize_args
360
+ # resolves later, so it passes through as written
361
+ if isinstance(v, str) and desired_type in (dict, type(None)):
362
+ return v
363
+
364
+ # A dict or list cannot be coerced into a string - str({...}) never
365
+ # raises, it just stringifies the Python repr, so a caller passing the
366
+ # old {"location": ...} object for a variable now declared as a plain
367
+ # string (e.g. templates/ltx2/keyframes' first_image/last_image, #433)
368
+ # sailed through validation and only failed at run time with a path
369
+ # built from the dict's repr
370
+ if isinstance(v, (dict, list)) and desired_type is str:
371
+ var_label = name if name is not None else "<unknown>"
372
+ message = (
373
+ f"Cannot convert variable '{var_label}' value {v!r} to type str: "
374
+ f"this variable takes a plain string (a URL, or an 'asset:'/"
375
+ f"'output:' reference), not an object"
376
+ )
377
+ logger.error(message)
378
+ raise ValueError(message)
379
+
380
+ # Attempt type conversion. A failure here is surfaced immediately with a clear,
381
+ # named error instead of silently passing the unconverted value through - letting
382
+ # it through would fail several layers later inside diffusers/torch with a
383
+ # confusing traceback that doesn't mention the variable at fault.
384
+ try:
385
+ converted = desired_type(v)
386
+ logger.debug(f"Successfully converted to {desired_type.__name__}: {converted}")
387
+ return converted
388
+ except Exception as e:
389
+ var_label = name if name is not None else "<unknown>"
390
+ message = (
391
+ f"Cannot convert variable '{var_label}' value {v!r} to type "
392
+ f"{desired_type.__name__}: {e}"
393
+ )
394
+ logger.error(message)
395
+ raise ValueError(message) from e
396
+
397
+
398
+ def argument_errors(definition, arguments):
399
+ """What is wrong with a caller's `arguments` for this workflow, before
400
+ anything is queued.
401
+
402
+ Exactly the check `set_variables` makes at the top of a run - an
403
+ undeclared name, a value that will not coerce to the type the default
404
+ declares - made against a copy, so nothing is mutated and the answer
405
+ costs no GPU time. A workflow that declares no variables at all takes no
406
+ arguments: today those are dropped in silence (`Workflow.run` only
407
+ substitutes when a `variables` block exists), which is the one case the
408
+ run itself does not report.
409
+
410
+ Args:
411
+ definition: A workflow definition
412
+ arguments: The caller's argument dict; empty or None means no errors
413
+
414
+ Returns:
415
+ [{"path": "arguments.<name>", "message": str}, ...], one per bad
416
+ argument, in the order they were given
417
+ """
418
+ if not arguments:
419
+ return []
420
+ if not isinstance(arguments, dict):
421
+ return [{"path": "arguments", "message": "arguments must be an object"}]
422
+
423
+ declared = definition.get("variables") if isinstance(definition, dict) else None
424
+ if not isinstance(declared, dict) or not declared:
425
+ return [
426
+ {
427
+ "path": f"arguments.{name}",
428
+ "message": f"This workflow declares no variables, so '{name}' "
429
+ "has nowhere to land - it would be ignored by the run",
430
+ }
431
+ for name in arguments
432
+ ]
433
+
434
+ errors = []
435
+ for name, value in arguments.items():
436
+ # One at a time against a fresh copy, so each bad argument is
437
+ # reported with its own name rather than the first one stopping the
438
+ # rest from being checked
439
+ try:
440
+ set_variables({name: value}, copy.deepcopy(declared))
441
+ except (ValueError, TypeError, SecurityError) as e:
442
+ errors.append({"path": f"arguments.{name}", "message": str(e)})
443
+ return errors
dw/video_extensions.py ADDED
@@ -0,0 +1,141 @@
1
+ """The extension of a `video` argument, checked before the run when it can be.
2
+
3
+ `fetch_video` (`dw/arguments.py`) loads every argument named `video` or
4
+ `*_video`, and every `{"media_type": "video", "location": ...}` reference
5
+ whatever it is named, as a video file - and refuses one whose extension is
6
+ not `ALLOWED_VIDEO_EXTENSIONS` there, at run time. A still image handed to
7
+ one of those arguments (`"video": "asset:sheet.png"`) validated clean and
8
+ then failed the job in seconds with "Video file extension not allowed:
9
+ .png", after `get_task("loop_frames")` had described the same argument as
10
+ taking "a still image, or frames in any shape a result carries" (#347).
11
+
12
+ Moved here, into `validation_errors`, for exactly the cases whose extension
13
+ is knowable without running anything: a literal file path, or an
14
+ `asset:`/`output:` reference whose name carries one - `resolve_path_references`
15
+ turns either into a real path before `fetch_video` ever sees it, and the
16
+ extension survives that unchanged. A `previous_result:` (or any reference
17
+ `expand_for_each` left unresolved) names no file yet and is left to the
18
+ existing run-time check, and a URL is left alone too - `fetch_video` never
19
+ gates a URL's extension, so refusing one here would refuse something the run
20
+ itself accepts.
21
+ """
22
+
23
+ from .arguments import (
24
+ CONSTANT_PREFIX,
25
+ PROMPT_PREFIX,
26
+ is_media_reference,
27
+ )
28
+ from .for_each import MEMBER_SEPARATOR, render_path
29
+ from .security import ALLOWED_IMAGE_EXTENSIONS, ALLOWED_VIDEO_EXTENSIONS
30
+
31
+ # Left to the run-time check: not yet resolved to anything an extension can
32
+ # be read off, at the point validation walks the expanded definition
33
+ _UNRESOLVED_PREFIXES = ("previous_result:", "variable:", "item:", "gather:")
34
+
35
+
36
+ def _is_video_key(key):
37
+ return isinstance(key, str) and (key == "video" or key.endswith("_video"))
38
+
39
+
40
+ def _extension_problem(value):
41
+ """Why this string's extension is not one `fetch_video` will accept, or
42
+ None - including None for anything whose extension is not yet knowable."""
43
+ if not isinstance(value, str):
44
+ return None
45
+ if value.startswith(("http://", "https://")):
46
+ return None
47
+ if value.startswith(_UNRESOLVED_PREFIXES):
48
+ return None
49
+ if value.startswith(CONSTANT_PREFIX) or value.startswith(PROMPT_PREFIX):
50
+ return None
51
+ ext = value.rsplit(".", 1)
52
+ if len(ext) != 2 or not ext[1] or "/" in ext[1]:
53
+ return None
54
+ ext = f".{ext[1].lower()}"
55
+ if ext in ALLOWED_VIDEO_EXTENSIONS:
56
+ return None
57
+ if ext in ALLOWED_IMAGE_EXTENSIONS:
58
+ return (
59
+ f"'{value}' is a still image, and a video argument loads video "
60
+ f"files - pass it as "
61
+ f'{{"media_type": "image", "location": "{value}"}} to load it as '
62
+ f"a still, or reference a prior image step with previous_result:"
63
+ )
64
+ return f"Video file extension not allowed: {ext}"
65
+
66
+
67
+ def _video_key_values(value, path):
68
+ """Strings a video-named key hands to `fetch_video` - itself, or each
69
+ element of a list of them. A dict here is a `media_type` reference and
70
+ is walked by `_video_values` instead, same as `fetch_video` handles it."""
71
+ if isinstance(value, str):
72
+ yield path, value
73
+ elif isinstance(value, list):
74
+ for index, item in enumerate(value):
75
+ yield from _video_key_values(item, path + (index,))
76
+ elif isinstance(value, dict):
77
+ yield from _video_values(value, path)
78
+
79
+
80
+ def _video_values(value, path):
81
+ """Every string this walk can attribute to a location `fetch_video`
82
+ will load, paired with the path it sits at."""
83
+ if isinstance(value, dict):
84
+ if is_media_reference(value) and value.get("media_type") == "video":
85
+ location = value.get("location")
86
+ if isinstance(location, str):
87
+ yield path + ("location",), location
88
+ return
89
+ for key, item in value.items():
90
+ if _is_video_key(key):
91
+ yield from _video_key_values(item, path + (key,))
92
+ else:
93
+ yield from _video_values(item, path + (key,))
94
+ elif isinstance(value, list):
95
+ for index, item in enumerate(value):
96
+ yield from _video_values(item, path + (index,))
97
+
98
+
99
+ def video_extension_errors(workflow_definition, source_indices=None):
100
+ """Every video argument whose extension `fetch_video` will refuse, for
101
+ every case that extension is knowable before the run, as
102
+ [{path, message}].
103
+
104
+ Walks the substituted, expanded definition, the same convention
105
+ `reference_name_errors` and `task_argument_errors` follow: `source_indices`
106
+ maps an expanded step back to the one the author wrote, and a path inside
107
+ a `for_each` member names the member.
108
+ """
109
+ steps = workflow_definition.get("steps")
110
+ if not isinstance(steps, list):
111
+ return []
112
+
113
+ errors = []
114
+ for index, step in enumerate(steps):
115
+ if not isinstance(step, dict):
116
+ continue
117
+ source = (
118
+ source_indices[index]
119
+ if source_indices is not None and index < len(source_indices)
120
+ else index
121
+ )
122
+ name = step.get("name")
123
+ where = (
124
+ f" in member '{name}'"
125
+ if isinstance(name, str) and MEMBER_SEPARATOR in name
126
+ else ""
127
+ )
128
+ for path, value in _video_values(step, ()):
129
+ problem = _extension_problem(value)
130
+ if problem is None:
131
+ continue
132
+ errors.append(
133
+ {
134
+ "path": render_path(("steps", source) + path),
135
+ "message": f"{problem}{where}",
136
+ }
137
+ )
138
+ return errors
139
+
140
+
141
+ __all__ = ["video_extension_errors"]