diffusers-workflow 0.4.0a3__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 (171) hide show
  1. diffusers_workflow-0.4.0a3.dist-info/METADATA +310 -0
  2. diffusers_workflow-0.4.0a3.dist-info/RECORD +171 -0
  3. diffusers_workflow-0.4.0a3.dist-info/WHEEL +5 -0
  4. diffusers_workflow-0.4.0a3.dist-info/entry_points.txt +6 -0
  5. diffusers_workflow-0.4.0a3.dist-info/licenses/LICENSE +201 -0
  6. diffusers_workflow-0.4.0a3.dist-info/top_level.txt +1 -0
  7. dw/__init__.py +353 -0
  8. dw/arguments.py +906 -0
  9. dw/cache_blocks.json +16 -0
  10. dw/cache_blocks.py +145 -0
  11. dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
  12. dw/events.py +78 -0
  13. dw/hub_cache.py +289 -0
  14. dw/introspection.py +458 -0
  15. dw/log_setup.py +45 -0
  16. dw/pipeline_processors/chain.py +750 -0
  17. dw/pipeline_processors/config_objects.py +235 -0
  18. dw/pipeline_processors/pipeline.py +1687 -0
  19. dw/pipeline_processors/remote.py +18 -0
  20. dw/previous_results.py +259 -0
  21. dw/prompt_weighting.py +378 -0
  22. dw/repl.py +298 -0
  23. dw/repl_commands.py +808 -0
  24. dw/repl_worker.py +129 -0
  25. dw/result.py +850 -0
  26. dw/run.py +92 -0
  27. dw/schema.py +24 -0
  28. dw/security.py +379 -0
  29. dw/serve.py +70 -0
  30. dw/server/__init__.py +2 -0
  31. dw/server/app.py +588 -0
  32. dw/server/jobs.py +547 -0
  33. dw/server/ui/assets/abap-08VXUWAP.js +1 -0
  34. dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
  35. dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  36. dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
  37. dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
  38. dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
  39. dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
  40. dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
  41. dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
  42. dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
  43. dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
  44. dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
  45. dw/server/ui/assets/css-DIMkf-bt.js +3 -0
  46. dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
  47. dw/server/ui/assets/cssMode-CEh6hWi2.js +1 -0
  48. dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
  49. dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
  50. dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  51. dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
  52. dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
  53. dw/server/ui/assets/editor.api-CExg3_mM.js +847 -0
  54. dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
  55. dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
  56. dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
  57. dw/server/ui/assets/freemarker2-DH6orYh2.js +3 -0
  58. dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  59. dw/server/ui/assets/go-C-y9NEjX.js +1 -0
  60. dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
  61. dw/server/ui/assets/handlebars-CbrMVW4Q.js +1 -0
  62. dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
  63. dw/server/ui/assets/html-YDNPZw2M.js +1 -0
  64. dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
  65. dw/server/ui/assets/htmlMode-B_zSGWO2.js +1 -0
  66. dw/server/ui/assets/index-B7-VcYS-.css +1 -0
  67. dw/server/ui/assets/index-D_EiPU3b.js +13 -0
  68. dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
  69. dw/server/ui/assets/java-BEtHBSE6.js +1 -0
  70. dw/server/ui/assets/javascript-dYuBvioq.js +1 -0
  71. dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
  72. dw/server/ui/assets/jsonMode-CUqLM39V.js +7 -0
  73. dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
  74. dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
  75. dw/server/ui/assets/less-B9JPFI3C.js +2 -0
  76. dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
  77. dw/server/ui/assets/liquid-D6vxBzMv.js +1 -0
  78. dw/server/ui/assets/lspLanguageFeatures-1WJ2palX.js +4 -0
  79. dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
  80. dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
  81. dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
  82. dw/server/ui/assets/mdx-SHQb6vmD.js +1 -0
  83. dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
  84. dw/server/ui/assets/monaco--ixms01u.css +1 -0
  85. dw/server/ui/assets/monaco-CP-s5rcP.js +56 -0
  86. dw/server/ui/assets/msdax-DauUninz.js +1 -0
  87. dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
  88. dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  89. dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
  90. dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  91. dw/server/ui/assets/perl-oz_6vUea.js +1 -0
  92. dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
  93. dw/server/ui/assets/php-nr791fC2.js +1 -0
  94. dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
  95. dw/server/ui/assets/postiats-43DmfD33.js +1 -0
  96. dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
  97. dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
  98. dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
  99. dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
  100. dw/server/ui/assets/python-x0_EGHq9.js +1 -0
  101. dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
  102. dw/server/ui/assets/r-BwWrilGY.js +1 -0
  103. dw/server/ui/assets/razor-BZC4LQDP.js +1 -0
  104. dw/server/ui/assets/redis-ClamHrr6.js +1 -0
  105. dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
  106. dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  107. dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
  108. dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
  109. dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
  110. dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
  111. dw/server/ui/assets/scheme-BeGwcela.js +1 -0
  112. dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
  113. dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
  114. dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
  115. dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
  116. dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  117. dw/server/ui/assets/sql-NEE52Syq.js +1 -0
  118. dw/server/ui/assets/st-DbInun42.js +1 -0
  119. dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
  120. dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  121. dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
  122. dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
  123. dw/server/ui/assets/tsMode-BTfA6SbD.js +11 -0
  124. dw/server/ui/assets/twig-De2hgUGE.js +1 -0
  125. dw/server/ui/assets/typescript-CWA4MsNk.js +1 -0
  126. dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
  127. dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
  128. dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
  129. dw/server/ui/assets/workers-CWU0uvj5.js +1 -0
  130. dw/server/ui/assets/xml-KmfTm3rg.js +1 -0
  131. dw/server/ui/assets/yaml-nFO_dDS6.js +1 -0
  132. dw/server/ui/index.html +17 -0
  133. dw/settings.py +77 -0
  134. dw/step.py +132 -0
  135. dw/tasks/audio_utils.py +266 -0
  136. dw/tasks/background_remover.py +43 -0
  137. dw/tasks/borders.py +113 -0
  138. dw/tasks/concat_videos.py +80 -0
  139. dw/tasks/depth_estimator.py +54 -0
  140. dw/tasks/diffusion_upscale.py +109 -0
  141. dw/tasks/format_messages.py +24 -0
  142. dw/tasks/gather.py +139 -0
  143. dw/tasks/image_to_text.py +43 -0
  144. dw/tasks/image_utils.py +661 -0
  145. dw/tasks/interpolate_frames.py +227 -0
  146. dw/tasks/model_cache.py +39 -0
  147. dw/tasks/pair_audio.py +58 -0
  148. dw/tasks/qr_code.py +19 -0
  149. dw/tasks/restore_faces.py +175 -0
  150. dw/tasks/rife_model.py +192 -0
  151. dw/tasks/segment.py +121 -0
  152. dw/tasks/task.py +474 -0
  153. dw/tasks/tensor_image.py +57 -0
  154. dw/tasks/text_generation.py +168 -0
  155. dw/tasks/text_sections.py +80 -0
  156. dw/tasks/upscale.py +203 -0
  157. dw/tasks/video_utils.py +154 -0
  158. dw/tasks/zoe_depth.py +71 -0
  159. dw/teacache.py +376 -0
  160. dw/teacache_models.json +99 -0
  161. dw/test.py +29 -0
  162. dw/type_helpers.py +68 -0
  163. dw/validate.py +43 -0
  164. dw/variables.py +153 -0
  165. dw/worker.py +517 -0
  166. dw/workflow.py +553 -0
  167. dw/workflow_schema.json +1157 -0
  168. dw/workflows/augment_prompt.json +65 -0
  169. dw/workflows/describe_image.json +58 -0
  170. dw/workflows/h3_context_ir.json +57 -0
  171. dw/workflows/test.json +31 -0
