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.
- diffusers_workflow-0.4.0a3.dist-info/METADATA +310 -0
- diffusers_workflow-0.4.0a3.dist-info/RECORD +171 -0
- diffusers_workflow-0.4.0a3.dist-info/WHEEL +5 -0
- diffusers_workflow-0.4.0a3.dist-info/entry_points.txt +6 -0
- diffusers_workflow-0.4.0a3.dist-info/licenses/LICENSE +201 -0
- diffusers_workflow-0.4.0a3.dist-info/top_level.txt +1 -0
- dw/__init__.py +353 -0
- dw/arguments.py +906 -0
- dw/cache_blocks.json +16 -0
- dw/cache_blocks.py +145 -0
- dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
- dw/events.py +78 -0
- dw/hub_cache.py +289 -0
- dw/introspection.py +458 -0
- dw/log_setup.py +45 -0
- dw/pipeline_processors/chain.py +750 -0
- dw/pipeline_processors/config_objects.py +235 -0
- dw/pipeline_processors/pipeline.py +1687 -0
- dw/pipeline_processors/remote.py +18 -0
- dw/previous_results.py +259 -0
- dw/prompt_weighting.py +378 -0
- dw/repl.py +298 -0
- dw/repl_commands.py +808 -0
- dw/repl_worker.py +129 -0
- dw/result.py +850 -0
- dw/run.py +92 -0
- dw/schema.py +24 -0
- dw/security.py +379 -0
- dw/serve.py +70 -0
- dw/server/__init__.py +2 -0
- dw/server/app.py +588 -0
- dw/server/jobs.py +547 -0
- dw/server/ui/assets/abap-08VXUWAP.js +1 -0
- dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
- dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
- dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
- dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
- dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
- dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
- dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
- dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
- dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
- dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
- dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
- dw/server/ui/assets/css-DIMkf-bt.js +3 -0
- dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
- dw/server/ui/assets/cssMode-CEh6hWi2.js +1 -0
- dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
- dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
- dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
- dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
- dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
- dw/server/ui/assets/editor.api-CExg3_mM.js +847 -0
- dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
- dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
- dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
- dw/server/ui/assets/freemarker2-DH6orYh2.js +3 -0
- dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
- dw/server/ui/assets/go-C-y9NEjX.js +1 -0
- dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
- dw/server/ui/assets/handlebars-CbrMVW4Q.js +1 -0
- dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
- dw/server/ui/assets/html-YDNPZw2M.js +1 -0
- dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
- dw/server/ui/assets/htmlMode-B_zSGWO2.js +1 -0
- dw/server/ui/assets/index-B7-VcYS-.css +1 -0
- dw/server/ui/assets/index-D_EiPU3b.js +13 -0
- dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
- dw/server/ui/assets/java-BEtHBSE6.js +1 -0
- dw/server/ui/assets/javascript-dYuBvioq.js +1 -0
- dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
- dw/server/ui/assets/jsonMode-CUqLM39V.js +7 -0
- dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
- dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
- dw/server/ui/assets/less-B9JPFI3C.js +2 -0
- dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
- dw/server/ui/assets/liquid-D6vxBzMv.js +1 -0
- dw/server/ui/assets/lspLanguageFeatures-1WJ2palX.js +4 -0
- dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
- dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
- dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
- dw/server/ui/assets/mdx-SHQb6vmD.js +1 -0
- dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
- dw/server/ui/assets/monaco--ixms01u.css +1 -0
- dw/server/ui/assets/monaco-CP-s5rcP.js +56 -0
- dw/server/ui/assets/msdax-DauUninz.js +1 -0
- dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
- dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
- dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
- dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
- dw/server/ui/assets/perl-oz_6vUea.js +1 -0
- dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
- dw/server/ui/assets/php-nr791fC2.js +1 -0
- dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
- dw/server/ui/assets/postiats-43DmfD33.js +1 -0
- dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
- dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
- dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
- dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
- dw/server/ui/assets/python-x0_EGHq9.js +1 -0
- dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
- dw/server/ui/assets/r-BwWrilGY.js +1 -0
- dw/server/ui/assets/razor-BZC4LQDP.js +1 -0
- dw/server/ui/assets/redis-ClamHrr6.js +1 -0
- dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
- dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
- dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
- dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
- dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
- dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
- dw/server/ui/assets/scheme-BeGwcela.js +1 -0
- dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
- dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
- dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
- dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
- dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
- dw/server/ui/assets/sql-NEE52Syq.js +1 -0
- dw/server/ui/assets/st-DbInun42.js +1 -0
- dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
- dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
- dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
- dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
- dw/server/ui/assets/tsMode-BTfA6SbD.js +11 -0
- dw/server/ui/assets/twig-De2hgUGE.js +1 -0
- dw/server/ui/assets/typescript-CWA4MsNk.js +1 -0
- dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
- dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
- dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
- dw/server/ui/assets/workers-CWU0uvj5.js +1 -0
- dw/server/ui/assets/xml-KmfTm3rg.js +1 -0
- dw/server/ui/assets/yaml-nFO_dDS6.js +1 -0
- dw/server/ui/index.html +17 -0
- dw/settings.py +77 -0
- dw/step.py +132 -0
- dw/tasks/audio_utils.py +266 -0
- dw/tasks/background_remover.py +43 -0
- dw/tasks/borders.py +113 -0
- dw/tasks/concat_videos.py +80 -0
- dw/tasks/depth_estimator.py +54 -0
- dw/tasks/diffusion_upscale.py +109 -0
- dw/tasks/format_messages.py +24 -0
- dw/tasks/gather.py +139 -0
- dw/tasks/image_to_text.py +43 -0
- dw/tasks/image_utils.py +661 -0
- dw/tasks/interpolate_frames.py +227 -0
- dw/tasks/model_cache.py +39 -0
- dw/tasks/pair_audio.py +58 -0
- dw/tasks/qr_code.py +19 -0
- dw/tasks/restore_faces.py +175 -0
- dw/tasks/rife_model.py +192 -0
- dw/tasks/segment.py +121 -0
- dw/tasks/task.py +474 -0
- dw/tasks/tensor_image.py +57 -0
- dw/tasks/text_generation.py +168 -0
- dw/tasks/text_sections.py +80 -0
- dw/tasks/upscale.py +203 -0
- dw/tasks/video_utils.py +154 -0
- dw/tasks/zoe_depth.py +71 -0
- dw/teacache.py +376 -0
- dw/teacache_models.json +99 -0
- dw/test.py +29 -0
- dw/type_helpers.py +68 -0
- dw/validate.py +43 -0
- dw/variables.py +153 -0
- dw/worker.py +517 -0
- dw/workflow.py +553 -0
- dw/workflow_schema.json +1157 -0
- dw/workflows/augment_prompt.json +65 -0
- dw/workflows/describe_image.json +58 -0
- dw/workflows/h3_context_ir.json +57 -0
- 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
|