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/previous_results.py ADDED
@@ -0,0 +1,411 @@
1
+ import logging
2
+ from itertools import product
3
+
4
+ from .arguments import (
5
+ FROM_PREVIOUS_RESULT_KEY,
6
+ PREVIOUS_RESULT_PREFIX,
7
+ build_objects,
8
+ )
9
+ from .for_each import MEMBER_SEPARATOR, render_path
10
+ from .step_cache import reference_resolves_to
11
+
12
+ logger = logging.getLogger("dw")
13
+
14
+ # Maximum number of iterations to prevent resource exhaustion
15
+ MAX_ITERATIONS = 10000
16
+
17
+
18
+ def get_iterations(argument_template, previous_results):
19
+ """Generate argument combinations using previous task results.
20
+
21
+ Takes a template of arguments and expands any references to previous results
22
+ into all possible combinations of those results.
23
+
24
+ Args:
25
+ argument_template: Dict or list containing argument definitions
26
+ previous_results: Dict of results from previously executed steps
27
+
28
+ Returns:
29
+ List of argument dictionaries, one for each possible combination
30
+ """
31
+ # An 'inputs' list is already one iteration per entry. The static check
32
+ # (previous_result_reference_errors) accepts a reference inside one, so the
33
+ # run must resolve it too rather than hand the task the literal string:
34
+ # a reference entry is one iteration per result it names, and an object
35
+ # entry expands exactly as an 'arguments' template would
36
+ if isinstance(argument_template, list):
37
+ iterations = []
38
+ for entry in argument_template:
39
+ if isinstance(entry, str) and entry.startswith(PREVIOUS_RESULT_PREFIX):
40
+ iterations.extend(
41
+ get_previous_results(
42
+ previous_results, entry[len(PREVIOUS_RESULT_PREFIX) :]
43
+ )
44
+ )
45
+ elif isinstance(entry, dict):
46
+ iterations.extend(get_iterations(entry, previous_results))
47
+ else:
48
+ iterations.append(entry)
49
+ if len(iterations) > MAX_ITERATIONS:
50
+ raise ValueError(
51
+ f"Too many iterations generated: more than {MAX_ITERATIONS} "
52
+ f"from an 'inputs' list. Consider splitting it across steps."
53
+ )
54
+ return iterations
55
+
56
+ # Find any references to previous results in the template
57
+ # Returns dict of {arg_key: result_reference}
58
+ result_refs = find_previous_result_refs(argument_template)
59
+
60
+ # If no references found, return the template as-is
61
+ if not result_refs:
62
+ logger.debug("No result references found in template")
63
+ # Shallow copy: realize_args may have already loaded large media
64
+ # (PIL images, full video frame lists) into the template, so a deep
65
+ # copy would multiply memory use. Contract: iteration dicts may only
66
+ # be mutated at the top level (key pop/assign); nested values are
67
+ # shared across iterations and must never be mutated in place.
68
+ return [dict(argument_template)]
69
+
70
+ logger.debug(f"Found {len(result_refs)} result references: {result_refs}")
71
+
72
+ # Create a dictionary mapping each reference path to its possible values
73
+ # Example: {('image',): [img1, img2], ('prompt',): ['text1', 'text2']}
74
+ ref_results = {
75
+ ref_path: list(get_previous_results(previous_results, ref_value))
76
+ for ref_path, ref_value in result_refs.items()
77
+ }
78
+
79
+ # Generate all possible combinations of argument values
80
+ keys = list(ref_results.keys())
81
+ iterations = []
82
+
83
+ # Guard the cartesian product BEFORE building it - the product's size is
84
+ # known from the reference counts, and checking after the loop would only
85
+ # report the resource exhaustion it exists to prevent
86
+ combinations = 1
87
+ for key in keys:
88
+ combinations *= len(ref_results[key])
89
+ if combinations > MAX_ITERATIONS:
90
+ raise ValueError(
91
+ f"Too many iterations generated: {combinations} exceeds maximum of {MAX_ITERATIONS}. "
92
+ f"This usually indicates too many previous_result references creating a cartesian product. "
93
+ f"Consider reducing the number of multi-value results or splitting into multiple steps."
94
+ )
95
+
96
+ # Use itertools.product to create cartesian product of all possible values
97
+ # Example: if ref_results has 2 images and 2 prompts, creates 4 combinations
98
+ for values in product(*[ref_results[k] for k in keys]):
99
+ # Create fresh shallow copy of template for each combination.
100
+ # Nested values (e.g. loaded PIL images, video frame lists) are
101
+ # shared across iterations, not deep-copied, to avoid multiplying
102
+ # media memory usage by the iteration count. Contract: iteration
103
+ # dicts may only be mutated at the top level (key pop/assign);
104
+ # nested values must never be mutated in place.
105
+ arguments = dict(argument_template)
106
+
107
+ # Replace each reference with its actual value
108
+ for path, value in zip(keys, values):
109
+ # Handle nested dictionary properties
110
+ # If value is dict and contains the key we're looking for, use that property
111
+ key = path[-1]
112
+ arguments = substitute_at_path(
113
+ arguments,
114
+ path,
115
+ value[key] if isinstance(value, dict) and key in value else value,
116
+ )
117
+
118
+ # Now that the media exists, build the objects that were waiting for it -
119
+ # a reference constructed from a step's output rather than from a file
120
+ iterations.append(build_objects(arguments))
121
+
122
+ logger.debug(f"Generated {len(iterations)} argument combinations")
123
+ return iterations
124
+
125
+
126
+ def get_previous_results(previous_results, previous_result_name):
127
+ """Retrieve results or specific properties from previous tasks.
128
+
129
+ Args:
130
+ previous_results: Dict of results from previous steps
131
+ previous_result_name: String identifying the result, optionally with property
132
+ Format: "step_name" or "step_name.property_name"
133
+
134
+ Returns:
135
+ List of results or specific properties from the referenced step
136
+ """
137
+ # Step names are unrestricted strings and may themselves contain dots
138
+ # (e.g. "v1.0"), so resolve against the known step names rather than
139
+ # blindly splitting on the first/only ".".
140
+
141
+ # Exact match: the whole reference is a known step name, no property.
142
+ if previous_result_name in previous_results:
143
+ logger.debug(f"Getting all artifacts from result {previous_result_name}")
144
+ return previous_results[previous_result_name].get_artifacts()
145
+
146
+ if "." not in previous_result_name:
147
+ raise _not_found(previous_results, previous_result_name)
148
+
149
+ # Find the longest known step name the reference resolves to, and treat
150
+ # the remainder as the property name. The exact-match case returned
151
+ # above, so every name reaching here is a strict '<name>.' prefix.
152
+ result_name = max(
153
+ (
154
+ name
155
+ for name in previous_results
156
+ if reference_resolves_to(previous_result_name, name)
157
+ ),
158
+ key=len,
159
+ default=None,
160
+ )
161
+
162
+ if result_name is None:
163
+ raise _not_found(previous_results, previous_result_name)
164
+
165
+ property_name = previous_result_name[len(result_name) + 1 :]
166
+ logger.debug(f"Getting property {property_name} from result {result_name}")
167
+ return previous_results[result_name].get_artifact_properties(property_name)
168
+
169
+
170
+ def resolve_chain_prompts(step_action, previous_results):
171
+ """Resolve a pipeline chain's per-segment prompts against previous results.
172
+
173
+ A chain's "prompts" list is not part of the step's argument template, so the
174
+ cartesian pass that expands "previous_result:" everywhere else never reaches
175
+ it. That matters for a chain whose opening segment is written by a different
176
+ step from the ones that continue it - a continuation prompt declares a video
177
+ reference the first segment does not have.
178
+
179
+ Each entry resolves independently and yields one prompt, so this never
180
+ multiplies iterations the way an argument reference does; a reference that
181
+ produced several artifacts uses the first.
182
+
183
+ The resolved list is left on the step action for run_chain to pick up, and
184
+ nothing happens at all for a pipeline without chain prompts.
185
+ """
186
+ definition = getattr(step_action, "pipeline_definition", None)
187
+ if not isinstance(definition, dict):
188
+ return
189
+
190
+ chain = definition.get("chain", None) or {}
191
+ prompts = chain.get("prompts", None)
192
+ if not prompts:
193
+ return
194
+
195
+ resolved = []
196
+ for entry in prompts:
197
+ if isinstance(entry, str) and entry.startswith("previous_result:"):
198
+ artifacts = get_previous_results(
199
+ previous_results, entry.removeprefix("previous_result:")
200
+ )
201
+ if not artifacts:
202
+ raise ValueError(f"Chain prompt reference '{entry}' produced no result")
203
+ if len(artifacts) > 1:
204
+ logger.warning(
205
+ f"Chain prompt reference '{entry}' produced {len(artifacts)} "
206
+ f"results - using the first"
207
+ )
208
+ entry = artifacts[0]
209
+ resolved.append(entry)
210
+
211
+ step_action.chain_prompts = resolved
212
+
213
+
214
+ def find_previous_result_refs(arguments):
215
+ """Find all values in an argument structure that reference previous results.
216
+
217
+ A reference is written either as a value with the "previous_result:" prefix, or
218
+ as the step name a 'from_previous_result' object description is built from. Both
219
+ are found at any depth: an argument that takes a constructed object holds it
220
+ inside a list - MiniMax-H3's 'references' - so the reference is nested rather
221
+ than sitting at the top of the arguments.
222
+
223
+ Args:
224
+ arguments: Dictionary of argument definitions
225
+
226
+ Returns:
227
+ Dict mapping the path of each reference to the result name it names. A path
228
+ is the tuple of keys and list indices that reaches the value, so a top-level
229
+ {'image': 'previous_result:step1'} comes back as {('image',): 'step1'}
230
+ """
231
+ found = {}
232
+ _collect_refs(arguments, (), found)
233
+ return found
234
+
235
+
236
+ def _collect_refs(value, path, found):
237
+ """Walk an argument structure, collecting every reference by its path."""
238
+ if isinstance(value, dict):
239
+ for key, item in value.items():
240
+ # The object description names its step bare, the way it would name a
241
+ # file - the prefix would only repeat what the key already says
242
+ if key == FROM_PREVIOUS_RESULT_KEY and isinstance(item, str):
243
+ found[path + (key,)] = item
244
+ else:
245
+ _collect_refs(item, path + (key,), found)
246
+
247
+ elif isinstance(value, list):
248
+ for index, item in enumerate(value):
249
+ _collect_refs(item, path + (index,), found)
250
+
251
+ elif isinstance(value, str) and value.startswith(PREVIOUS_RESULT_PREFIX):
252
+ found[path] = value[len(PREVIOUS_RESULT_PREFIX) :]
253
+
254
+
255
+ def substitute_at_path(container, path, value):
256
+ """A copy of container with value placed at path.
257
+
258
+ Only the containers along the path are copied. Everything beside them stays
259
+ shared, which is the same contract the top-level copy keeps: iterations share
260
+ their nested values, so a substitution deep in one must not be visible in the
261
+ others.
262
+
263
+ Args:
264
+ container: The dict or list to substitute into
265
+ path: Tuple of keys and indices reaching the value to replace
266
+ value: What to put there
267
+
268
+ Returns:
269
+ The copied container
270
+ """
271
+ key = path[0]
272
+ replacement = (
273
+ value if len(path) == 1 else substitute_at_path(container[key], path[1:], value)
274
+ )
275
+
276
+ if isinstance(container, list):
277
+ copied = list(container)
278
+ copied[key] = replacement
279
+ return copied
280
+
281
+ copied = dict(container)
282
+ copied[key] = replacement
283
+ return copied
284
+
285
+
286
+ class StepResults(dict):
287
+ """The run's results, which also remembers every step that produced one.
288
+
289
+ `release_unreferenced_results` deletes a result the moment no remaining
290
+ step references it, so by the time a misspelled reference fails, the
291
+ steps that ran are gone from the dict and the error printed
292
+ 'Available results: []' on a run where several steps had completed
293
+ (T015). Keeping the names - not the results, which is the whole point of
294
+ releasing them - costs nothing and is the one thing that diagnostic
295
+ needed.
296
+ """
297
+
298
+ def __init__(self, *args, **kwargs):
299
+ super().__init__(*args, **kwargs)
300
+ self.completed_steps = list(self.keys())
301
+
302
+ def __setitem__(self, key, value):
303
+ if key not in self.completed_steps:
304
+ self.completed_steps.append(key)
305
+ super().__setitem__(key, value)
306
+
307
+
308
+ def _not_found(previous_results, previous_result_name):
309
+ """The error a reference that names nothing raises.
310
+
311
+ Names what is there as well as what was asked for: the gap between them
312
+ is the fix, and on a long run it is the only thing standing between a
313
+ typo and another 40 minutes of GPU.
314
+ """
315
+ message = (
316
+ f"Previous result '{previous_result_name}' not found. "
317
+ f"Available results: {list(previous_results.keys())}"
318
+ )
319
+ released = [
320
+ name
321
+ for name in getattr(previous_results, "completed_steps", ())
322
+ if name not in previous_results
323
+ ]
324
+ if released:
325
+ message += (
326
+ f". Earlier steps that ran: {released} - their results were "
327
+ f"released because no remaining step references them"
328
+ )
329
+ return KeyError(message)
330
+
331
+
332
+ def previous_result_reference_errors(workflow_definition, source_indices=None):
333
+ """Every 'previous_result:' reference that names no earlier step.
334
+
335
+ References resolve lazily, one step at a time, so a reference naming a
336
+ step that does not exist is only discovered when execution reaches it -
337
+ after everything before it has run. On a workflow whose steps are
338
+ generation steps that is 40 minutes of GPU spent to learn about a typo
339
+ a read of the file would have caught (T005). The names are all in the
340
+ definition, so this is answerable before anything runs.
341
+
342
+ The definition handed here has already been substituted and expanded,
343
+ so every reference in it is literal; a 'variable:' still spelled out is
344
+ one nothing resolved and is left alone.
345
+
346
+ `source_indices`, when given, is the source step index of each step -
347
+ expansion of a 'for_each' group turns one written step into several, and
348
+ the path an error carries has to be one the author can find in the file
349
+ they wrote.
350
+ """
351
+ steps = workflow_definition.get("steps")
352
+ if not isinstance(steps, list):
353
+ return []
354
+
355
+ errors = []
356
+ seen = []
357
+ for index, step in enumerate(steps):
358
+ if not isinstance(step, dict):
359
+ continue
360
+ name = step.get("name")
361
+ found = {}
362
+ _collect_reference_paths(step, (), found)
363
+ for path, reference in sorted(found.items(), key=lambda item: str(item[0])):
364
+ if any(reference_resolves_to(reference, name) for name in seen):
365
+ continue
366
+ source = (
367
+ source_indices[index]
368
+ if source_indices is not None and index < len(source_indices)
369
+ else index
370
+ )
371
+ location = render_path(("steps", source) + path)
372
+ # Which expansion it was: the source path alone points at the one
373
+ # step the author wrote, and every member reports the same path
374
+ where = (
375
+ f" in member '{name}'"
376
+ if isinstance(name, str) and MEMBER_SEPARATOR in name
377
+ else ""
378
+ )
379
+ errors.append(
380
+ {
381
+ "path": location,
382
+ "message": (
383
+ f"previous_result '{reference}'{where} names no earlier "
384
+ f"step. Steps available here: {seen}"
385
+ ),
386
+ }
387
+ )
388
+ if isinstance(name, str):
389
+ seen.append(name)
390
+ return errors
391
+
392
+
393
+ def _collect_reference_paths(value, path, found):
394
+ """Every literal previous-result reference under `value`, by JSON path.
395
+
396
+ Both spellings: the 'previous_result:' prefix on a string, and the
397
+ 'from_previous_result' key of a constructed object, which names a step
398
+ without the prefix.
399
+ """
400
+ if isinstance(value, dict):
401
+ for key, item in value.items():
402
+ if key == FROM_PREVIOUS_RESULT_KEY and isinstance(item, str):
403
+ if not item.startswith("variable:"):
404
+ found[path + (key,)] = item
405
+ continue
406
+ _collect_reference_paths(item, path + (key,), found)
407
+ elif isinstance(value, list):
408
+ for index, item in enumerate(value):
409
+ _collect_reference_paths(item, path + (index,), found)
410
+ elif isinstance(value, str) and value.startswith(PREVIOUS_RESULT_PREFIX):
411
+ found[path] = value[len(PREVIOUS_RESULT_PREFIX) :]
dw/probe_paths.py ADDED
@@ -0,0 +1,59 @@
1
+ """The local file a validate-time media probe may read, or None.
2
+
3
+ `dissolve_frame_errors` (#400) and `slice_preflight` (#402) each probe an
4
+ input's real length before the run, and each echoes what the probe found.
5
+ A free `validate_workflow` must not answer questions about files the run
6
+ itself would refuse to read, so a literal path goes through the same
7
+ containment policy the run applies (`validate_media_path`, lifted under
8
+ --trust-workflows) and is resolved against the workflow's directory; one it
9
+ refuses defers to the run, and `location_errors` reports the refusal.
10
+ """
11
+
12
+ import os
13
+
14
+ from .assets import fetch_asset, is_asset_reference
15
+ from .locations import is_http_url, validate_media_path
16
+ from .runs import fetch_output, is_output_reference
17
+
18
+ # Left to the run-time check: not yet resolved to a real file at the point
19
+ # validation walks the expanded definition.
20
+ UNRESOLVED_PREFIXES = ("previous_result:", "variable:", "item:", "gather:")
21
+
22
+
23
+ def resolve_probe_path(value, base_dir, what="a media argument"):
24
+ """The local file `value` names, or None when it is not yet resolvable,
25
+ is not a local file, does not exist, or lies outside what this workflow
26
+ may read - any of which defers the check to the run.
27
+
28
+ An `asset:`/`output:` reference resolves through its own confined
29
+ resolver; a literal path is resolved against `base_dir` and must pass
30
+ `validate_media_path`.
31
+ """
32
+ if not isinstance(value, str) or not value:
33
+ return None
34
+ if value.startswith(UNRESOLVED_PREFIXES) or is_http_url(value):
35
+ return None
36
+ if is_asset_reference(value) or is_output_reference(value):
37
+ try:
38
+ value = (
39
+ fetch_asset(value, base_dir=base_dir)
40
+ if is_asset_reference(value)
41
+ else fetch_output(value)
42
+ )
43
+ except Exception:
44
+ # Existence/traversal problems belong to reference_name_errors
45
+ # and reference resolution at run time, not to this check
46
+ return None
47
+ if not isinstance(value, str):
48
+ return None
49
+ else:
50
+ try:
51
+ value = validate_media_path(value, base_dir, what, require_exists=False)
52
+ except Exception:
53
+ # A refusal is location_errors' to report; echoing anything
54
+ # about the file here would answer what the run will not read
55
+ return None
56
+ return value if os.path.isfile(value) else None
57
+
58
+
59
+ __all__ = ["UNRESOLVED_PREFIXES", "resolve_probe_path"]
dw/prompt_schema.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "prompt_schema.json",
4
+ "title": "Stored prompt",
5
+ "description": "One prompt in the prompt library - the text a workflow loads with a 'prompt:' reference, plus the metadata the library pages show.",
6
+ "type": "object",
7
+ "required": ["text"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "text": {
11
+ "type": "string",
12
+ "minLength": 1,
13
+ "description": "The prompt text itself - what a 'prompt:' reference resolves to. May not begin with a reference prefix ('previous_result:', 'variable:', 'constant:', 'prompt:')."
14
+ },
15
+ "description": {
16
+ "type": "string",
17
+ "description": "Short summary shown on the prompt's library card."
18
+ },
19
+ "intended_model": {
20
+ "type": "string",
21
+ "description": "The model or model family the prompt was written for, such as 'minimax-h3'. Informational - the engine ignores it - but one spelling per family: 'list_prompts' narrows by an exact match, so a variant hides the prompt from its own family. The repo's library is swept by tests/test_prompt_library.py. Leave it out for text written for no particular model."
22
+ },
23
+ "negative_prompt": {
24
+ "type": "string",
25
+ "description": "A negative prompt paired with the text, for models that take one."
26
+ },
27
+ "tags": {
28
+ "type": "array",
29
+ "items": { "type": "string" },
30
+ "description": "Free-form labels for filtering the library."
31
+ },
32
+ "enhanced": {
33
+ "type": "object",
34
+ "additionalProperties": false,
35
+ "description": "Provenance when the text came from the AI enhancer - which model expanded which idea.",
36
+ "properties": {
37
+ "model": {
38
+ "type": "string",
39
+ "description": "The LLM that generated the text."
40
+ },
41
+ "idea": {
42
+ "type": "string",
43
+ "description": "The idea the text was expanded from."
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }