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/slice_preflight.py ADDED
@@ -0,0 +1,148 @@
1
+ """A `slice_audio` step whose source duration validate can already learn,
2
+ sliced past where that source ends, warned about before the run (#402).
3
+
4
+ `slice_audio` zero-pads a slice that reaches past its source and only says so
5
+ at run time (`_warn_on_slice_past_end`, `dw/tasks/audio_utils.py`) - a
6
+ correct message, but late when the slice sits downstream of a long render
7
+ (#402's repro: `assemble-and-score` scoring a 15.5 s cut with a 4.96 s
8
+ `score` asset, `validate_workflow` answering clean). Mirrors
9
+ `dissolve_frame_errors.py` (#400): walk the expanded definition,
10
+ `resolve_path_references` an `asset:`/`output:` audio into a real path, and
11
+ `probe_media` it - the same resolution and decode the run itself would do,
12
+ just ahead of the queue.
13
+
14
+ Deliberately narrower than the run-time check, same as #400's: a
15
+ `previous_result:` audio (nothing written yet), a remote URL, a literal path
16
+ outside the directories the run may read (`dw/probe_paths.py` - a literal
17
+ inside them is resolved against the workflow's directory and probed), or a
18
+ source `probe_media` cannot read, all answer "unknown" rather than
19
+ guessing - silence here is correct, not a gap, since the run-time warning
20
+ still fires once the file exists. `variable:` needs no hop of its own: by the
21
+ time `validation_errors`/`adapter_warnings` hand this module the *expanded*
22
+ definition, `replace_variables` has already substituted every `variable:`
23
+ reference (or the run cannot start at all), so what is left unresolved is
24
+ only a reference that genuinely cannot resolve yet. The threshold
25
+ (`SLICE_PAD_WARN_MS`) and the requested-length arithmetic mirror
26
+ `slice_audio`'s own two argument shapes, so the two agree on the same
27
+ padding for the same arguments.
28
+ """
29
+
30
+ from .for_each import MEMBER_SEPARATOR, render_path
31
+ from .media_info import probe_media
32
+ from .probe_paths import resolve_probe_path
33
+ from .tasks.audio_utils import SLICE_PAD_WARN_MS
34
+
35
+
36
+ def _source_seconds(path):
37
+ """The duration `slice_audio` would see for this file, or None when it
38
+ cannot be probed or carries no audio."""
39
+ info = probe_media(path)
40
+ if info is None or info.get("kind") != "audio":
41
+ return None
42
+ return info.get("duration_seconds")
43
+
44
+
45
+ def _as_number(value):
46
+ if isinstance(value, bool) or not isinstance(value, (int, float, str)):
47
+ return None
48
+ try:
49
+ return float(value)
50
+ except (TypeError, ValueError):
51
+ return None
52
+
53
+
54
+ def _requested_region(task_args):
55
+ """The (start_seconds, length_seconds) `slice_audio` would compute for
56
+ these arguments, or None when the shape given cannot be resolved to a
57
+ length without running anything - mirrors `slice_audio`'s own branch
58
+ order in `dw/tasks/audio_utils.py`."""
59
+ start_seconds = _as_number(task_args.get("start_seconds"))
60
+ duration_seconds = _as_number(task_args.get("duration_seconds"))
61
+ start_frame = _as_number(task_args.get("start_frame"))
62
+ num_frames = _as_number(task_args.get("num_frames"))
63
+ fps = _as_number(task_args.get("fps"))
64
+
65
+ if (
66
+ task_args.get("start_seconds") is not None
67
+ or task_args.get("duration_seconds") is not None
68
+ ):
69
+ if duration_seconds is None:
70
+ # Runs to the source's own end - cannot overrun it
71
+ return None
72
+ return start_seconds or 0.0, duration_seconds
73
+ if (
74
+ task_args.get("start_frame") is not None
75
+ or task_args.get("num_frames") is not None
76
+ ):
77
+ if num_frames is None or not fps:
78
+ return None
79
+ return (start_frame or 0.0) / fps, num_frames / fps
80
+ return None
81
+
82
+
83
+ def slice_past_end_warnings(workflow_definition, source_indices=None, base_dir=None):
84
+ """Every `slice_audio` step whose source's real duration is already
85
+ knowable and whose requested slice reaches past it, as messages.
86
+
87
+ Walks the substituted, expanded definition, the same convention
88
+ `dissolve_frame_errors` follows: `source_indices` maps an expanded step
89
+ back to the one the author wrote, and a path inside a `for_each` member
90
+ names the member.
91
+ """
92
+ steps = workflow_definition.get("steps")
93
+ if not isinstance(steps, list):
94
+ return []
95
+
96
+ warnings = []
97
+ for index, step in enumerate(steps):
98
+ if not isinstance(step, dict):
99
+ continue
100
+ task = step.get("task")
101
+ if not isinstance(task, dict) or task.get("command") != "slice_audio":
102
+ continue
103
+ task_args = task.get("arguments")
104
+ if not isinstance(task_args, dict):
105
+ continue
106
+
107
+ path = resolve_probe_path(task_args.get("audio"), base_dir, "an audio argument")
108
+ if path is None:
109
+ continue
110
+ source_seconds = _source_seconds(path)
111
+ if not source_seconds:
112
+ continue
113
+
114
+ region = _requested_region(task_args)
115
+ if region is None:
116
+ continue
117
+ requested_start, requested_length = region
118
+
119
+ available = max(0.0, min(source_seconds - requested_start, requested_length))
120
+ padded_seconds = requested_length - available
121
+ if padded_seconds * 1000.0 < SLICE_PAD_WARN_MS:
122
+ continue
123
+
124
+ source = (
125
+ source_indices[index]
126
+ if source_indices is not None and index < len(source_indices)
127
+ else index
128
+ )
129
+ name = step.get("name")
130
+ where = (
131
+ f" in member '{name}'"
132
+ if isinstance(name, str) and MEMBER_SEPARATOR in name
133
+ else ""
134
+ )
135
+ path_str = render_path(("steps", source, "task", "arguments", "audio"))
136
+ warnings.append(
137
+ f"{path_str}: slice_audio will run {padded_seconds:.2f} s past "
138
+ f"the end of a {source_seconds:.2f} s source ({task_args.get('audio')}), "
139
+ f"so that much of the {requested_start + requested_length:.2f} s "
140
+ f"requested will be digital silence{where}. If you meant to fill "
141
+ f"a cut of this length, make a bed with the 'loop_audio' task "
142
+ f"('target_frames' + 'fps' matches one exactly) and slice that; "
143
+ f"if you meant the tail pad, nothing is wrong."
144
+ )
145
+ return warnings
146
+
147
+
148
+ __all__ = ["slice_past_end_warnings"]
dw/step.py ADDED
@@ -0,0 +1,187 @@
1
+ import logging
2
+ from .events import WorkflowCancelled, get_context
3
+ from .result import Result
4
+ from .previous_results import get_iterations, resolve_chain_prompts
5
+
6
+ logger = logging.getLogger("dw")
7
+
8
+
9
+ class Step:
10
+ """
11
+ Represents a single step in a workflow execution.
12
+ Manages execution of pipelines, tasks, or sub-workflows with their configurations.
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ step_definition,
18
+ default_seed,
19
+ workflow_definition=None,
20
+ consumed_by_normalizer=False,
21
+ ):
22
+ """Initialize step with its configuration and seed value.
23
+
24
+ workflow_definition, when given, is the original (unsubstituted)
25
+ definition of the workflow this step belongs to - embedded metadata
26
+ carries it so a saved image can be reopened as the workflow that
27
+ made it.
28
+
29
+ consumed_by_normalizer is whether a later step resets this result's
30
+ level (normalize_audio/match_levels) before anything ships it - see
31
+ step_cache.normalized_downstream and result.py's headroom checks."""
32
+ self.step_definition = step_definition
33
+ self.workflow_definition = workflow_definition
34
+ self.consumed_by_normalizer = consumed_by_normalizer
35
+ self.iteration = None
36
+
37
+ # Get step-specific seed or use default if not specified
38
+ self.default_seed = self.step_definition.get("seed", default_seed)
39
+ logger.debug(f"Initialized step: {self.name} with seed: {self.default_seed}")
40
+
41
+ @property
42
+ def name(self):
43
+ return self.step_definition.get("name", "unknown")
44
+
45
+ def run(self, previous_results, previous_pipelines, step_action):
46
+ """
47
+ Execute the step's action with all possible argument combinations.
48
+
49
+ Args:
50
+ previous_results: Results from previous steps, used for argument generation
51
+ previous_pipelines: Previously created pipelines that might be referenced
52
+ step_action: The actual action to execute (Pipeline/Task/Workflow)
53
+ """
54
+ try:
55
+ step_name = self.step_definition["name"]
56
+ logger.debug(f"Starting execution of step: {step_name}")
57
+
58
+ # Create result container with any special configuration from step definition
59
+ # This handles how results should be saved/processed
60
+ result = Result(
61
+ self.step_definition.get("result", {}),
62
+ consumed_by_normalizer=self.consumed_by_normalizer,
63
+ )
64
+
65
+ # Collect metadata for embedding if enabled
66
+ result_def = self.step_definition.get("result", {})
67
+ if result_def.get("embed_metadata", False):
68
+ metadata = self._collect_metadata()
69
+ result.set_metadata(metadata)
70
+
71
+ # Log what type of action we're executing (Pipeline/Task/Workflow)
72
+ action_type = type(step_action).__name__
73
+ logger.info(f"Running {action_type} {step_name}:{step_action.name}...")
74
+
75
+ # A chained pipeline's per-segment prompts live outside the argument
76
+ # template, so they are resolved here rather than by the pass below
77
+ resolve_chain_prompts(step_action, previous_results)
78
+
79
+ # Get all possible argument combinations for this step
80
+ # This expands any references to previous results into concrete values
81
+ iterations = get_iterations(step_action.argument_template, previous_results)
82
+ logger.debug(f"Generated {len(iterations)} argument combinations")
83
+
84
+ # The metadata above was read from the definition, where an
85
+ # argument may still be a 'previous_result:' reference - now that
86
+ # the iterations exist, record what the step actually ran with
87
+ if result.metadata is not None:
88
+ _realize_metadata_arguments(result.metadata, iterations)
89
+
90
+ # Execute the action for each set of arguments
91
+ if not iterations:
92
+ logger.warning(f"Step {step_name} has no iterations to execute")
93
+ return result
94
+
95
+ run_context = get_context()
96
+ for i, arguments in enumerate(iterations, 1):
97
+ run_context.check_cancelled()
98
+ logger.debug(
99
+ f"Running iteration {i}/{len(iterations)} with arguments: {arguments}"
100
+ )
101
+ run_context.emit(
102
+ "iteration_start",
103
+ step=step_name,
104
+ iteration=i,
105
+ total_iterations=len(iterations),
106
+ )
107
+ self.iteration = i
108
+ iteration_result = step_action.run(arguments, previous_pipelines)
109
+ result.add_result(iteration_result)
110
+
111
+ logger.debug(f"Successfully completed step: {step_name}")
112
+ return result
113
+
114
+ except WorkflowCancelled:
115
+ logger.info(f"Step {self.name} cancelled")
116
+ raise
117
+ except Exception as e:
118
+ # One log line with the full traceback - callers decide handling.
119
+ # (Configuration, I/O, runtime and unexpected errors all logged and
120
+ # re-raised identically, so one clause replaces the old four)
121
+ iteration = getattr(self, "iteration", None)
122
+ where = f"iteration {iteration} of " if iteration else ""
123
+ logger.error(
124
+ f"{type(e).__name__} in {where}step {self.name}: {e}",
125
+ exc_info=True,
126
+ )
127
+ raise
128
+
129
+ def _collect_metadata(self):
130
+ """Collect step metadata for embedding in saved images."""
131
+ metadata = {"step_name": self.name}
132
+
133
+ # The whole recipe, not just this step's slice of it - this is what
134
+ # lets a gallery open an image as the workflow that produced it,
135
+ # and the seed is what makes reopening it reproduce this exact image
136
+ if self.workflow_definition is not None:
137
+ metadata["workflow"] = self.workflow_definition
138
+ metadata["seed"] = self.default_seed
139
+
140
+ if "pipeline" in self.step_definition:
141
+ pipeline_def = self.step_definition["pipeline"]
142
+ pretrained_args = pipeline_def.get("from_pretrained_arguments", {})
143
+ if "model_name" in pretrained_args:
144
+ metadata["model_name"] = pretrained_args["model_name"]
145
+ metadata["arguments"] = dict(pipeline_def.get("arguments", {}))
146
+
147
+ elif "task" in self.step_definition:
148
+ task_def = self.step_definition["task"]
149
+ metadata["task_command"] = task_def.get("command", "unknown")
150
+ metadata["arguments"] = dict(task_def.get("arguments", {}))
151
+
152
+ return metadata
153
+
154
+
155
+ # What json.dumps can embed without falling back to str() - anything else
156
+ # (a PIL image, a tensor, a video's frames) keeps its reference in metadata
157
+ JSON_SCALARS = (str, int, float, bool)
158
+
159
+
160
+ def _realize_metadata_arguments(metadata, iterations):
161
+ """Replace the embedded arguments with the values the step really ran with.
162
+
163
+ Step metadata is collected from the definition, so an argument written as
164
+ "previous_result:expand" is still a reference there - a gallery showing it
165
+ would display the reference instead of the prompt the pipeline saw. The
166
+ iterations hold the resolved values, so lift the JSON-safe ones back into
167
+ the metadata. When iterations disagree (a step fanned out over several
168
+ prior results) every distinct value is kept, in order.
169
+ """
170
+ arguments = metadata.get("arguments")
171
+ if not arguments or not iterations:
172
+ return
173
+
174
+ for key in list(arguments):
175
+ values = []
176
+ for iteration in iterations:
177
+ if not isinstance(iteration, dict) or key not in iteration:
178
+ values = []
179
+ break
180
+ value = iteration[key]
181
+ if not isinstance(value, JSON_SCALARS):
182
+ values = []
183
+ break
184
+ if value not in values:
185
+ values.append(value)
186
+ if values:
187
+ arguments[key] = values[0] if len(values) == 1 else values