dw/workflow.py ADDED
@@ -0,0 +1,553 @@
1
+ # Core functionality for loading and executing workflows
2
+ import os
3
+ import json
4
+ import torch
5
+ import copy
6
+ import gc
7
+ import hashlib
8
+ import logging
9
+ from .arguments import realize_args, realize_constants
10
+ from .events import (
11
+ RunContext,
12
+ WorkflowCancelled,
13
+ get_context,
14
+ current_context,
15
+ activate_context,
16
+ deactivate_context,
17
+ )
18
+ from .step import Step
19
+ from .schema import validate_data, load_schema
20
+ from .variables import replace_variables, set_variables
21
+ from .pipeline_processors.pipeline import Pipeline
22
+ from .tasks.model_cache import clear_model_cache
23
+ from .tasks.task import Task
24
+ from . import get_device, empty_device_cache
25
+ from .security import (
26
+ validate_path,
27
+ validate_workflow_path,
28
+ validate_json_size,
29
+ validate_output_path,
30
+ SecurityError,
31
+ PathTraversalError,
32
+ InvalidInputError,
33
+ )
34
+
35
+ logger = logging.getLogger("dw")
36
+
37
+
38
+ def workflow_from_file(file_spec, output_dir):
39
+ """Loads a workflow from a JSON file with security validation"""
40
+ logger.debug(f"Loading workflow from file: {file_spec}")
41
+
42
+ try:
43
+ # Validate file path and size
44
+ validated_path = validate_workflow_path(file_spec)
45
+ validate_json_size(validated_path)
46
+ validated_output = validate_output_path(output_dir, None)
47
+
48
+ with open(validated_path, "r") as file:
49
+ workflow_data = json.load(file)
50
+
51
+ return Workflow(workflow_data, validated_output, validated_path)
52
+
53
+ except SecurityError as e:
54
+ logger.error(f"Security validation failed for workflow {file_spec}: {e}")
55
+ raise
56
+ except (json.JSONDecodeError, OSError) as e:
57
+ logger.error(f"Failed to load workflow from {file_spec}: {e}")
58
+ raise
59
+
60
+
61
+ def workflow_from_definition(workflow_definition, output_dir, base_dir=None):
62
+ """A Workflow from an inline definition (no file on disk).
63
+
64
+ The synthetic '__inline__.json' file_spec exists only to carry the
65
+ directory that relative paths inside the definition resolve against.
66
+ base_dir is caller-supplied (over HTTP, client-supplied) path-shaped
67
+ input, so it goes through the security validator like every other path.
68
+ """
69
+ validated_output = validate_output_path(output_dir, None)
70
+ if base_dir:
71
+ validated_base = validate_path(base_dir, allow_create=False)
72
+ if not os.path.isdir(validated_base):
73
+ raise InvalidInputError(f"base_dir is not a directory: {base_dir}")
74
+ else:
75
+ validated_base = os.getcwd()
76
+ return Workflow(
77
+ workflow_definition,
78
+ validated_output,
79
+ os.path.join(validated_base, "__inline__.json"),
80
+ )
81
+
82
+
83
+ def pipeline_cache_key(pipeline_definition):
84
+ """Stable identity for a loaded pipeline.
85
+
86
+ Hashes everything that shapes loading - configuration, components,
87
+ quantization, loras - and excludes what varies per call (arguments, seed,
88
+ chain), so a cache hit means "this exact model stack is already loaded".
89
+ Keying the cache by identity instead of step name means two workflows
90
+ whose steps happen to share a name can no longer collide, and a rerun of
91
+ an edited workflow keeps every pipeline whose definition did not change.
92
+
93
+ Computed after variable substitution but the excluded keys keep realized
94
+ per-run values (images, generators) out of the hash; realized types and
95
+ dtypes stringify stably via default=str.
96
+ """
97
+ load_definition = {
98
+ k: v
99
+ for k, v in pipeline_definition.items()
100
+ if k not in ("arguments", "seed", "chain")
101
+ }
102
+ serialized = json.dumps(load_definition, sort_keys=True, default=str)
103
+ return hashlib.sha256(serialized.encode()).hexdigest()
104
+
105
+
106
+ def referenced_result_names(steps):
107
+ """Every previous_result reference the given steps make, as full names.
108
+
109
+ Scans nested dicts and lists, so references inside pipeline arguments,
110
+ task arguments and sub-workflow argument maps are all found.
111
+ """
112
+ prefix = "previous_result:"
113
+ names = set()
114
+
115
+ def scan(value):
116
+ if isinstance(value, str) and value.startswith(prefix):
117
+ names.add(value[len(prefix) :])
118
+ elif isinstance(value, dict):
119
+ for item in value.values():
120
+ scan(item)
121
+ elif isinstance(value, list):
122
+ for item in value:
123
+ scan(item)
124
+
125
+ for step in steps:
126
+ scan(step)
127
+ return names
128
+
129
+
130
+ def release_unreferenced_results(results, remaining_refs):
131
+ """Drop results no remaining reference can resolve to.
132
+
133
+ A reference resolves to a result whose name it equals or extends with a
134
+ property ('step.mask'), so any result that is such a prefix stays. Saved
135
+ artifacts are already on disk - holding every intermediate image and frame
136
+ list in RAM until the workflow ends is what OOMs long chains.
137
+ """
138
+ for name in [
139
+ n
140
+ for n in results
141
+ if not any(ref == n or ref.startswith(n + ".") for ref in remaining_refs)
142
+ ]:
143
+ logger.debug(f"Releasing result: {name}")
144
+ del results[name]
145
+
146
+
147
+ class Workflow:
148
+ """
149
+ Main class for managing and executing workflows defined in JSON format
150
+ Handles variable substitution, step execution, and result management
151
+ """
152
+
153
+ def __init__(self, workflow_definition, output_dir, file_spec):
154
+ self.workflow_definition = workflow_definition
155
+ self.output_dir = output_dir
156
+ self.file_spec = file_spec
157
+
158
+ @property
159
+ def name(self):
160
+ return self.workflow_definition.get("id", "unknown")
161
+
162
+ @property
163
+ def argument_template(self):
164
+ return self.workflow_definition.get("argument_template", {})
165
+
166
+ @property
167
+ def variables(self):
168
+ return self.workflow_definition.get("variables", {})
169
+
170
+ def step_file_prefix(self, step_name):
171
+ """Naming prefix for files a step writes on its own (chain segment
172
+ spills), matching the workflow-id-step naming its results are saved
173
+ under."""
174
+ return f"{self.name}-{step_name}"
175
+
176
+ def validate(self):
177
+ """Validates workflow definition against JSON schema"""
178
+ logger.debug(f"Validating workflow: {self.name}")
179
+ status, message = validate_data(
180
+ self.workflow_definition, load_schema("workflow")
181
+ )
182
+ if not status:
183
+ logger.error(f"Validation error: {message}")
184
+ raise Exception(f"Validation error: {message}")
185
+ logger.debug(f"Workflow {self.name} validated successfully")
186
+
187
+ def run(
188
+ self, arguments, previous_pipelines=None, context=None, prior_step_keys=None
189
+ ):
190
+ """
191
+ Executes the workflow by:
192
+ 1. Processing variables
193
+ 2. Setting up random seed
194
+ 3. Running each step in sequence
195
+ 4. Managing results between steps
196
+
197
+ An explicit RunContext receives progress events and can cancel the
198
+ run; without one, the ambient context is reused (a sub-workflow
199
+ reports into its parent's run) or a no-op context is created.
200
+ Saved file paths accumulate in self.manifest, one entry per step.
201
+ """
202
+ run_context = context or current_context() or RunContext()
203
+ context_token = activate_context(run_context)
204
+ # Step name -> cache key for this run, so release_pipeline and
205
+ # pipeline_reference still address pipelines by the step that made them
206
+ self._pipeline_keys_by_step = {}
207
+ # Last run's step->key map: a redefined step's old model is evicted
208
+ # BEFORE its replacement loads, or the transition holds both at once
209
+ self._prior_step_keys = prior_step_keys or {}
210
+ self.manifest = []
211
+ try:
212
+ # CRITICAL: Work on a copy to avoid mutating the original workflow definition
213
+ # This allows the workflow to be run multiple times with different arguments
214
+ workflow_def = copy.deepcopy(self.workflow_definition)
215
+
216
+ workflow_id = workflow_def["id"]
217
+ logger.debug(f"Processing workflow: {workflow_id}")
218
+
219
+ # File paths in workflows are relative to the workflow file
220
+ base_dir = (
221
+ os.path.dirname(os.path.abspath(self.file_spec))
222
+ if self.file_spec
223
+ else None
224
+ )
225
+
226
+ # Handle variable substitution if variables are defined
227
+ variables = workflow_def.get("variables", None)
228
+ if variables is not None:
229
+ logger.debug(f"Setting variables for workflow: {workflow_id}")
230
+ # a constant is the value a variable declares, so it resolves before
231
+ # anything is converted to the type of that declaration
232
+ realize_constants(variables)
233
+ # first set variable values base don the arguments passed to the workflow
234
+ # these may come form the command line or form a parent workflow
235
+ set_variables(arguments, variables)
236
+ # realize the variables, initialiting downloads of images etc
237
+ realize_args(variables, base_dir)
238
+ ## then replace any variable references in the workflow definition with the actual values
239
+ replace_variables(workflow_def, variables)
240
+
241
+ # Set up random seed for reproducibility. Resolved lazily - as a
242
+ # dict.get default, torch.seed() would run on every call and reseed
243
+ # the global RNG even when the workflow names an explicit seed
244
+ default_seed = workflow_def.get("seed")
245
+ if default_seed is None:
246
+ # A fresh generator draws a random seed without touching the
247
+ # global RNG the process may have seeded for reproducibility
248
+ default_seed = torch.Generator().seed()
249
+ workflow_def["seed"] = default_seed
250
+
251
+ # Initialize collections for sharing state between steps
252
+ results = {} # Stores results from each step
253
+ shared_components = {} # Shared resources between steps
254
+
255
+ # Use provided pipelines cache or create new dict
256
+ # This allows pipeline reuse across multiple workflow runs
257
+ if previous_pipelines is None:
258
+ pipelines = {}
259
+ logger.debug("Starting with empty pipeline cache")
260
+ else:
261
+ pipelines = previous_pipelines
262
+ logger.debug(f"Reusing pipeline cache with {len(pipelines)} pipelines")
263
+
264
+ last_result = None # Final result is the workflow return value
265
+
266
+ # realize any arguments for the steps, i.e. load images etc
267
+ # that are referenced directly in the step
268
+ steps = workflow_def.get("steps", [])
269
+
270
+ if not steps:
271
+ logger.warning(f"Workflow {workflow_id} has no steps defined")
272
+ return []
273
+
274
+ realize_args(steps, base_dir)
275
+
276
+ run_context.emit(
277
+ "workflow_start",
278
+ workflow=workflow_id,
279
+ total_steps=len(steps),
280
+ steps=[step_data["name"] for step_data in steps],
281
+ seed=default_seed,
282
+ )
283
+
284
+ # Execute each step in sequence
285
+ for i, step_data in enumerate(steps):
286
+ run_context.check_cancelled()
287
+ logger.debug(f"Running step {i+1}/{len(steps)}: {step_data['name']}")
288
+ run_context.emit(
289
+ "step_start",
290
+ workflow=workflow_id,
291
+ step=step_data["name"],
292
+ index=i,
293
+ total_steps=len(steps),
294
+ )
295
+
296
+ # Seeds resolve most-specific-first: pipeline > step > workflow
297
+ step_seed = step_data.get("seed", default_seed)
298
+
299
+ step = Step(step_data, step_seed, self.workflow_definition)
300
+ step_action = self.create_step_action(
301
+ step_data,
302
+ shared_components,
303
+ pipelines,
304
+ step_seed,
305
+ get_device(),
306
+ )
307
+ result = step.run(results, pipelines, step_action)
308
+ last_result = result
309
+ results[step.name] = result
310
+ saved_files = result.save(
311
+ self.output_dir, f"{workflow_id}-{step.name}.{i}"
312
+ )
313
+ self.manifest.append({"step": step.name, "files": saved_files})
314
+ # A sub-workflow's saves land in the child's manifest - roll
315
+ # them up so job history and the gallery see every file
316
+ if isinstance(step_action, Workflow):
317
+ self.manifest.extend(getattr(step_action, "manifest", []))
318
+ run_context.emit(
319
+ "step_end",
320
+ workflow=workflow_id,
321
+ step=step.name,
322
+ index=i,
323
+ total_steps=len(steps),
324
+ files=saved_files,
325
+ )
326
+ logger.debug(f"Step {step.name} completed with result: {result}")
327
+
328
+ # Release results no later step references - saved to disk
329
+ # already, and last_result keeps the workflow's return value
330
+ release_unreferenced_results(
331
+ results, referenced_result_names(steps[i + 1 :])
332
+ )
333
+
334
+ # A released pipeline frees its memory for later steps - the
335
+ # alternative on a card that cannot hold two models is offloading
336
+ # everything, which taxes every run to survive one transition
337
+ if step_data.get("release_pipeline", False):
338
+ logger.info(f"Releasing pipeline for step: {step.name}")
339
+ pipelines.pop(self._pipeline_keys_by_step.get(step.name), None)
340
+
341
+ # Task models are cached for the life of the process - the cache
342
+ # exists so a step's cartesian product loads its model once, and
343
+ # nothing else evicts it. A prompt-expanding language model
344
+ # feeding a generation step would otherwise hold its weights on
345
+ # the device for the whole run
346
+ if step_data.get("release_models", False):
347
+ logger.info(f"Releasing task models for step: {step.name}")
348
+ clear_model_cache()
349
+
350
+ # Cleanup between steps (but keep pipelines loaded). Returning
351
+ # cached blocks to the device lets the next step's differently
352
+ # shaped allocations use them
353
+ gc.collect()
354
+ empty_device_cache()
355
+
356
+ logger.debug(f"Workflow {workflow_id} completed successfully")
357
+ run_context.emit(
358
+ "workflow_end", workflow=workflow_id, manifest=self.manifest
359
+ )
360
+ # Return only the last step's results for child workflows
361
+ return last_result.result_list if last_result is not None else []
362
+
363
+ except WorkflowCancelled:
364
+ # The user asked for this - report it without an error traceback
365
+ workflow_id = self.workflow_definition.get("id", "unknown")
366
+ logger.info(f"Workflow {workflow_id} cancelled")
367
+ raise
368
+ except (SecurityError, PathTraversalError, InvalidInputError) as e:
369
+ # Security validation failures - these should fail fast, without the
370
+ # traceback noise of the general handler
371
+ workflow_id = self.workflow_definition.get("id", "unknown")
372
+ logger.error(f"Security error in workflow {workflow_id}: {e}")
373
+ raise
374
+ except Exception as e:
375
+ # One log line with the full traceback - the step already logged its
376
+ # own context, and every clause here did the same log-and-reraise
377
+ workflow_id = self.workflow_definition.get("id", "unknown")
378
+ logger.error(
379
+ f"{type(e).__name__} in workflow {workflow_id}: {e}", exc_info=True
380
+ )
381
+ raise
382
+ finally:
383
+ deactivate_context(context_token)
384
+
385
+ def _step_pipeline_key(self, step_name, cache_key):
386
+ """Record which cache key a step's pipeline lives under this run."""
387
+ if not hasattr(self, "_pipeline_keys_by_step"):
388
+ self._pipeline_keys_by_step = {}
389
+ self._pipeline_keys_by_step[step_name] = cache_key
390
+
391
+ def create_step_action(
392
+ self,
393
+ step_definition,
394
+ shared_components,
395
+ previous_pipelines,
396
+ default_seed,
397
+ device,
398
+ ):
399
+ """
400
+ Creates the appropriate action object based on step type:
401
+ - Pipeline: Creates new pipeline or reuses cached one
402
+ - Pipeline reference: References existing pipeline
403
+ - Workflow: Loads and validates sub-workflow
404
+ - Task: Creates task object
405
+ """
406
+ # Handle pipeline creation
407
+ if "pipeline" in step_definition:
408
+ step_name = step_definition["name"]
409
+
410
+ # Pipelines are cached by what they load, not what step loads them
411
+ cache_key = pipeline_cache_key(step_definition["pipeline"])
412
+ self._step_pipeline_key(step_name, cache_key)
413
+ get_context().touch_pipeline(cache_key)
414
+
415
+ # Check if pipeline already loaded in cache (GPU persistence)
416
+ if cache_key in previous_pipelines:
417
+ logger.debug(f"Reusing cached pipeline for step: {step_name}")
418
+ cached_pipeline = previous_pipelines[cache_key]
419
+ # The shared_components dict is fresh every run and only load()
420
+ # fills it - a cache hit must republish or a later step's
421
+ # reused_components finds nothing (impossible under the old
422
+ # whole-file cache, the normal case under identity keys)
423
+ cached_pipeline.publish_shared_components(shared_components)
424
+ # Create new Pipeline wrapper with updated step definition
425
+ # but reuse the loaded model from cache
426
+ new_pipeline_wrapper = Pipeline(
427
+ step_definition["pipeline"],
428
+ default_seed,
429
+ device,
430
+ cached_pipeline.pipeline, # Reuse the actual loaded model
431
+ output_dir=self.output_dir,
432
+ file_prefix=self.step_file_prefix(step_name),
433
+ )
434
+ # Set up generator with potentially new seed. no_generator is a
435
+ # boolean - only an explicit true disables the generator - and the
436
+ # generator lives on the pipeline's own device, which may override
437
+ # the workflow default (the fresh-load path resolves it the same way)
438
+ if not new_pipeline_wrapper.configuration.get("no_generator", False):
439
+ logger.debug(
440
+ "Setting up generator for cached pipeline with new arguments"
441
+ )
442
+ new_pipeline_wrapper.argument_template[
443
+ "generator"
444
+ ] = torch.Generator(new_pipeline_wrapper.device).manual_seed(
445
+ new_pipeline_wrapper.pipeline_definition.get(
446
+ "seed", default_seed
447
+ )
448
+ )
449
+
450
+ return new_pipeline_wrapper
451
+
452
+ # Not in cache - a redefined step frees its previous model first,
453
+ # so the swap never holds old and new stacks simultaneously
454
+ prior_key = getattr(self, "_prior_step_keys", {}).get(step_name)
455
+ if prior_key and prior_key != cache_key and prior_key in previous_pipelines:
456
+ logger.info(
457
+ f"Step '{step_name}' was redefined - releasing its previous "
458
+ "pipeline before loading the new one"
459
+ )
460
+ previous_pipelines.pop(prior_key, None)
461
+ gc.collect()
462
+ empty_device_cache()
463
+
464
+ logger.debug(f"Creating pipeline for step: {step_name}")
465
+ pipeline = Pipeline(
466
+ step_definition["pipeline"],
467
+ default_seed,
468
+ device,
469
+ output_dir=self.output_dir,
470
+ file_prefix=self.step_file_prefix(step_name),
471
+ )
472
+ pipeline.load(shared_components)
473
+ previous_pipelines[cache_key] = pipeline
474
+ return pipeline
475
+
476
+ # Handle pipeline reference
477
+ if "pipeline_reference" in step_definition:
478
+ logger.debug(
479
+ f"Referencing existing pipeline for step: {step_definition['name']}"
480
+ )
481
+ pipeline_reference = step_definition["pipeline_reference"]
482
+ reference_name = pipeline_reference["reference_name"]
483
+ referenced_key = self._pipeline_keys_by_step.get(reference_name)
484
+ if referenced_key is None or referenced_key not in previous_pipelines:
485
+ raise ValueError(
486
+ f"pipeline_reference '{reference_name}' does not name an "
487
+ "earlier pipeline step in this run (or it was released)"
488
+ )
489
+ previous_pipeline = previous_pipelines[referenced_key]
490
+ return Pipeline(
491
+ pipeline_reference,
492
+ default_seed,
493
+ device,
494
+ previous_pipeline.pipeline,
495
+ output_dir=self.output_dir,
496
+ file_prefix=self.step_file_prefix(step_definition["name"]),
497
+ )
498
+
499
+ # Handle sub-workflow
500
+ if "workflow" in step_definition:
501
+ logger.debug(f"Loading sub-workflow for step: {step_definition['name']}")
502
+ workflow_reference = step_definition["workflow"]
503
+ path = workflow_reference["path"]
504
+
505
+ try:
506
+ # Handle built-in workflows
507
+ if path.startswith("builtin:"):
508
+ builtin_name = path.replace("builtin:", "")
509
+ # Validate builtin workflow name
510
+ if (
511
+ not builtin_name.endswith(".json")
512
+ or "/" in builtin_name
513
+ or "\\" in builtin_name
514
+ ):
515
+ raise InvalidInputError(
516
+ f"Invalid builtin workflow name: {builtin_name}"
517
+ )
518
+ path = os.path.join(
519
+ os.path.dirname(os.path.abspath(__file__)),
520
+ "workflows",
521
+ builtin_name,
522
+ )
523
+ # Handle relative paths
524
+ elif not os.path.isabs(path):
525
+ base_dir = os.path.dirname(self.file_spec)
526
+ path = os.path.join(base_dir, path)
527
+
528
+ # Validate the resolved path
529
+ validated_path = validate_workflow_path(path)
530
+ workflow = workflow_from_file(validated_path, self.output_dir)
531
+
532
+ except SecurityError as e:
533
+ logger.error(f"Security validation failed for sub-workflow {path}: {e}")
534
+ raise
535
+
536
+ # this is where the arguments in the paretn script are passed to the child workflow
537
+ # they will already be populated with values from previous steps or parent variables
538
+ workflow.workflow_definition["argument_template"] = workflow_reference.get(
539
+ "arguments", {}
540
+ )
541
+ # A child left to itself draws its own random seed, which makes the
542
+ # parent's seed stop short of the work it delegates. Inheriting it
543
+ # keeps one seed reproducing the whole run; a child that names its
544
+ # own still wins, the same way a step overrides its workflow
545
+ workflow.workflow_definition.setdefault("seed", default_seed)
546
+ workflow.validate()
547
+ return workflow
548
+
549
+ logger.debug(f"Creating task for step: {step_definition['name']}")
550
+ # Handle task creation
551
+ task_definition = step_definition["task"]
552
+ task = Task(task_definition, device)
553
+ return task