diffusers-workflow 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diffusers_workflow-0.4.0.dist-info/METADATA +318 -0
- diffusers_workflow-0.4.0.dist-info/RECORD +260 -0
- diffusers_workflow-0.4.0.dist-info/WHEEL +5 -0
- diffusers_workflow-0.4.0.dist-info/entry_points.txt +7 -0
- diffusers_workflow-0.4.0.dist-info/licenses/LICENSE +201 -0
- diffusers_workflow-0.4.0.dist-info/top_level.txt +2 -0
- dw/__init__.py +440 -0
- dw/adapter_compatibility.py +226 -0
- dw/arguments.py +1231 -0
- dw/assessment_rules.py +159 -0
- dw/assets.py +130 -0
- dw/cache_blocks.json +16 -0
- dw/cache_blocks.py +146 -0
- dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
- dw/content_types.py +150 -0
- dw/dissolve_frame_errors.py +121 -0
- dw/docs/ACCELERATION.md +352 -0
- dw/docs/AGENT_LOOP.md +95 -0
- dw/docs/DEPENDENCIES.md +91 -0
- dw/docs/IP_ADAPTER.md +109 -0
- dw/docs/LORAS.md +131 -0
- dw/docs/MCP.md +517 -0
- dw/docs/PROMPT_WEIGHTING.md +78 -0
- dw/docs/QUANTIZATION.md +230 -0
- dw/docs/RECIPES_24GB.md +201 -0
- dw/docs/RELEASING.md +195 -0
- dw/docs/REMOTE.md +140 -0
- dw/docs/REPL_COMMANDS.md +121 -0
- dw/docs/REPL_WORKER_GUIDE.md +51 -0
- dw/docs/SECURITY.md +272 -0
- dw/docs/SECURITY_QUICKREF.md +112 -0
- dw/docs/SERVER.md +679 -0
- dw/docs/TASKS.md +1741 -0
- dw/docs/TESTING.md +71 -0
- dw/docs/WORKFLOW_GUIDE.md +2038 -0
- dw/docs/WORKSPACES.md +316 -0
- dw/download_watch.py +335 -0
- dw/elision.py +306 -0
- dw/events.py +275 -0
- dw/for_each.py +409 -0
- dw/host_memory.py +258 -0
- dw/host_memory_projection.py +230 -0
- dw/hub_cache.py +432 -0
- dw/introspection.py +1228 -0
- dw/kernel_availability.py +208 -0
- dw/locations.py +599 -0
- dw/log_setup.py +45 -0
- dw/loudness.py +82 -0
- dw/media_audio.py +217 -0
- dw/media_frames.py +367 -0
- dw/media_info.py +297 -0
- dw/pipeline_processors/chain.py +821 -0
- dw/pipeline_processors/config_objects.py +237 -0
- dw/pipeline_processors/pipeline.py +2297 -0
- dw/pipeline_processors/remote.py +46 -0
- dw/plan.py +920 -0
- dw/previous_results.py +411 -0
- dw/probe_paths.py +59 -0
- dw/prompt_schema.json +48 -0
- dw/prompt_weighting.py +378 -0
- dw/prompts.py +159 -0
- dw/realize.py +250 -0
- dw/reference_limits.py +215 -0
- dw/reference_names.py +125 -0
- dw/repl.py +338 -0
- dw/repl_commands.py +836 -0
- dw/repl_worker.py +159 -0
- dw/result.py +1720 -0
- dw/result_fps.py +82 -0
- dw/run.py +162 -0
- dw/runs.py +768 -0
- dw/scalar_result_validation.py +97 -0
- dw/schema.py +283 -0
- dw/security.py +1038 -0
- dw/select_validation.py +115 -0
- dw/serve.py +277 -0
- dw/server/__init__.py +2 -0
- dw/server/app.py +4586 -0
- dw/server/assess.py +132 -0
- dw/server/catalog_shape.py +487 -0
- dw/server/enhancers.py +129 -0
- dw/server/exports.py +480 -0
- dw/server/guides.py +257 -0
- dw/server/jobs.py +1561 -0
- dw/server/mcp_mount.py +95 -0
- dw/server/netinfo.py +124 -0
- dw/server/observed_cost.py +379 -0
- dw/server/sysinfo.py +71 -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-CPznxfY8.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-CpWcotrd.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-CXtRM8N4.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-N7x-6NMY.js +1 -0
- dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
- dw/server/ui/assets/html-PhsdjHSr.js +1 -0
- dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
- dw/server/ui/assets/htmlMode-Dgj0SEok.js +1 -0
- dw/server/ui/assets/index-3Vw6WAPW.css +1 -0
- dw/server/ui/assets/index-DgrYhQd9.js +43 -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-BJqN9Qhv.js +1 -0
- dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
- dw/server/ui/assets/jsonMode-DbM4SWSv.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-BWr8lEc4.js +1 -0
- dw/server/ui/assets/lspLanguageFeatures-C1iGuDyZ.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-DAdMi_0p.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-BGCeEqaw.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-Bcn70HdC.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-D1HmNnby.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-D6u0XmOW.js +11 -0
- dw/server/ui/assets/twig-De2hgUGE.js +1 -0
- dw/server/ui/assets/typescript-BU6v-LMV.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-Cn7cTUKr.js +1 -0
- dw/server/ui/assets/xml--0LP2Lwk.js +1 -0
- dw/server/ui/assets/yaml-mpBg9jnt.js +1 -0
- dw/server/ui/index.html +17 -0
- dw/server/updater.py +192 -0
- dw/settings.py +98 -0
- dw/shot_span_preflight.py +116 -0
- dw/shots.py +359 -0
- dw/slice_preflight.py +148 -0
- dw/step.py +187 -0
- dw/step_cache.py +442 -0
- dw/subfolders.py +107 -0
- dw/task_domains.py +307 -0
- dw/tasks/assess.py +826 -0
- dw/tasks/audio_transcription.py +88 -0
- dw/tasks/audio_utils.py +1862 -0
- dw/tasks/background_remover.py +43 -0
- dw/tasks/borders.py +113 -0
- dw/tasks/compose_text.py +74 -0
- dw/tasks/concat_videos.py +300 -0
- dw/tasks/depth_estimator.py +54 -0
- dw/tasks/diffusion_upscale.py +109 -0
- dw/tasks/dissolve_videos.py +342 -0
- dw/tasks/format_messages.py +24 -0
- dw/tasks/gather.py +173 -0
- dw/tasks/grade.py +97 -0
- dw/tasks/image_to_text.py +43 -0
- dw/tasks/image_utils.py +764 -0
- dw/tasks/interpolate_frames.py +252 -0
- dw/tasks/judge.py +68 -0
- dw/tasks/model_cache.py +55 -0
- dw/tasks/pair_audio.py +268 -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/select.py +111 -0
- dw/tasks/speech_generation.py +228 -0
- dw/tasks/stabilize.py +129 -0
- dw/tasks/task.py +920 -0
- dw/tasks/tensor_image.py +57 -0
- dw/tasks/text_generation.py +169 -0
- dw/tasks/text_sections.py +80 -0
- dw/tasks/upscale.py +203 -0
- dw/tasks/video_utils.py +624 -0
- dw/tasks/zoe_depth.py +71 -0
- dw/teacache.py +381 -0
- dw/teacache_models.json +99 -0
- dw/test.py +29 -0
- dw/type_helpers.py +231 -0
- dw/validate.py +68 -0
- dw/variable_constraints.py +444 -0
- dw/variables.py +443 -0
- dw/video_extensions.py +141 -0
- dw/vram_estimate.py +116 -0
- dw/worker.py +764 -0
- dw/workflow.py +2007 -0
- dw/workflow_schema.json +1346 -0
- dw/workflow_sources.py +383 -0
- dw/workflows/h3_context_ir.json +57 -0
- dw/workflows/test.json +31 -0
- dw/workspace.py +730 -0
- dw_mcp/__init__.py +6 -0
- dw_mcp/__main__.py +133 -0
- dw_mcp/assets.py +336 -0
- dw_mcp/authoring.py +114 -0
- dw_mcp/catalog.py +360 -0
- dw_mcp/client.py +486 -0
- dw_mcp/diagnose.py +371 -0
- dw_mcp/exports.py +84 -0
- dw_mcp/guides.py +35 -0
- dw_mcp/media.py +638 -0
- dw_mcp/models.py +97 -0
- dw_mcp/prompts.py +104 -0
- dw_mcp/server.py +1343 -0
- dw_mcp/workspaces.py +212 -0
dw/variables.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging
|
|
3
|
+
from .arguments import (
|
|
4
|
+
FROM_ARGUMENTS_KEY,
|
|
5
|
+
FROM_FILE_KEY,
|
|
6
|
+
FROM_PREVIOUS_RESULT_KEY,
|
|
7
|
+
)
|
|
8
|
+
from .security import (
|
|
9
|
+
validate_variable_name,
|
|
10
|
+
validate_string_input,
|
|
11
|
+
SecurityError,
|
|
12
|
+
MAX_VARIABLE_VALUE_LENGTH,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("dw")
|
|
16
|
+
|
|
17
|
+
# The keys an object description names its media with. A "variable:" under one
|
|
18
|
+
# of these that resolves to null has to stay present-and-null for
|
|
19
|
+
# realize_object to read it as an omitted optional reference
|
|
20
|
+
MEDIA_SOURCE_KEYS = (FROM_FILE_KEY, FROM_PREVIOUS_RESULT_KEY, FROM_ARGUMENTS_KEY)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class VariableNotFoundError(ValueError):
|
|
24
|
+
"""Raised when a workflow references a "variable:name" that isn't declared."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class VariableCycleError(ValueError):
|
|
28
|
+
"""Raised when a list- or dict-valued variable references itself, directly
|
|
29
|
+
or through others, inside `resolve_variable_values`."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _resolve_variable_reference(value, variables):
|
|
33
|
+
"""
|
|
34
|
+
If value is a "variable:name" reference, look it up and return (True, resolved).
|
|
35
|
+
Otherwise return (False, None) so the caller knows to recurse instead.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
VariableNotFoundError: if the referenced name isn't in variables, naming the
|
|
39
|
+
variables that are actually available.
|
|
40
|
+
"""
|
|
41
|
+
if isinstance(value, str) and value.startswith("variable:"):
|
|
42
|
+
variable_name = value.removeprefix("variable:")
|
|
43
|
+
logger.debug(f"Replacing variable reference: {variable_name}")
|
|
44
|
+
if variable_name not in variables:
|
|
45
|
+
available = ", ".join(sorted(variables.keys())) or "<none>"
|
|
46
|
+
message = f"Variable <{variable_name}> not found; available variables: {available}"
|
|
47
|
+
raise VariableNotFoundError(message)
|
|
48
|
+
return True, variables[variable_name]
|
|
49
|
+
return False, None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def replace_variables(data, variables):
|
|
53
|
+
"""
|
|
54
|
+
Recursively replaces variable references in data structures with their actual values.
|
|
55
|
+
|
|
56
|
+
Does not mutate its input - a new structure is returned and `data` is left as it
|
|
57
|
+
was passed in, so callers don't have to deep-copy defensively before calling.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
data: The data structure (dict or list) containing variable references
|
|
61
|
+
variables: Dictionary of variable names and their values
|
|
62
|
+
Returns:
|
|
63
|
+
A new structure with "variable:name" references replaced. Any part of `data`
|
|
64
|
+
that isn't a dict/list/reference string is returned as-is.
|
|
65
|
+
"""
|
|
66
|
+
if variables is None:
|
|
67
|
+
return data
|
|
68
|
+
|
|
69
|
+
logger.debug(f"Processing variables: {list(variables.keys())}")
|
|
70
|
+
|
|
71
|
+
# Handle lists - replace any "variable:name" strings with their values
|
|
72
|
+
if isinstance(data, list):
|
|
73
|
+
logger.debug(f"Processing list of length {len(data)}")
|
|
74
|
+
result = []
|
|
75
|
+
for item in data:
|
|
76
|
+
matched, resolved = _resolve_variable_reference(item, variables)
|
|
77
|
+
if matched:
|
|
78
|
+
result.append(resolved)
|
|
79
|
+
else:
|
|
80
|
+
# Recursively process nested structures
|
|
81
|
+
result.append(replace_variables(item, variables))
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
# Handle dictionaries - replace values that are variable references
|
|
85
|
+
if isinstance(data, dict):
|
|
86
|
+
logger.debug(f"Processing dictionary with keys: {list(data.keys())}")
|
|
87
|
+
result = {}
|
|
88
|
+
for k, v in data.items():
|
|
89
|
+
matched, resolved = _resolve_variable_reference(v, variables)
|
|
90
|
+
if matched:
|
|
91
|
+
# A variable: reference that resolves to null is dropped as
|
|
92
|
+
# if the key had never been passed - an optional argument
|
|
93
|
+
# left at its unset default (e.g. select's threshold/index)
|
|
94
|
+
# shouldn't count as "present" just because a variable named
|
|
95
|
+
# it. A literal null written inline in the workflow JSON
|
|
96
|
+
# never goes through this branch, so its meaning elsewhere
|
|
97
|
+
# (a declared-but-unset default) is untouched.
|
|
98
|
+
#
|
|
99
|
+
# Except where the key itself is how an object description
|
|
100
|
+
# names its media: realize_object reads a present-and-null
|
|
101
|
+
# 'from_file'/'from_previous_result'/'from_arguments' as
|
|
102
|
+
# OMITTED - an optional reference this run was given nothing
|
|
103
|
+
# for (_names_no_media in arguments.py). Dropping the key
|
|
104
|
+
# there turns that into a media-less stub that reaches the
|
|
105
|
+
# pipeline instead.
|
|
106
|
+
if resolved is None and k not in MEDIA_SOURCE_KEYS:
|
|
107
|
+
continue
|
|
108
|
+
result[k] = resolved
|
|
109
|
+
else:
|
|
110
|
+
# Recursively process nested structures in dictionary values
|
|
111
|
+
result[k] = replace_variables(v, variables)
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
# Scalars (and anything else) pass through unchanged. copy.deepcopy guards
|
|
115
|
+
# against a caller mutating a returned mutable leaf (e.g. a PIL.Image or a
|
|
116
|
+
# list-typed variable's value) and having that reach back into `variables`.
|
|
117
|
+
return copy.deepcopy(data)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def resolve_variable_values(variables):
|
|
121
|
+
"""A copy of `variables` in which every "variable:name" inside a list-
|
|
122
|
+
or dict-valued variable is replaced by that variable's value.
|
|
123
|
+
|
|
124
|
+
A list-driven step reads its entries from a variable, and an entry that
|
|
125
|
+
says "from_file": "variable:character_a_voice" is how one variable sets
|
|
126
|
+
a voice in every shot the character speaks in. replace_variables only
|
|
127
|
+
walks the definition, so those references would reach the step as the
|
|
128
|
+
literal strings; this resolves them once, before realize_args, so a
|
|
129
|
+
reference type inside an entry is a type name by the time it is loaded.
|
|
130
|
+
|
|
131
|
+
Only list and dict values are walked. A scalar value that begins with
|
|
132
|
+
"variable:" is passed through as it always was.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
VariableNotFoundError: a reference names nothing declared
|
|
136
|
+
VariableCycleError: a value references itself, directly or through others
|
|
137
|
+
"""
|
|
138
|
+
resolved = {}
|
|
139
|
+
|
|
140
|
+
def resolve(name, chain):
|
|
141
|
+
if name in resolved:
|
|
142
|
+
return resolved[name]
|
|
143
|
+
if name in chain:
|
|
144
|
+
loop = " -> ".join(chain[chain.index(name) :] + [name])
|
|
145
|
+
raise VariableCycleError(
|
|
146
|
+
f"Variable '{name}' references itself through: {loop}"
|
|
147
|
+
)
|
|
148
|
+
value = variables[name]
|
|
149
|
+
if isinstance(value, (list, dict)):
|
|
150
|
+
value = walk(value, chain + [name])
|
|
151
|
+
else:
|
|
152
|
+
value = copy.deepcopy(value)
|
|
153
|
+
resolved[name] = value
|
|
154
|
+
return value
|
|
155
|
+
|
|
156
|
+
def walk(node, chain):
|
|
157
|
+
matched, _ = _resolve_variable_reference(node, variables)
|
|
158
|
+
if matched:
|
|
159
|
+
return resolve(node.removeprefix("variable:"), chain)
|
|
160
|
+
if isinstance(node, list):
|
|
161
|
+
return [walk(item, chain) for item in node]
|
|
162
|
+
if isinstance(node, dict):
|
|
163
|
+
return {key: walk(item, chain) for key, item in node.items()}
|
|
164
|
+
return copy.deepcopy(node)
|
|
165
|
+
|
|
166
|
+
for name in variables:
|
|
167
|
+
resolve(name, [])
|
|
168
|
+
return resolved
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def undeclared_variable_references(definition):
|
|
172
|
+
"""The "variable:name" references in a workflow definition that name no
|
|
173
|
+
entry of its `variables` - the ones `replace_variables` will refuse at run
|
|
174
|
+
time, found before anything loads.
|
|
175
|
+
|
|
176
|
+
Walks everything but `variables` itself, plus the inside of every list-
|
|
177
|
+
or dict-valued variable, the way `resolve_variable_values` and
|
|
178
|
+
`replace_variables` together do. Returns a list of (path, name) pairs,
|
|
179
|
+
path being where the reference sits (`steps[0].pipeline.arguments.prompt`)
|
|
180
|
+
and name what it asked for - which is the whole remainder of the string,
|
|
181
|
+
since a reference is the entire value and nothing is interpolated
|
|
182
|
+
around it.
|
|
183
|
+
"""
|
|
184
|
+
declared = definition.get("variables") or {}
|
|
185
|
+
found = []
|
|
186
|
+
|
|
187
|
+
def walk(node, path):
|
|
188
|
+
if isinstance(node, str) and node.startswith("variable:"):
|
|
189
|
+
name = node.removeprefix("variable:")
|
|
190
|
+
if name not in declared:
|
|
191
|
+
found.append((path, name))
|
|
192
|
+
elif isinstance(node, dict):
|
|
193
|
+
for k, v in node.items():
|
|
194
|
+
walk(v, f"{path}.{k}" if path else k)
|
|
195
|
+
elif isinstance(node, list):
|
|
196
|
+
for i, v in enumerate(node):
|
|
197
|
+
walk(v, f"{path}[{i}]")
|
|
198
|
+
|
|
199
|
+
for key, value in definition.items():
|
|
200
|
+
if key != "variables":
|
|
201
|
+
walk(value, key)
|
|
202
|
+
if isinstance(declared, dict):
|
|
203
|
+
for name, value in declared.items():
|
|
204
|
+
if isinstance(value, (list, dict)):
|
|
205
|
+
walk(value, f"variables.{name}")
|
|
206
|
+
return found
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _validated_strings(value):
|
|
210
|
+
"""A copy of a list- or dict-valued caller argument with every string
|
|
211
|
+
leaf passed through `validate_string_input` - the same check a
|
|
212
|
+
top-level string argument gets in `set_variables` below. A for_each
|
|
213
|
+
entry's `prompt` or `from_file` is exactly as reachable to an attacker
|
|
214
|
+
as a top-level variable, and `isinstance(v, str)` alone would skip it.
|
|
215
|
+
|
|
216
|
+
Non-string leaves (numbers, bools, None, nested lists/dicts) pass
|
|
217
|
+
through unchanged; only str is validated.
|
|
218
|
+
"""
|
|
219
|
+
if isinstance(value, str):
|
|
220
|
+
return validate_string_input(
|
|
221
|
+
value, max_length=MAX_VARIABLE_VALUE_LENGTH, allow_empty=True
|
|
222
|
+
)
|
|
223
|
+
if isinstance(value, list):
|
|
224
|
+
return [_validated_strings(item) for item in value]
|
|
225
|
+
if isinstance(value, dict):
|
|
226
|
+
return {key: _validated_strings(item) for key, item in value.items()}
|
|
227
|
+
return value
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def set_variables(values, variables):
|
|
231
|
+
"""
|
|
232
|
+
Sets the values of variables from a dictionary of new values with validation
|
|
233
|
+
Args:
|
|
234
|
+
values: Dictionary of new values to set
|
|
235
|
+
variables: Dictionary of existing variables with their default values/types
|
|
236
|
+
"""
|
|
237
|
+
logger.debug(f"Setting variables: {list(values.keys())}")
|
|
238
|
+
|
|
239
|
+
if not isinstance(values, dict) or not isinstance(variables, dict):
|
|
240
|
+
logger.error("Both values and variables must be dictionaries")
|
|
241
|
+
raise TypeError("Both values and variables must be dictionaries")
|
|
242
|
+
|
|
243
|
+
for k, v in values.items():
|
|
244
|
+
try:
|
|
245
|
+
# Validate variable name
|
|
246
|
+
validated_name = validate_variable_name(k)
|
|
247
|
+
|
|
248
|
+
# The workflow must have already declared this variable (with a default
|
|
249
|
+
# value/type) - reject unknown names instead of raising a bare KeyError
|
|
250
|
+
if validated_name not in variables:
|
|
251
|
+
declared = ", ".join(sorted(variables.keys()))
|
|
252
|
+
logger.error(
|
|
253
|
+
f"Unknown variable '{validated_name}'; declared variables: {declared}"
|
|
254
|
+
)
|
|
255
|
+
raise ValueError(
|
|
256
|
+
f"Unknown variable '{validated_name}'; declared variables: {declared}"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Validate string values - including strings nested inside a
|
|
260
|
+
# list- or dict-valued argument, which a for_each entry's
|
|
261
|
+
# prompt or from_file always is
|
|
262
|
+
if isinstance(v, str):
|
|
263
|
+
validated_value = validate_string_input(
|
|
264
|
+
v, max_length=MAX_VARIABLE_VALUE_LENGTH, allow_empty=True
|
|
265
|
+
)
|
|
266
|
+
elif isinstance(v, (list, dict)):
|
|
267
|
+
validated_value = _validated_strings(v)
|
|
268
|
+
else:
|
|
269
|
+
validated_value = v
|
|
270
|
+
|
|
271
|
+
logger.debug(
|
|
272
|
+
f"Setting variable {validated_name} to value: {validated_value}"
|
|
273
|
+
)
|
|
274
|
+
# Use the type of the existing variable to convert the new value
|
|
275
|
+
variables[validated_name] = get_value(
|
|
276
|
+
validated_value, type(variables[validated_name]), validated_name
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
except SecurityError as e:
|
|
280
|
+
logger.error(f"Security validation failed for variable {k}: {e}")
|
|
281
|
+
raise
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def get_value(v, desired_type, name=None):
|
|
285
|
+
"""
|
|
286
|
+
Converts a value to the desired type, with special handling for booleans
|
|
287
|
+
Args:
|
|
288
|
+
v: Value to convert
|
|
289
|
+
desired_type: Target type for conversion
|
|
290
|
+
name: Name of the variable being converted, used for error messages
|
|
291
|
+
Returns:
|
|
292
|
+
Converted value
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
ValueError: if v cannot be converted to desired_type, naming the variable,
|
|
296
|
+
its target type, and the offending value.
|
|
297
|
+
"""
|
|
298
|
+
logger.debug(f"Converting value {v} to type {desired_type}")
|
|
299
|
+
|
|
300
|
+
# A variable declared null is an optional one the workflow states no type
|
|
301
|
+
# for - passing a value to it is the expected case, not a suspicious one
|
|
302
|
+
if desired_type is None or desired_type is type(None):
|
|
303
|
+
logger.debug("Variable has no declared type, using the value as given")
|
|
304
|
+
return v
|
|
305
|
+
|
|
306
|
+
# A value already realized by an earlier step (an AudioTrack, an
|
|
307
|
+
# AudioVideo, a PIL.Image, ...) is a live object, not a JSON literal -
|
|
308
|
+
# 'previous_result:' resolves it to this before set_variables ever sees
|
|
309
|
+
# it, so a sibling step's task argument receives it unchanged. A
|
|
310
|
+
# sub-workflow's declared variable must too, rather than being coerced
|
|
311
|
+
# through the type of the variable's own default (usually a string
|
|
312
|
+
# 'asset:'/'output:' reference): desired_type(v) on one of these called
|
|
313
|
+
# str() on an AudioTrack and got its Python repr, which then reached
|
|
314
|
+
# slice_audio looking like a bogus path (#404)
|
|
315
|
+
if not isinstance(v, (str, int, float, bool, list, dict)):
|
|
316
|
+
logger.debug(
|
|
317
|
+
f"Value for variable '{name}' is an already-realized {type(v).__name__}; "
|
|
318
|
+
"using it as given"
|
|
319
|
+
)
|
|
320
|
+
return v
|
|
321
|
+
|
|
322
|
+
# Special handling for boolean string values - bool("0") and bool("no") are
|
|
323
|
+
# both truthy in Python, which would silently invert the user's intent, so
|
|
324
|
+
# only a known set of true/false spellings is accepted here
|
|
325
|
+
if isinstance(v, str) and desired_type is bool:
|
|
326
|
+
lowered = v.lower()
|
|
327
|
+
if lowered in ("true", "1", "yes", "on"):
|
|
328
|
+
return True
|
|
329
|
+
if lowered in ("false", "0", "no", "off"):
|
|
330
|
+
return False
|
|
331
|
+
var_label = name if name is not None else "<unknown>"
|
|
332
|
+
message = f"Cannot interpret '{v}' as true/false for variable '{var_label}'"
|
|
333
|
+
logger.error(message)
|
|
334
|
+
raise ValueError(message)
|
|
335
|
+
|
|
336
|
+
# Special handling for list string values - list("cat") would mangle the
|
|
337
|
+
# string into ['c', 'a', 't'], so a comma-separated string is split instead
|
|
338
|
+
if isinstance(v, str) and desired_type is list:
|
|
339
|
+
return [item.strip() for item in v.split(",")]
|
|
340
|
+
|
|
341
|
+
# A variable typed int by its default (e.g. `"score_gain": 1`) silently
|
|
342
|
+
# truncates a fractional override - int(0.3) == 0, with no error - which
|
|
343
|
+
# reads as a valid, if small, argument rather than the wrong type. Refuse
|
|
344
|
+
# it instead of truncating; the fix is to declare the default as a float
|
|
345
|
+
# (`1.0`) if the variable is meant to accept fractions
|
|
346
|
+
if desired_type is int and isinstance(v, float) and not v.is_integer():
|
|
347
|
+
var_label = name if name is not None else "<unknown>"
|
|
348
|
+
message = (
|
|
349
|
+
f"{var_label} {v!r} would be realized as {int(v)}: this variable "
|
|
350
|
+
f"is typed integer by its default; declare its default as a float "
|
|
351
|
+
f"(e.g. {float(int(v))!r}) to accept fractional values"
|
|
352
|
+
)
|
|
353
|
+
logger.error(message)
|
|
354
|
+
raise ValueError(message)
|
|
355
|
+
|
|
356
|
+
# A string cannot be coerced into a dict or a None - dict('/a/b.png') is
|
|
357
|
+
# nonsense, NoneType('x') a TypeError. Those defaults are how media
|
|
358
|
+
# variables ({'location': ...}) and optional inputs (null) are declared,
|
|
359
|
+
# and a string override is a path or a reference that realize_args
|
|
360
|
+
# resolves later, so it passes through as written
|
|
361
|
+
if isinstance(v, str) and desired_type in (dict, type(None)):
|
|
362
|
+
return v
|
|
363
|
+
|
|
364
|
+
# A dict or list cannot be coerced into a string - str({...}) never
|
|
365
|
+
# raises, it just stringifies the Python repr, so a caller passing the
|
|
366
|
+
# old {"location": ...} object for a variable now declared as a plain
|
|
367
|
+
# string (e.g. templates/ltx2/keyframes' first_image/last_image, #433)
|
|
368
|
+
# sailed through validation and only failed at run time with a path
|
|
369
|
+
# built from the dict's repr
|
|
370
|
+
if isinstance(v, (dict, list)) and desired_type is str:
|
|
371
|
+
var_label = name if name is not None else "<unknown>"
|
|
372
|
+
message = (
|
|
373
|
+
f"Cannot convert variable '{var_label}' value {v!r} to type str: "
|
|
374
|
+
f"this variable takes a plain string (a URL, or an 'asset:'/"
|
|
375
|
+
f"'output:' reference), not an object"
|
|
376
|
+
)
|
|
377
|
+
logger.error(message)
|
|
378
|
+
raise ValueError(message)
|
|
379
|
+
|
|
380
|
+
# Attempt type conversion. A failure here is surfaced immediately with a clear,
|
|
381
|
+
# named error instead of silently passing the unconverted value through - letting
|
|
382
|
+
# it through would fail several layers later inside diffusers/torch with a
|
|
383
|
+
# confusing traceback that doesn't mention the variable at fault.
|
|
384
|
+
try:
|
|
385
|
+
converted = desired_type(v)
|
|
386
|
+
logger.debug(f"Successfully converted to {desired_type.__name__}: {converted}")
|
|
387
|
+
return converted
|
|
388
|
+
except Exception as e:
|
|
389
|
+
var_label = name if name is not None else "<unknown>"
|
|
390
|
+
message = (
|
|
391
|
+
f"Cannot convert variable '{var_label}' value {v!r} to type "
|
|
392
|
+
f"{desired_type.__name__}: {e}"
|
|
393
|
+
)
|
|
394
|
+
logger.error(message)
|
|
395
|
+
raise ValueError(message) from e
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def argument_errors(definition, arguments):
|
|
399
|
+
"""What is wrong with a caller's `arguments` for this workflow, before
|
|
400
|
+
anything is queued.
|
|
401
|
+
|
|
402
|
+
Exactly the check `set_variables` makes at the top of a run - an
|
|
403
|
+
undeclared name, a value that will not coerce to the type the default
|
|
404
|
+
declares - made against a copy, so nothing is mutated and the answer
|
|
405
|
+
costs no GPU time. A workflow that declares no variables at all takes no
|
|
406
|
+
arguments: today those are dropped in silence (`Workflow.run` only
|
|
407
|
+
substitutes when a `variables` block exists), which is the one case the
|
|
408
|
+
run itself does not report.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
definition: A workflow definition
|
|
412
|
+
arguments: The caller's argument dict; empty or None means no errors
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
[{"path": "arguments.<name>", "message": str}, ...], one per bad
|
|
416
|
+
argument, in the order they were given
|
|
417
|
+
"""
|
|
418
|
+
if not arguments:
|
|
419
|
+
return []
|
|
420
|
+
if not isinstance(arguments, dict):
|
|
421
|
+
return [{"path": "arguments", "message": "arguments must be an object"}]
|
|
422
|
+
|
|
423
|
+
declared = definition.get("variables") if isinstance(definition, dict) else None
|
|
424
|
+
if not isinstance(declared, dict) or not declared:
|
|
425
|
+
return [
|
|
426
|
+
{
|
|
427
|
+
"path": f"arguments.{name}",
|
|
428
|
+
"message": f"This workflow declares no variables, so '{name}' "
|
|
429
|
+
"has nowhere to land - it would be ignored by the run",
|
|
430
|
+
}
|
|
431
|
+
for name in arguments
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
errors = []
|
|
435
|
+
for name, value in arguments.items():
|
|
436
|
+
# One at a time against a fresh copy, so each bad argument is
|
|
437
|
+
# reported with its own name rather than the first one stopping the
|
|
438
|
+
# rest from being checked
|
|
439
|
+
try:
|
|
440
|
+
set_variables({name: value}, copy.deepcopy(declared))
|
|
441
|
+
except (ValueError, TypeError, SecurityError) as e:
|
|
442
|
+
errors.append({"path": f"arguments.{name}", "message": str(e)})
|
|
443
|
+
return errors
|
dw/video_extensions.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""The extension of a `video` argument, checked before the run when it can be.
|
|
2
|
+
|
|
3
|
+
`fetch_video` (`dw/arguments.py`) loads every argument named `video` or
|
|
4
|
+
`*_video`, and every `{"media_type": "video", "location": ...}` reference
|
|
5
|
+
whatever it is named, as a video file - and refuses one whose extension is
|
|
6
|
+
not `ALLOWED_VIDEO_EXTENSIONS` there, at run time. A still image handed to
|
|
7
|
+
one of those arguments (`"video": "asset:sheet.png"`) validated clean and
|
|
8
|
+
then failed the job in seconds with "Video file extension not allowed:
|
|
9
|
+
.png", after `get_task("loop_frames")` had described the same argument as
|
|
10
|
+
taking "a still image, or frames in any shape a result carries" (#347).
|
|
11
|
+
|
|
12
|
+
Moved here, into `validation_errors`, for exactly the cases whose extension
|
|
13
|
+
is knowable without running anything: a literal file path, or an
|
|
14
|
+
`asset:`/`output:` reference whose name carries one - `resolve_path_references`
|
|
15
|
+
turns either into a real path before `fetch_video` ever sees it, and the
|
|
16
|
+
extension survives that unchanged. A `previous_result:` (or any reference
|
|
17
|
+
`expand_for_each` left unresolved) names no file yet and is left to the
|
|
18
|
+
existing run-time check, and a URL is left alone too - `fetch_video` never
|
|
19
|
+
gates a URL's extension, so refusing one here would refuse something the run
|
|
20
|
+
itself accepts.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .arguments import (
|
|
24
|
+
CONSTANT_PREFIX,
|
|
25
|
+
PROMPT_PREFIX,
|
|
26
|
+
is_media_reference,
|
|
27
|
+
)
|
|
28
|
+
from .for_each import MEMBER_SEPARATOR, render_path
|
|
29
|
+
from .security import ALLOWED_IMAGE_EXTENSIONS, ALLOWED_VIDEO_EXTENSIONS
|
|
30
|
+
|
|
31
|
+
# Left to the run-time check: not yet resolved to anything an extension can
|
|
32
|
+
# be read off, at the point validation walks the expanded definition
|
|
33
|
+
_UNRESOLVED_PREFIXES = ("previous_result:", "variable:", "item:", "gather:")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _is_video_key(key):
|
|
37
|
+
return isinstance(key, str) and (key == "video" or key.endswith("_video"))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _extension_problem(value):
|
|
41
|
+
"""Why this string's extension is not one `fetch_video` will accept, or
|
|
42
|
+
None - including None for anything whose extension is not yet knowable."""
|
|
43
|
+
if not isinstance(value, str):
|
|
44
|
+
return None
|
|
45
|
+
if value.startswith(("http://", "https://")):
|
|
46
|
+
return None
|
|
47
|
+
if value.startswith(_UNRESOLVED_PREFIXES):
|
|
48
|
+
return None
|
|
49
|
+
if value.startswith(CONSTANT_PREFIX) or value.startswith(PROMPT_PREFIX):
|
|
50
|
+
return None
|
|
51
|
+
ext = value.rsplit(".", 1)
|
|
52
|
+
if len(ext) != 2 or not ext[1] or "/" in ext[1]:
|
|
53
|
+
return None
|
|
54
|
+
ext = f".{ext[1].lower()}"
|
|
55
|
+
if ext in ALLOWED_VIDEO_EXTENSIONS:
|
|
56
|
+
return None
|
|
57
|
+
if ext in ALLOWED_IMAGE_EXTENSIONS:
|
|
58
|
+
return (
|
|
59
|
+
f"'{value}' is a still image, and a video argument loads video "
|
|
60
|
+
f"files - pass it as "
|
|
61
|
+
f'{{"media_type": "image", "location": "{value}"}} to load it as '
|
|
62
|
+
f"a still, or reference a prior image step with previous_result:"
|
|
63
|
+
)
|
|
64
|
+
return f"Video file extension not allowed: {ext}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _video_key_values(value, path):
|
|
68
|
+
"""Strings a video-named key hands to `fetch_video` - itself, or each
|
|
69
|
+
element of a list of them. A dict here is a `media_type` reference and
|
|
70
|
+
is walked by `_video_values` instead, same as `fetch_video` handles it."""
|
|
71
|
+
if isinstance(value, str):
|
|
72
|
+
yield path, value
|
|
73
|
+
elif isinstance(value, list):
|
|
74
|
+
for index, item in enumerate(value):
|
|
75
|
+
yield from _video_key_values(item, path + (index,))
|
|
76
|
+
elif isinstance(value, dict):
|
|
77
|
+
yield from _video_values(value, path)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _video_values(value, path):
|
|
81
|
+
"""Every string this walk can attribute to a location `fetch_video`
|
|
82
|
+
will load, paired with the path it sits at."""
|
|
83
|
+
if isinstance(value, dict):
|
|
84
|
+
if is_media_reference(value) and value.get("media_type") == "video":
|
|
85
|
+
location = value.get("location")
|
|
86
|
+
if isinstance(location, str):
|
|
87
|
+
yield path + ("location",), location
|
|
88
|
+
return
|
|
89
|
+
for key, item in value.items():
|
|
90
|
+
if _is_video_key(key):
|
|
91
|
+
yield from _video_key_values(item, path + (key,))
|
|
92
|
+
else:
|
|
93
|
+
yield from _video_values(item, path + (key,))
|
|
94
|
+
elif isinstance(value, list):
|
|
95
|
+
for index, item in enumerate(value):
|
|
96
|
+
yield from _video_values(item, path + (index,))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def video_extension_errors(workflow_definition, source_indices=None):
|
|
100
|
+
"""Every video argument whose extension `fetch_video` will refuse, for
|
|
101
|
+
every case that extension is knowable before the run, as
|
|
102
|
+
[{path, message}].
|
|
103
|
+
|
|
104
|
+
Walks the substituted, expanded definition, the same convention
|
|
105
|
+
`reference_name_errors` and `task_argument_errors` follow: `source_indices`
|
|
106
|
+
maps an expanded step back to the one the author wrote, and a path inside
|
|
107
|
+
a `for_each` member names the member.
|
|
108
|
+
"""
|
|
109
|
+
steps = workflow_definition.get("steps")
|
|
110
|
+
if not isinstance(steps, list):
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
errors = []
|
|
114
|
+
for index, step in enumerate(steps):
|
|
115
|
+
if not isinstance(step, dict):
|
|
116
|
+
continue
|
|
117
|
+
source = (
|
|
118
|
+
source_indices[index]
|
|
119
|
+
if source_indices is not None and index < len(source_indices)
|
|
120
|
+
else index
|
|
121
|
+
)
|
|
122
|
+
name = step.get("name")
|
|
123
|
+
where = (
|
|
124
|
+
f" in member '{name}'"
|
|
125
|
+
if isinstance(name, str) and MEMBER_SEPARATOR in name
|
|
126
|
+
else ""
|
|
127
|
+
)
|
|
128
|
+
for path, value in _video_values(step, ()):
|
|
129
|
+
problem = _extension_problem(value)
|
|
130
|
+
if problem is None:
|
|
131
|
+
continue
|
|
132
|
+
errors.append(
|
|
133
|
+
{
|
|
134
|
+
"path": render_path(("steps", source) + path),
|
|
135
|
+
"message": f"{problem}{where}",
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
return errors
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
__all__ = ["video_extension_errors"]
|