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/arguments.py ADDED
@@ -0,0 +1,906 @@
1
+ import os
2
+ import copy
3
+ import logging
4
+ from inspect import Parameter, signature
5
+ from .type_helpers import load_type_from_name, load_constant_from_name, has_method
6
+ from diffusers.utils import load_image, load_video
7
+ from .security import (
8
+ validate_path,
9
+ validate_constant_name,
10
+ validate_url,
11
+ validate_file_extension,
12
+ SecurityError,
13
+ ALLOWED_IMAGE_EXTENSIONS,
14
+ ALLOWED_VIDEO_EXTENSIONS,
15
+ ALLOWED_AUDIO_EXTENSIONS,
16
+ )
17
+
18
+ logger = logging.getLogger("dw")
19
+
20
+ # Keys that end in '_type' but name a category rather than a python type. Any other such
21
+ # key can be escaped where it is used, by wrapping its value in braces
22
+ NON_TYPE_KEYS = {"content_type", "offload_type"}
23
+
24
+
25
+ class EscapedString(str):
26
+ """A string whose {} escape has already been consumed.
27
+
28
+ Arguments are realized twice - once for the workflow's variables, and again for
29
+ the steps the variables are substituted into. A variable's own name can look
30
+ like a type reference ("weights_dtype": "{int4}"), so the first pass strips the
31
+ braces and the second would load the bare name as a type. Marking the stripped
32
+ value keeps the second pass from touching it, while it stays an ordinary string
33
+ everywhere else.
34
+ """
35
+
36
+
37
+ def is_escaped(value):
38
+ """Whether a value is a type reference escaped with {} braces"""
39
+ return isinstance(value, str) and value.startswith("{") and value.endswith("}")
40
+
41
+
42
+ # The key naming the file an argument object is constructed from
43
+ FROM_FILE_KEY = "from_file"
44
+
45
+ # The key naming the step whose output an argument object is constructed from
46
+ FROM_PREVIOUS_RESULT_KEY = "from_previous_result"
47
+
48
+ # The key holding the arguments an argument object is constructed from, for a type
49
+ # that takes its contents as plain fields rather than opening media itself
50
+ FROM_ARGUMENTS_KEY = "from_arguments"
51
+
52
+ # The prefix marking a value as a reference to an earlier step's output. Those are
53
+ # substituted once that step has run, so an object whose arguments hold one is
54
+ # constructed then rather than at load time
55
+ PREVIOUS_RESULT_PREFIX = "previous_result:"
56
+
57
+ # The prefix marking a value as a reference to a constant declared in python - the
58
+ # schedule a distilled model was trained on, the negative prompt a model family ships.
59
+ # Copying those into a workflow is how they go stale when the library moves on
60
+ CONSTANT_PREFIX = "constant:"
61
+
62
+ # The media such an object may be built from - it opens the file itself, so the
63
+ # extension is all that is checked here
64
+ ALLOWED_FROM_FILE_EXTENSIONS = (
65
+ ALLOWED_IMAGE_EXTENSIONS | ALLOWED_VIDEO_EXTENSIONS | ALLOWED_AUDIO_EXTENSIONS
66
+ )
67
+
68
+ # The media kinds an object built from a previous step's output can carry. A type
69
+ # declares which one it is with a 'kind' attribute, the way MiniMax-H3's reference
70
+ # classes do, and that is what says which of its fields the media goes into
71
+ MEDIA_KINDS = ("image", "video", "audio")
72
+
73
+
74
+ # Helper functions for processing and loading workflow arguments
75
+ def realize_args(arg, base_dir=None):
76
+ """
77
+ Recursively processes workflow arguments to:
78
+ 1. Convert type references into actual Python types
79
+ 2. Load images from file paths/URLs
80
+ 3. Load videos from file paths/URLs
81
+ 4. Construct objects that name a type and the file to build it from
82
+
83
+ Args:
84
+ arg: The arguments to process, modified in place
85
+ base_dir: Directory relative file paths are resolved against - the
86
+ workflow file's directory. Defaults to the process working directory
87
+ """
88
+ if isinstance(arg, dict):
89
+ logger.debug(f"Processing dictionary arguments: {list(arg.keys())}")
90
+ for k, v in arg.items():
91
+ # A constant resolves under any argument name, and before the
92
+ # conventions below - what it holds is the value, not a file to load
93
+ if is_constant_reference(v):
94
+ arg[k] = fetch_constant(v)
95
+ # An explicit media reference loads under any argument name - the
96
+ # key conventions below only cover arguments named like their media
97
+ elif is_media_reference(v):
98
+ arg[k] = fetch_media(v, base_dir)
99
+ # Handle image loading for keys ending in '_image' or exactly 'image'
100
+ elif k.endswith("_image") or k == "image":
101
+ logger.debug(f"Loading image for key: {k}")
102
+ arg[k] = fetch_image(v, base_dir)
103
+ # Handle video loading for keys ending in '_video' or exactly 'video'
104
+ elif k.endswith("_video") or k == "video":
105
+ logger.debug(f"Loading video for key: {k}")
106
+ arg[k] = fetch_video(v, base_dir)
107
+ # Handle type references, and the keys that only look like one
108
+ elif k.endswith("_type") or k.endswith("_dtype") or k == "dtype":
109
+ if isinstance(v, EscapedString):
110
+ # An earlier pass already consumed this value's escape
111
+ continue
112
+ if k in NON_TYPE_KEYS:
113
+ # The value stays a string, but the {} escape is still honored
114
+ # so both the escaped and the bare spelling name the category
115
+ if is_escaped(v):
116
+ arg[k] = EscapedString(v.strip("{}"))
117
+ continue
118
+ logger.debug(f"Processing type reference for key: {k}")
119
+ # Allow escaping type references using {} brackets
120
+ # this is for instances when the argument name is "something_type" but it is
121
+ # not a reference to a python type, but rather a category or something else
122
+ if isinstance(v, str):
123
+ if is_escaped(v):
124
+ arg[k] = EscapedString(v.strip("{}"))
125
+ else:
126
+ arg[k] = load_type_from_name(v)
127
+ elif isinstance(v, type):
128
+ # the value already a type
129
+ arg[k] = v
130
+ # Recursively process nested dictionaries, then build any object they
131
+ # describe - the type reference it names is realized by the recursion
132
+ else:
133
+ realize_args(v, base_dir)
134
+ arg[k] = realize_object(v, base_dir)
135
+
136
+ # Recursively process lists
137
+ elif isinstance(arg, list):
138
+ logger.debug("Processing list arguments")
139
+ for i, item in enumerate(arg):
140
+ if is_constant_reference(item):
141
+ arg[i] = fetch_constant(item)
142
+ continue
143
+ if is_media_reference(item):
144
+ arg[i] = fetch_media(item, base_dir)
145
+ continue
146
+ realize_args(item, base_dir)
147
+ arg[i] = realize_object(item, base_dir)
148
+
149
+
150
+ def is_constant_reference(value):
151
+ """Whether a value references a constant declared in python."""
152
+ return isinstance(value, str) and value.startswith(CONSTANT_PREFIX)
153
+
154
+
155
+ def fetch_constant(reference):
156
+ """Read the value a 'constant:' reference names.
157
+
158
+ A constant is data - a schedule, a default prompt, a token budget - so what the
159
+ name resolves to has to be data too. Anything callable is refused: a type is
160
+ named with a '_type' key and constructed there, and a workflow that could reach
161
+ a function through this would be evaluating python rather than referencing it.
162
+
163
+ Mutable values are copied. The workflow holds the module's own object otherwise,
164
+ and a pipeline that consumes its sigmas in place would edit the library's
165
+ constant for every later run in the process - the REPL keeps one alive for a
166
+ whole session.
167
+
168
+ Args:
169
+ reference: The 'constant:dotted.NAME' string
170
+
171
+ Returns:
172
+ The value the name refers to
173
+
174
+ Raises:
175
+ ValueError: If the name resolves to nothing, or to something callable
176
+ InvalidInputError: If the name is not a dotted python name
177
+ """
178
+ name = validate_constant_name(reference.removeprefix(CONSTANT_PREFIX).strip())
179
+
180
+ try:
181
+ value = load_constant_from_name(name)
182
+ except (ImportError, AttributeError) as error:
183
+ raise ValueError(
184
+ f"No constant named '{name}' - a constant reference names the module "
185
+ f"it is declared in and the attribute to read from it ({error})"
186
+ ) from error
187
+
188
+ if callable(value):
189
+ raise ValueError(
190
+ f"'{name}' is a {type(value).__name__}, not a constant - "
191
+ f"'{CONSTANT_PREFIX}' reads a value, and a type is named with a "
192
+ f"'_type' argument instead"
193
+ )
194
+
195
+ logger.info(f"Reading constant {name}")
196
+ return copy.deepcopy(value) if isinstance(value, (list, dict, set)) else value
197
+
198
+
199
+ def realize_constants(arg):
200
+ """Resolve every constant reference in a structure, and nothing else.
201
+
202
+ Variables are declared before they are set: a value passed in is converted to
203
+ the type of the declared default, so a default that is still the string naming
204
+ a constant would type a schedule as text. Resolving them first makes the
205
+ constant the declared value, which is what it is meant to be.
206
+
207
+ Args:
208
+ arg: The structure to resolve, modified in place
209
+ """
210
+ if isinstance(arg, dict):
211
+ for k, v in arg.items():
212
+ if is_constant_reference(v):
213
+ arg[k] = fetch_constant(v)
214
+ else:
215
+ realize_constants(v)
216
+ elif isinstance(arg, list):
217
+ for i, item in enumerate(arg):
218
+ if is_constant_reference(item):
219
+ arg[i] = fetch_constant(item)
220
+ else:
221
+ realize_constants(item)
222
+
223
+
224
+ def is_media_reference(value):
225
+ """Whether a value is an explicit media reference.
226
+
227
+ The form { "media_type": "image", "location": "subject.png" } says what the
228
+ media is instead of relying on what its argument is called, so a "mask" or
229
+ "depth_map" argument can load a file too. A bare {"location": ...} dict is
230
+ NOT treated as one - it stays whatever its consumer expects.
231
+ """
232
+ return isinstance(value, dict) and "media_type" in value and "location" in value
233
+
234
+
235
+ def fetch_media(spec, base_dir=None):
236
+ """Load the media an explicit reference names.
237
+
238
+ Args:
239
+ spec: Dict with 'media_type' ('image' or 'video') and 'location'
240
+ base_dir: Directory relative paths are resolved against
241
+
242
+ Returns:
243
+ The loaded media - or the location string unchanged when it is a
244
+ deferred variable/previous_result reference
245
+
246
+ Raises:
247
+ ValueError: If media_type names neither image nor video
248
+ SecurityError: If the location fails validation
249
+ """
250
+ media_type = spec["media_type"]
251
+ location = {"location": spec["location"]}
252
+ if media_type == "image":
253
+ return fetch_image(location, base_dir)
254
+ if media_type == "video":
255
+ return fetch_video(location, base_dir)
256
+ raise ValueError(f"Unknown media_type {media_type!r} - use 'image' or 'video'")
257
+
258
+
259
+ def object_type_key(value, from_key):
260
+ """The '*_type' key of an object description, or None if it is not one.
261
+
262
+ The type to construct is named by a '*_type' key, matching the convention
263
+ realize_args resolves. Without one the dict is not an object description - it
264
+ is left for whatever consumes it, exactly as it was before this feature.
265
+
266
+ Args:
267
+ value: The dict to inspect
268
+ from_key: The key that marked it as an object description, named in errors
269
+
270
+ Returns:
271
+ The name of the key holding the type, or None
272
+
273
+ Raises:
274
+ ValueError: If the dict names more than one type
275
+ """
276
+ type_keys = [k for k in value if k.endswith("_type") and k not in NON_TYPE_KEYS]
277
+ if not type_keys:
278
+ return None
279
+ if len(type_keys) > 1:
280
+ raise ValueError(
281
+ f"'{from_key}' needs exactly one '_type' argument naming the type to "
282
+ f"construct, got {type_keys}"
283
+ )
284
+ return type_keys[0]
285
+
286
+
287
+ def realize_object(value, base_dir=None):
288
+ """Construct an argument that names a type and the media to build it from.
289
+
290
+ Some pipelines take arguments that are objects rather than plain media - MiniMax-H3's
291
+ references, which carry the frame rate or the sample rate of the media they hold.
292
+ Those are written as a type and where the media comes from, either a file:
293
+
294
+ { "reference_type": "...MiniMaxH3ImageReference", "from_file": "subject.png" }
295
+
296
+ built by the type's own from_file(), since only it knows what to bring along with
297
+ the media, or an earlier step of the workflow:
298
+
299
+ { "reference_type": "...MiniMaxH3ImageReference", "from_previous_result": "draw_subject" }
300
+
301
+ which is built by build_objects() instead, once the step it names has run. Only the
302
+ from_file form is constructed here - the other names media that does not exist yet.
303
+ Any other keys are arguments to from_file() where it takes them, and fields set on
304
+ the object it returns where it does not.
305
+
306
+ A type that has no from_file() of its own - LTX-2's conditions, which are plain
307
+ dataclasses holding frames the caller already loaded - is written as the arguments
308
+ to construct it with instead:
309
+
310
+ { "condition_type": "...LTX2VideoCondition",
311
+ "from_arguments": { "frames": { "media_type": "image", "location": "last.png" },
312
+ "index": -1, "strength": 1.0 } }
313
+
314
+ Those arguments are ordinary arguments: the media in them is loaded by the recursion
315
+ that reaches this, and one naming an earlier step ("previous_result:draw_subject")
316
+ waits for build_objects() the same way the from_previous_result form does.
317
+
318
+ Args:
319
+ value: An already realized argument - anything but a dict naming a '_type'
320
+ and a 'from_file' is returned unchanged
321
+ base_dir: Directory a relative 'from_file' path is resolved against
322
+
323
+ Returns:
324
+ The constructed object, or value unchanged
325
+
326
+ Raises:
327
+ ValueError: If the dict names more than one type, a type that cannot be built
328
+ from a file, or a file location that cannot be resolved
329
+ SecurityError: If the file it names fails validation
330
+ """
331
+ if isinstance(value, dict) and FROM_PREVIOUS_RESULT_KEY in value:
332
+ # Validated now and built later - a type that cannot hold the step's output
333
+ # is a workflow error worth raising before any of it runs
334
+ validate_deferred_object(value)
335
+ return value
336
+
337
+ if isinstance(value, dict) and FROM_ARGUMENTS_KEY in value:
338
+ object_type, arguments = validate_constructed_object(value)
339
+ if names_a_previous_result(arguments):
340
+ # One of its arguments is a step's output, which does not exist yet -
341
+ # build_objects constructs it once that step has run
342
+ return value
343
+ return construct_object(object_type, arguments)
344
+
345
+ if not isinstance(value, dict) or FROM_FILE_KEY not in value:
346
+ return value
347
+
348
+ type_key = object_type_key(value, FROM_FILE_KEY)
349
+ if type_key is None:
350
+ return value
351
+ type_keys = [type_key]
352
+
353
+ object_type = value[type_keys[0]]
354
+ if not isinstance(object_type, type):
355
+ raise ValueError(
356
+ f"'{type_keys[0]}' must name a type to construct, got {object_type!r}"
357
+ )
358
+ if not has_method(object_type, FROM_FILE_KEY):
359
+ raise ValueError(
360
+ f"{object_type.__name__} cannot be constructed from a file - "
361
+ f"it has no {FROM_FILE_KEY}()"
362
+ )
363
+
364
+ location = value[FROM_FILE_KEY]
365
+ if isinstance(location, str):
366
+ # These resolve per step iteration, after objects are already built -
367
+ # a clear error here beats a path-validation failure naming the wrong cause
368
+ if location.startswith("previous_result:"):
369
+ raise ValueError(
370
+ f"'{FROM_FILE_KEY}' cannot reference a previous step's result - "
371
+ f"it names a file the object is constructed from. Use "
372
+ f"'{FROM_PREVIOUS_RESULT_KEY}' to build it from what a step "
373
+ f"generated instead"
374
+ )
375
+ if location.startswith("variable:"):
376
+ raise ValueError(
377
+ f"'{FROM_FILE_KEY}' references {location!r} but no such "
378
+ f"variable is defined"
379
+ )
380
+
381
+ location = validate_media_location(location, base_dir)
382
+ logger.info(f"Constructing {object_type.__name__} from {location}")
383
+
384
+ arguments = {
385
+ k: v for k, v in value.items() if k not in (FROM_FILE_KEY, type_keys[0])
386
+ }
387
+ accepted, overrides = split_from_file_arguments(object_type, arguments)
388
+ return apply_field_overrides(
389
+ object_type.from_file(location, **accepted), overrides, object_type
390
+ )
391
+
392
+
393
+ def split_from_file_arguments(object_type, arguments):
394
+ """Split the keys of a from_file description by where the type can take them.
395
+
396
+ A from_file() that takes keyword arguments is handed all of them - that is the
397
+ generic case, where the type decodes the media and the arguments say how. One that
398
+ takes the media and nothing else, which is what MiniMax-H3's references do, gets
399
+ only what its signature names, and the rest are set on the object it returns. That
400
+ is the way diffusers documents correcting a reference whose container lied about
401
+ its frame rate, and without it there is no way to say so from a workflow.
402
+
403
+ Args:
404
+ object_type: The type being constructed
405
+ arguments: The description's keys, minus the type and the location
406
+
407
+ Returns:
408
+ (arguments for from_file, fields to set on the result)
409
+ """
410
+ parameters = signature(getattr(object_type, FROM_FILE_KEY)).parameters
411
+ if any(
412
+ parameter.kind is Parameter.VAR_KEYWORD for parameter in parameters.values()
413
+ ):
414
+ return arguments, {}
415
+
416
+ named = {
417
+ name
418
+ for name, parameter in parameters.items()
419
+ if parameter.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
420
+ }
421
+ accepted = {k: v for k, v in arguments.items() if k in named}
422
+ return accepted, {k: v for k, v in arguments.items() if k not in named}
423
+
424
+
425
+ def apply_field_overrides(constructed, overrides, object_type):
426
+ """Set the fields a description names on the object that was built from its media.
427
+
428
+ Args:
429
+ constructed: The object from_file() returned
430
+ overrides: The keys from_file() could not take
431
+ object_type: The type, named in errors
432
+
433
+ Returns:
434
+ The object, with the fields set
435
+
436
+ Raises:
437
+ ValueError: If it has no field by one of those names
438
+ """
439
+ for name, value in overrides.items():
440
+ if not hasattr(constructed, name):
441
+ fields = getattr(object_type, "__dataclass_fields__", None)
442
+ takes = f" - it holds {', '.join(fields)}" if fields else ""
443
+ raise ValueError(
444
+ f"{object_type.__name__} has no field '{name}', and its "
445
+ f"{FROM_FILE_KEY}() does not take it either{takes}"
446
+ )
447
+ logger.debug(f"Setting {object_type.__name__}.{name} from the workflow")
448
+ setattr(constructed, name, value)
449
+
450
+ return constructed
451
+
452
+
453
+ def validate_deferred_object(description):
454
+ """Check an object description built from a previous step, at load time.
455
+
456
+ The media it names does not exist until the step it references has run, so the
457
+ construction itself waits. What can be checked now is checked now: a workflow
458
+ that names a type it cannot build should say so before the first model loads.
459
+
460
+ Args:
461
+ description: The dict naming a '_type' and a 'from_previous_result'
462
+
463
+ Raises:
464
+ ValueError: If it names no type, more than one, something that is not a type,
465
+ or a type that declares no media kind
466
+ """
467
+ type_key = object_type_key(description, FROM_PREVIOUS_RESULT_KEY)
468
+ if type_key is None:
469
+ raise ValueError(
470
+ f"'{FROM_PREVIOUS_RESULT_KEY}' needs a '_type' argument naming the type "
471
+ f"to construct from the step's output"
472
+ )
473
+
474
+ object_type = description[type_key]
475
+ if not isinstance(object_type, type):
476
+ raise ValueError(
477
+ f"'{type_key}' must name a type to construct, got {object_type!r}"
478
+ )
479
+
480
+ kind = getattr(object_type, "kind", None)
481
+ if kind not in MEDIA_KINDS:
482
+ raise ValueError(
483
+ f"{object_type.__name__} cannot be constructed from a step's output - "
484
+ f"a type built this way declares which media it holds with a 'kind' of "
485
+ f"{', '.join(MEDIA_KINDS)}, and this one declares {kind!r}"
486
+ )
487
+
488
+
489
+ def validate_constructed_object(description):
490
+ """Check an object description built from the arguments it names, at load time.
491
+
492
+ Args:
493
+ description: The dict naming a '_type' and a 'from_arguments'
494
+
495
+ Returns:
496
+ (the type to construct, the arguments to construct it with)
497
+
498
+ Raises:
499
+ ValueError: If it names no type, more than one, something that is not a type,
500
+ carries keys beside the two, or arguments that are not a dict
501
+ """
502
+ type_key = object_type_key(description, FROM_ARGUMENTS_KEY)
503
+ if type_key is None:
504
+ raise ValueError(
505
+ f"'{FROM_ARGUMENTS_KEY}' needs a '_type' argument naming the type to "
506
+ f"construct from the arguments it holds"
507
+ )
508
+
509
+ object_type = description[type_key]
510
+ if not isinstance(object_type, type):
511
+ raise ValueError(
512
+ f"'{type_key}' must name a type to construct, got {object_type!r}"
513
+ )
514
+
515
+ arguments = description[FROM_ARGUMENTS_KEY]
516
+ if not isinstance(arguments, dict):
517
+ raise ValueError(
518
+ f"'{FROM_ARGUMENTS_KEY}' must hold the arguments {object_type.__name__} "
519
+ f"is constructed with, got {type(arguments).__name__}"
520
+ )
521
+
522
+ extra = set(description) - {type_key, FROM_ARGUMENTS_KEY}
523
+ if extra:
524
+ raise ValueError(
525
+ f"'{FROM_ARGUMENTS_KEY}' holds every argument {object_type.__name__} is "
526
+ f"constructed with - move {', '.join(sorted(extra))} inside it"
527
+ )
528
+
529
+ return object_type, arguments
530
+
531
+
532
+ def names_a_previous_result(value):
533
+ """Whether anything in an argument structure references an earlier step."""
534
+ if isinstance(value, dict):
535
+ return any(names_a_previous_result(item) for item in value.values())
536
+ if isinstance(value, list):
537
+ return any(names_a_previous_result(item) for item in value)
538
+ return isinstance(value, str) and value.startswith(PREVIOUS_RESULT_PREFIX)
539
+
540
+
541
+ def construct_object(object_type, arguments):
542
+ """Build one object from the arguments its description named.
543
+
544
+ Args:
545
+ object_type: The type to construct
546
+ arguments: The arguments to construct it with, with any reference to an
547
+ earlier step already substituted for what it named
548
+
549
+ Returns:
550
+ The constructed object
551
+
552
+ Raises:
553
+ ValueError: If the type cannot be constructed from those arguments
554
+ """
555
+ logger.info(f"Constructing {object_type.__name__} from {', '.join(arguments)}")
556
+ try:
557
+ return object_type(**arguments)
558
+ except TypeError as error:
559
+ fields = getattr(object_type, "__dataclass_fields__", None)
560
+ takes = f" - it takes {', '.join(fields)}" if fields else ""
561
+ raise ValueError(
562
+ f"Cannot construct {object_type.__name__} from "
563
+ f"{', '.join(arguments) or 'no arguments'}{takes}: {error}"
564
+ ) from error
565
+
566
+
567
+ def build_objects(arguments):
568
+ """Construct the objects whose media came from an earlier step.
569
+
570
+ Runs after previous_results has substituted each 'from_previous_result' with the
571
+ artifact it named, which is the earliest the media exists. The same goes for a
572
+ 'from_arguments' description one of whose arguments named a step - realize_object
573
+ left it standing, and by now the reference inside it holds what the step produced.
574
+ Containers are rebuilt rather than mutated, and only where something below them
575
+ changed - the arguments of one iteration share their nested values with every
576
+ other iteration, so an in-place edit here would reach into all of them.
577
+
578
+ Args:
579
+ arguments: One iteration's arguments, with previous results substituted
580
+
581
+ Returns:
582
+ The arguments with every object description replaced by the built object -
583
+ the same object where there was nothing to build
584
+ """
585
+ if isinstance(arguments, dict):
586
+ if FROM_PREVIOUS_RESULT_KEY in arguments:
587
+ return object_from_result(arguments)
588
+ if FROM_ARGUMENTS_KEY in arguments:
589
+ # Its own arguments may hold descriptions too - a condition built from a
590
+ # reference built from a step - so they are built before it is
591
+ object_type, described = validate_constructed_object(arguments)
592
+ return construct_object(object_type, build_objects(described))
593
+ built = {k: build_objects(v) for k, v in arguments.items()}
594
+ return (
595
+ built if any(built[k] is not v for k, v in arguments.items()) else arguments
596
+ )
597
+
598
+ if isinstance(arguments, list):
599
+ built = [build_objects(item) for item in arguments]
600
+ return (
601
+ built
602
+ if any(new is not old for new, old in zip(built, arguments))
603
+ else arguments
604
+ )
605
+
606
+ return arguments
607
+
608
+
609
+ def object_from_result(description):
610
+ """Build one object from the step output substituted into its description.
611
+
612
+ The media is already in memory and already at the rates the step produced it at,
613
+ so it is handed to the constructor field by field rather than through from_file().
614
+ Which field it lands in comes from the type's own 'kind' - the convention
615
+ MiniMax-H3's reference classes follow, and the same one the segment chain reads
616
+ when it carries a generated segment back in as a reference.
617
+
618
+ Args:
619
+ description: The dict naming a '_type', with 'from_previous_result' now
620
+ holding the artifact rather than the step name
621
+
622
+ Returns:
623
+ The constructed object
624
+
625
+ Raises:
626
+ ValueError: If the artifact is not the media the type's kind calls for
627
+ """
628
+ validate_deferred_object(description)
629
+ type_key = object_type_key(description, FROM_PREVIOUS_RESULT_KEY)
630
+ object_type = description[type_key]
631
+ artifact = description[FROM_PREVIOUS_RESULT_KEY]
632
+
633
+ # A workflow can still name a field itself - the frame rate of a step that
634
+ # generated at something other than the consuming pipeline's own, say - and
635
+ # what it names wins over what the artifact carried
636
+ overrides = {
637
+ k: v
638
+ for k, v in description.items()
639
+ if k not in (FROM_PREVIOUS_RESULT_KEY, type_key)
640
+ }
641
+ arguments = media_arguments(object_type, artifact)
642
+ arguments.update(overrides)
643
+
644
+ logger.info(
645
+ f"Constructing {object_type.__name__} from a previous result "
646
+ f"({', '.join(arguments)})"
647
+ )
648
+ return object_type(**arguments)
649
+
650
+
651
+ def media_arguments(object_type, artifact):
652
+ """The constructor arguments a step's artifact makes, for a type's media kind.
653
+
654
+ Imported here rather than at module scope - these pull in the result and task
655
+ helpers, which is a heavier import than an argument file needs for the path
656
+ that never builds one of these.
657
+
658
+ Args:
659
+ object_type: The type being constructed, declaring its 'kind'
660
+ artifact: One artifact of the step the description named
661
+
662
+ Returns:
663
+ Dict of constructor arguments
664
+
665
+ Raises:
666
+ ValueError: If the artifact is not the media the kind calls for
667
+ """
668
+ import torch
669
+ from PIL import Image
670
+
671
+ from .result import AudioVideo
672
+ from .tasks.audio_utils import as_channels_samples
673
+ from .tasks.video_utils import frames_as_pil_list
674
+
675
+ kind = object_type.kind
676
+
677
+ if kind == "image":
678
+ if not isinstance(artifact, Image.Image):
679
+ raise ValueError(
680
+ f"{object_type.__name__} holds an image, but the step it names "
681
+ f"produced a {type(artifact).__name__} - reference a step that "
682
+ f"generates images, or its 'images' property"
683
+ )
684
+ return {"image": artifact}
685
+
686
+ # A generated soundtrack arrives paired with the frames it was generated with;
687
+ # anything else the step produced carries none
688
+ audio, sample_rate = None, None
689
+ if isinstance(artifact, AudioVideo) and artifact.audio is not None:
690
+ audio = torch.as_tensor(as_channels_samples(artifact.audio))
691
+ sample_rate = artifact.sample_rate
692
+
693
+ if kind == "video":
694
+ frames = frames_as_pil_list(artifact)
695
+ if not frames:
696
+ raise ValueError(
697
+ f"{object_type.__name__} holds a video, but the step it names "
698
+ f"produced no frames"
699
+ )
700
+ arguments = {"frames": frames}
701
+ if audio is not None:
702
+ arguments["audio"] = audio
703
+ arguments["sample_rate"] = sample_rate
704
+ return arguments
705
+
706
+ if audio is None:
707
+ raise ValueError(
708
+ f"{object_type.__name__} holds audio, but the step it names produced "
709
+ f"none - reference a step that generates a soundtrack"
710
+ )
711
+ return {"audio": audio, "sample_rate": sample_rate}
712
+
713
+
714
+ def validate_media_location(location, base_dir=None):
715
+ """Validate the media file an argument object is constructed from.
716
+
717
+ The object decodes the file itself, so only where it comes from is checked here.
718
+
719
+ Args:
720
+ location: Path or URL of the media file
721
+ base_dir: Directory a relative path is resolved against - the workflow
722
+ file's directory. Defaults to the process working directory
723
+
724
+ Returns:
725
+ The validated path or URL
726
+
727
+ Raises:
728
+ ValueError: If the location is not a string
729
+ SecurityError: If the path, URL or file extension is not allowed
730
+ """
731
+ if not isinstance(location, str):
732
+ raise ValueError(
733
+ f"'{FROM_FILE_KEY}' must be a path or a URL, got {type(location)}"
734
+ )
735
+
736
+ if location.startswith("http://") or location.startswith("https://"):
737
+ return validate_url(location)
738
+
739
+ validated_path = validate_path(
740
+ resolve_relative_path(location, base_dir), allow_create=False
741
+ )
742
+ return validate_file_extension(validated_path, ALLOWED_FROM_FILE_EXTENSIONS)
743
+
744
+
745
+ def resolve_relative_path(path, base_dir):
746
+ """Resolve a relative file path against the workflow file's directory.
747
+
748
+ Workflow files name their media relative to themselves; absolute paths and
749
+ callers with no base_dir keep the path as given (process working directory).
750
+ """
751
+ if base_dir and not os.path.isabs(os.path.expanduser(path)):
752
+ return os.path.join(base_dir, path)
753
+ return path
754
+
755
+
756
+ def fetch_image(img_spec, base_dir=None):
757
+ """
758
+ Load image from file path or URL with security validation.
759
+
760
+ Args:
761
+ img_spec: Image specification (file path, URL, dict with 'location' key, PIL Image, or list of any of these)
762
+ base_dir: Directory relative file paths are resolved against - the
763
+ workflow file's directory. Defaults to the process working directory
764
+
765
+ Returns:
766
+ Loaded PIL Image, list of PIL Images, or None if img_spec is None
767
+
768
+ Raises:
769
+ SecurityError: If validation fails
770
+ ValueError: If img_spec is invalid type
771
+ """
772
+ if img_spec is None:
773
+ return None
774
+
775
+ # Handle lists of images (recursively process each)
776
+ if isinstance(img_spec, list):
777
+ logger.debug(f"Loading list of {len(img_spec)} images")
778
+ return [fetch_image(img, base_dir) for img in img_spec]
779
+
780
+ # If already a PIL Image, return as-is (allows multiple realize_args calls)
781
+ if hasattr(img_spec, "mode") and hasattr(img_spec, "size"):
782
+ logger.debug(f"Image already loaded, returning as-is")
783
+ return img_spec
784
+
785
+ # Handle dict format: {"location": "url_or_path"}
786
+ if isinstance(img_spec, dict):
787
+ if "location" not in img_spec:
788
+ raise ValueError(
789
+ f"Image dict must have 'location' key, got keys: {list(img_spec.keys())}"
790
+ )
791
+ img_spec = img_spec["location"]
792
+
793
+ if not isinstance(img_spec, str):
794
+ raise ValueError(f"Image specification must be a string, got {type(img_spec)}")
795
+
796
+ # Skip cross-step and variable references — these are resolved later during execution
797
+ if img_spec.startswith("previous_result:") or img_spec.startswith("variable:"):
798
+ logger.debug(f"Skipping deferred reference: {img_spec}")
799
+ return img_spec
800
+
801
+ logger.debug(f"Loading image from: {img_spec}")
802
+
803
+ try:
804
+ # Check if it's a URL
805
+ if isinstance(img_spec, str) and (
806
+ img_spec.startswith("http://") or img_spec.startswith("https://")
807
+ ):
808
+ validated_url = validate_url(img_spec)
809
+ return load_image(validated_url)
810
+ else:
811
+ # Treat as file path, relative to the workflow file
812
+ validated_path = validate_path(
813
+ resolve_relative_path(str(img_spec), base_dir), allow_create=False
814
+ )
815
+ # Validate file extension
816
+ ext = os.path.splitext(validated_path)[1].lower()
817
+ if ext not in ALLOWED_IMAGE_EXTENSIONS:
818
+ raise SecurityError(f"Image file extension not allowed: {ext}")
819
+ return load_image(validated_path)
820
+
821
+ except SecurityError:
822
+ raise
823
+ except Exception as e:
824
+ logger.error(f"Failed to load image {img_spec}: {e}")
825
+ raise
826
+
827
+
828
+ def fetch_video(video_spec, base_dir=None):
829
+ """
830
+ Load video from file path or URL with security validation.
831
+
832
+ Args:
833
+ video_spec: Video specification (file path, URL, dict with 'location' key, loaded frames, or list of any of these)
834
+ base_dir: Directory relative file paths are resolved against - the
835
+ workflow file's directory. Defaults to the process working directory
836
+
837
+ Returns:
838
+ Loaded video frames, list of video frames, or None if video_spec is None
839
+
840
+ Raises:
841
+ SecurityError: If validation fails
842
+ ValueError: If video_spec is invalid type
843
+ """
844
+ if video_spec is None:
845
+ return None
846
+
847
+ # Handle lists of videos (need to distinguish from video frames)
848
+ # Check if it's a list of specifications (dicts/strings) rather than video frames
849
+ if isinstance(video_spec, list) and len(video_spec) > 0:
850
+ # If first element is a dict with 'location' or a string, treat as list of video specs
851
+ if isinstance(video_spec[0], (dict, str)):
852
+ logger.debug(f"Loading list of {len(video_spec)} videos")
853
+ return [fetch_video(vid, base_dir) for vid in video_spec]
854
+ # Otherwise assume it's already loaded video frames
855
+ else:
856
+ logger.debug(f"Video frames already loaded, returning as-is")
857
+ return video_spec
858
+
859
+ # If already loaded video frames (tuple), return as-is
860
+ if isinstance(video_spec, tuple):
861
+ logger.debug(f"Video frames already loaded, returning as-is")
862
+ return video_spec
863
+
864
+ # Handle dict format: {"location": "url_or_path"}
865
+ if isinstance(video_spec, dict):
866
+ if "location" not in video_spec:
867
+ raise ValueError(
868
+ f"Video dict must have 'location' key, got keys: {list(video_spec.keys())}"
869
+ )
870
+ video_spec = video_spec["location"]
871
+
872
+ if not isinstance(video_spec, str):
873
+ raise ValueError(
874
+ f"Video specification must be a string, got {type(video_spec)}"
875
+ )
876
+
877
+ # Skip cross-step and variable references — these are resolved later during execution
878
+ if video_spec.startswith("previous_result:") or video_spec.startswith("variable:"):
879
+ logger.debug(f"Skipping deferred reference: {video_spec}")
880
+ return video_spec
881
+
882
+ logger.debug(f"Loading video from: {video_spec}")
883
+
884
+ try:
885
+ # Check if it's a URL
886
+ if isinstance(video_spec, str) and (
887
+ video_spec.startswith("http://") or video_spec.startswith("https://")
888
+ ):
889
+ validated_url = validate_url(video_spec)
890
+ return load_video(validated_url)
891
+ else:
892
+ # Treat as file path, relative to the workflow file
893
+ validated_path = validate_path(
894
+ resolve_relative_path(str(video_spec), base_dir), allow_create=False
895
+ )
896
+ # Validate file extension
897
+ ext = os.path.splitext(validated_path)[1].lower()
898
+ if ext not in ALLOWED_VIDEO_EXTENSIONS:
899
+ raise SecurityError(f"Video file extension not allowed: {ext}")
900
+ return load_video(validated_path)
901
+
902
+ except SecurityError:
903
+ raise
904
+ except Exception as e:
905
+ logger.error(f"Failed to load video {video_spec}: {e}")
906
+ raise