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/realize.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Realizing a workflow: a copy of a definition with every mutable input
|
|
2
|
+
pinned, so the file left beside a run's manifest reproduces that run however
|
|
3
|
+
the library, the catalog or the output tree change afterwards.
|
|
4
|
+
|
|
5
|
+
What "mutable" means here is precisely the set of things that can differ
|
|
6
|
+
between two runs of the same file: the arguments a caller passed, the seed a
|
|
7
|
+
seedless workflow drew, the text a stored prompt held at the time, and which
|
|
8
|
+
run 'output:.../latest/...' picked. Everything else - 'asset:', 'constant:',
|
|
9
|
+
'previous_result:', 'builtin:' and a sub-workflow's path - is a name whose
|
|
10
|
+
meaning is pinned by something already recorded (the asset library, the
|
|
11
|
+
manifest's dw_version, the file itself), so it is kept as written and, for a
|
|
12
|
+
local sub-workflow, digested into the manifest instead.
|
|
13
|
+
|
|
14
|
+
Two rules hold this module together. It never mutates its input: the caller
|
|
15
|
+
hands it the definition the run is about to work from. And it never fails a
|
|
16
|
+
run: a reference that will not resolve is left exactly as written, so the
|
|
17
|
+
engine raises its own error at the point it would have raised anyway.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import copy
|
|
21
|
+
import hashlib
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
|
|
25
|
+
from .prompts import PROMPT_PREFIX, fetch_prompt
|
|
26
|
+
from .runs import (
|
|
27
|
+
LATEST,
|
|
28
|
+
OUTPUT_PREFIX,
|
|
29
|
+
is_output_reference,
|
|
30
|
+
output_root as default_output_root,
|
|
31
|
+
resolve_output_reference,
|
|
32
|
+
version_selector,
|
|
33
|
+
)
|
|
34
|
+
from .security import SecurityError, validate_workflow_path
|
|
35
|
+
from .workflow_sources import resolve_sub_workflow, SubWorkflowNotFound
|
|
36
|
+
from .variables import set_variables
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger("dw")
|
|
39
|
+
|
|
40
|
+
BUILTIN_PREFIX = "builtin:"
|
|
41
|
+
VARIABLE_PREFIX = "variable:"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def realize_workflow(
|
|
45
|
+
definition,
|
|
46
|
+
arguments,
|
|
47
|
+
seed,
|
|
48
|
+
base_dir=None,
|
|
49
|
+
prompt_dir=None,
|
|
50
|
+
output_root=None,
|
|
51
|
+
workflow_dir=None,
|
|
52
|
+
pin_outputs=True,
|
|
53
|
+
):
|
|
54
|
+
"""A copy of `definition` with every mutable input pinned.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
definition: The workflow as loaded, before Workflow.run's deep copy.
|
|
58
|
+
Never mutated.
|
|
59
|
+
arguments: The run's argument dict, folded into the variable defaults
|
|
60
|
+
exactly as `set_variables` folds them for the run itself.
|
|
61
|
+
seed: The seed the run resolved - an integer, never None, because
|
|
62
|
+
`Workflow.run` draws one when the workflow names none.
|
|
63
|
+
base_dir: The workflow file's directory, anchoring prompt discovery
|
|
64
|
+
and a sub-workflow's relative path.
|
|
65
|
+
prompt_dir: The prompt library, for `prompt:` inlining.
|
|
66
|
+
output_root: The output directory `output:` names resolve against.
|
|
67
|
+
workflow_dir: The root a sub-workflow path is confined to, as
|
|
68
|
+
`Workflow` confines it; None for an unconfined CLI run.
|
|
69
|
+
pin_outputs: Whether an 'output:.../latest/...' reference is rewritten
|
|
70
|
+
to the run it resolves to. The run leaves this True; the planner
|
|
71
|
+
(dw/plan.py) passes False so a run finishing between a validate
|
|
72
|
+
call and the queue call does not change the fingerprint of
|
|
73
|
+
identical work.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
(realized, annotations) - the pinned copy, and
|
|
77
|
+
{"prompts": [name, ...], "sub_workflows": {path: sha256 or None}}
|
|
78
|
+
for the manifest to carry, since the schema has nowhere to put them.
|
|
79
|
+
"""
|
|
80
|
+
annotations = {"prompts": [], "sub_workflows": {}}
|
|
81
|
+
realized = copy.deepcopy(definition)
|
|
82
|
+
|
|
83
|
+
variables = realized.get("variables")
|
|
84
|
+
if isinstance(variables, dict):
|
|
85
|
+
# Exactly what the run computed: set_variables coerces each value to
|
|
86
|
+
# the type of the declared default and rejects an undeclared name
|
|
87
|
+
set_variables(arguments or {}, variables)
|
|
88
|
+
|
|
89
|
+
realized["seed"] = seed
|
|
90
|
+
# A definition can point its top-level seed at a declared variable
|
|
91
|
+
# ('"seed": "variable:seed_arg"') rather than an integer, so the run's
|
|
92
|
+
# resolved seed can also be read wherever else the workflow names that
|
|
93
|
+
# variable. Pinning the top-level field alone would leave the variable's
|
|
94
|
+
# own default whatever it was written as (typically none) - and a rerun
|
|
95
|
+
# of this realized copy with no seed argument would put that null
|
|
96
|
+
# default back over the pinned integer everywhere but the top level.
|
|
97
|
+
definition_seed = definition.get("seed")
|
|
98
|
+
if isinstance(definition_seed, str) and definition_seed.startswith(VARIABLE_PREFIX):
|
|
99
|
+
seed_variable = definition_seed.removeprefix(VARIABLE_PREFIX)
|
|
100
|
+
if isinstance(variables, dict) and seed_variable in variables:
|
|
101
|
+
variables[seed_variable] = seed
|
|
102
|
+
realized = _pin(
|
|
103
|
+
realized, annotations, base_dir, prompt_dir, output_root, pin_outputs
|
|
104
|
+
)
|
|
105
|
+
_record_sub_workflows(realized.get("steps"), annotations, base_dir, workflow_dir)
|
|
106
|
+
return realized, annotations
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def strings_with_prefix(tree, prefix):
|
|
110
|
+
"""Every string in a nested dict/list tree that starts with `prefix`, in
|
|
111
|
+
first-seen order, deduplicated."""
|
|
112
|
+
found = []
|
|
113
|
+
|
|
114
|
+
def collect(value):
|
|
115
|
+
if value.startswith(prefix) and value not in found:
|
|
116
|
+
found.append(value)
|
|
117
|
+
return value
|
|
118
|
+
|
|
119
|
+
_map_strings(tree, collect)
|
|
120
|
+
return found
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _map_strings(value, transform):
|
|
124
|
+
"""Rebuild `value`, replacing every string it contains with
|
|
125
|
+
`transform(string)`. One recursive walk over dicts, lists and strings -
|
|
126
|
+
the shape `referenced_result_names` in dw/step_cache.py walks - shared by
|
|
127
|
+
`_pin` (which rewrites matching references) and `strings_with_prefix`
|
|
128
|
+
(which only collects them), so a reference is found wherever it sits: a
|
|
129
|
+
pipeline argument, a task argument, a sub-workflow's argument map, an
|
|
130
|
+
element of a list.
|
|
131
|
+
"""
|
|
132
|
+
if isinstance(value, str):
|
|
133
|
+
return transform(value)
|
|
134
|
+
if isinstance(value, dict):
|
|
135
|
+
return {key: _map_strings(item, transform) for key, item in value.items()}
|
|
136
|
+
if isinstance(value, list):
|
|
137
|
+
return [_map_strings(item, transform) for item in value]
|
|
138
|
+
return value
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _pin(value, annotations, base_dir, prompt_dir, output_root, pin_outputs=True):
|
|
142
|
+
"""Rebuild a value with prompt references inlined and, when
|
|
143
|
+
`pin_outputs`, output references pinned."""
|
|
144
|
+
|
|
145
|
+
def transform(string):
|
|
146
|
+
if string.startswith(PROMPT_PREFIX):
|
|
147
|
+
return _inline_prompt(string, annotations, prompt_dir, base_dir)
|
|
148
|
+
if pin_outputs and is_output_reference(string):
|
|
149
|
+
return _pin_output(string, output_root)
|
|
150
|
+
return string
|
|
151
|
+
|
|
152
|
+
return _map_strings(value, transform)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _inline_prompt(reference, annotations, prompt_dir, base_dir):
|
|
156
|
+
"""The stored text, and the name recorded for the manifest.
|
|
157
|
+
|
|
158
|
+
A stored prompt's text may not itself begin with a reference prefix (an
|
|
159
|
+
engine rule `fetch_prompt` enforces), so inlining cannot introduce a
|
|
160
|
+
second resolution.
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
text = fetch_prompt(reference, prompt_dir, base_dir)
|
|
164
|
+
except (SecurityError, OSError, ValueError) as e:
|
|
165
|
+
logger.warning(f"Realization kept {reference} as written: {e}")
|
|
166
|
+
return reference
|
|
167
|
+
name = reference.removeprefix(PROMPT_PREFIX).strip()
|
|
168
|
+
if name not in annotations["prompts"]:
|
|
169
|
+
annotations["prompts"].append(name)
|
|
170
|
+
return text
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _pin_output(reference, output_root):
|
|
174
|
+
"""'output:<identity>/latest/<file>' - or '/v4/' - rewritten to the run it
|
|
175
|
+
resolved to.
|
|
176
|
+
|
|
177
|
+
A version is stable, but deleting the newest run frees its number for
|
|
178
|
+
reuse, so the realized copy names the run id either way. An explicit run
|
|
179
|
+
id is already pinned, so it is returned untouched without touching the
|
|
180
|
+
disk - realizing must not fail on a reference the run has not reached
|
|
181
|
+
yet.
|
|
182
|
+
"""
|
|
183
|
+
name = reference.removeprefix(OUTPUT_PREFIX).strip()
|
|
184
|
+
if not any(
|
|
185
|
+
part == LATEST or version_selector(part) is not None for part in name.split("/")
|
|
186
|
+
):
|
|
187
|
+
return reference
|
|
188
|
+
root = output_root or default_output_root()
|
|
189
|
+
try:
|
|
190
|
+
resolved = resolve_output_reference(reference, root)
|
|
191
|
+
relative = os.path.relpath(resolved, root).replace(os.sep, "/")
|
|
192
|
+
except (SecurityError, OSError, ValueError) as e:
|
|
193
|
+
logger.warning(f"Realization kept {reference} as written: {e}")
|
|
194
|
+
return reference
|
|
195
|
+
return f"{OUTPUT_PREFIX}{relative}"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _record_sub_workflows(steps, annotations, base_dir, workflow_dir):
|
|
199
|
+
"""Digest every sub-workflow a step names by local path.
|
|
200
|
+
|
|
201
|
+
The schema's 'workflow' step takes a path, not a definition, so the
|
|
202
|
+
realized file keeps the path and the manifest records what the file held.
|
|
203
|
+
A builtin is packaged with the engine and pinned by the manifest's
|
|
204
|
+
dw_version, so it is not digested.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def scan(value):
|
|
208
|
+
if isinstance(value, dict):
|
|
209
|
+
reference = value.get("workflow")
|
|
210
|
+
if isinstance(reference, dict):
|
|
211
|
+
path = reference.get("path")
|
|
212
|
+
if isinstance(path, str) and not path.startswith(BUILTIN_PREFIX):
|
|
213
|
+
annotations["sub_workflows"][path] = _digest(
|
|
214
|
+
path, base_dir, workflow_dir
|
|
215
|
+
)
|
|
216
|
+
for item in value.values():
|
|
217
|
+
scan(item)
|
|
218
|
+
elif isinstance(value, list):
|
|
219
|
+
for item in value:
|
|
220
|
+
scan(item)
|
|
221
|
+
|
|
222
|
+
for step in steps or []:
|
|
223
|
+
scan(step)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def read_sub_workflow(path, base_dir, workflow_dir):
|
|
227
|
+
"""The bytes of the sub-workflow file a step's `path` names, or None
|
|
228
|
+
when it cannot be read.
|
|
229
|
+
|
|
230
|
+
Resolved the way `Workflow.create_step_action` resolves it - beside the
|
|
231
|
+
referencing file, then across the workflow search path, then through
|
|
232
|
+
`validate_workflow_path` confined to the root it came from - so a path
|
|
233
|
+
this run could not have loaded is not one realization (or the planner)
|
|
234
|
+
reads either, and a catalog name the run composed is read rather than
|
|
235
|
+
recorded as unreadable (#90).
|
|
236
|
+
"""
|
|
237
|
+
try:
|
|
238
|
+
candidate, root = resolve_sub_workflow(path, base_dir or ".", workflow_dir)
|
|
239
|
+
validated = validate_workflow_path(candidate, root)
|
|
240
|
+
with open(validated, "rb") as file:
|
|
241
|
+
return file.read()
|
|
242
|
+
except (SecurityError, OSError, ValueError, SubWorkflowNotFound) as e:
|
|
243
|
+
logger.debug(f"Sub-workflow {path} could not be read: {e}")
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _digest(path, base_dir, workflow_dir):
|
|
248
|
+
"""The SHA-256 of a sub-workflow file, or None when it cannot be read."""
|
|
249
|
+
raw = read_sub_workflow(path, base_dir, workflow_dir)
|
|
250
|
+
return hashlib.sha256(raw).hexdigest() if raw is not None else None
|
dw/reference_limits.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""How many references of each kind a pipeline's request may carry.
|
|
2
|
+
|
|
3
|
+
A pipeline that conditions on reference media - MiniMax-H3's `ref2va` is the
|
|
4
|
+
one in the catalog - bounds what it accepts: so many images, so many videos,
|
|
5
|
+
so many audio clips, so many in total, and for H3 an audio reference may never
|
|
6
|
+
be the only one. Those bounds are enforced by the pipeline itself, which means
|
|
7
|
+
they are enforced *after* the checkpoint is loaded: a caller who ran the free
|
|
8
|
+
`validate_workflow`, was quoted eight minutes and acknowledged the cost found
|
|
9
|
+
out minutes in, from a failed job, what a millisecond of arithmetic could have
|
|
10
|
+
told them (#136).
|
|
11
|
+
|
|
12
|
+
The numbers are not written here. They live on the diffusers block that
|
|
13
|
+
enforces them, as its constructor's defaults, and this module reads them off
|
|
14
|
+
that block - so a diffusers release that raises a limit raises it here too,
|
|
15
|
+
and the engine holds no model knowledge but the name of the class that
|
|
16
|
+
declares the limits (see REFERENCE_LIMIT_BLOCKS). A family diffusers has no
|
|
17
|
+
such block for is simply not checked: this pass only ever refuses a request
|
|
18
|
+
the pipeline itself would refuse.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import importlib
|
|
22
|
+
import inspect
|
|
23
|
+
import logging
|
|
24
|
+
|
|
25
|
+
from .for_each import MEMBER_SEPARATOR, render_path
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("dw")
|
|
28
|
+
|
|
29
|
+
# The module a family's reference classes live in -> the block whose
|
|
30
|
+
# __init__ defaults declare that family's limits. A pointer, not a number:
|
|
31
|
+
# what a limit *is* stays diffusers', which is the only place it can stay
|
|
32
|
+
# correct across a release
|
|
33
|
+
REFERENCE_LIMIT_BLOCKS = {
|
|
34
|
+
"diffusers.modular_pipelines.minimax_h3": (
|
|
35
|
+
"diffusers.modular_pipelines.minimax_h3.before_encoder",
|
|
36
|
+
"MiniMaxH3Ref2VASetupStep",
|
|
37
|
+
),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Families where an audio reference may not stand alone - H3 conditions a
|
|
41
|
+
# soundtrack on a picture, so audio by itself has nothing to speak over
|
|
42
|
+
AUDIO_NEEDS_A_PICTURE = frozenset(REFERENCE_LIMIT_BLOCKS)
|
|
43
|
+
|
|
44
|
+
REFERENCE_TYPE_KEY = "reference_type"
|
|
45
|
+
|
|
46
|
+
# Values substitution resolves before this pass runs; one still spelled out
|
|
47
|
+
# is another pass's complaint, not this one's
|
|
48
|
+
_UNRESOLVED_PREFIXES = ("variable:", "item:", "previous_result:", "gather:")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _family(module_name):
|
|
52
|
+
"""The REFERENCE_LIMIT_BLOCKS key a class's module belongs to, or None."""
|
|
53
|
+
for family in REFERENCE_LIMIT_BLOCKS:
|
|
54
|
+
if module_name == family or module_name.startswith(family + "."):
|
|
55
|
+
return family
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _limits(family):
|
|
60
|
+
"""The (model name, per-kind limits, total) a family declares.
|
|
61
|
+
|
|
62
|
+
Read from the block's constructor signature rather than from an instance:
|
|
63
|
+
constructing one is cheap but not free, and a default is exactly what the
|
|
64
|
+
signature holds. The model name is the block's own, so even the family's
|
|
65
|
+
name in the message is diffusers'.
|
|
66
|
+
"""
|
|
67
|
+
module_path, class_name = REFERENCE_LIMIT_BLOCKS[family]
|
|
68
|
+
try:
|
|
69
|
+
block = getattr(importlib.import_module(module_path), class_name)
|
|
70
|
+
parameters = inspect.signature(block.__init__).parameters
|
|
71
|
+
except Exception:
|
|
72
|
+
# A diffusers that renamed or dropped the block - checking nothing is
|
|
73
|
+
# the right failure here, since the pipeline still enforces its own
|
|
74
|
+
logger.debug(f"No reference limits available from {family}", exc_info=True)
|
|
75
|
+
return None, None, None
|
|
76
|
+
|
|
77
|
+
per_kind = {}
|
|
78
|
+
total = None
|
|
79
|
+
for name, parameter in parameters.items():
|
|
80
|
+
if parameter.default is inspect.Parameter.empty:
|
|
81
|
+
continue
|
|
82
|
+
if name == "max_references":
|
|
83
|
+
total = parameter.default
|
|
84
|
+
elif name.startswith("max_") and name.endswith("s"):
|
|
85
|
+
per_kind[name[len("max_") : -1]] = parameter.default
|
|
86
|
+
model_name = getattr(block, "model_name", family.rsplit(".", 1)[-1])
|
|
87
|
+
return model_name, (per_kind or None), total
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _reference_class(value):
|
|
91
|
+
"""The class a reference entry's '*_type' names, or None.
|
|
92
|
+
|
|
93
|
+
Anything that does not resolve, or that the untrusted gate refuses, is
|
|
94
|
+
left alone: realize_args reports a type it cannot load, and the type
|
|
95
|
+
reference check a refused one, with a better message than this pass
|
|
96
|
+
could give.
|
|
97
|
+
"""
|
|
98
|
+
if not isinstance(value, dict):
|
|
99
|
+
return None
|
|
100
|
+
name = value.get(REFERENCE_TYPE_KEY)
|
|
101
|
+
if not isinstance(name, str) or name.startswith(_UNRESOLVED_PREFIXES):
|
|
102
|
+
return None
|
|
103
|
+
if "." not in name:
|
|
104
|
+
return None
|
|
105
|
+
# Through the run's own resolver, so an untrusted workflow's name meets
|
|
106
|
+
# the same trust gate here, at validate time, before anything imports
|
|
107
|
+
from .type_helpers import load_type_from_full_name
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
return load_type_from_full_name(name, REFERENCE_TYPE_KEY)
|
|
111
|
+
except Exception:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _kinds(entries):
|
|
116
|
+
"""(family, [kind, ...]) for a list of reference entries, or None.
|
|
117
|
+
|
|
118
|
+
A family is the one whose limits this pass knows how to read; an entry
|
|
119
|
+
whose class carries no 'kind', or a list mixing two families, is not a
|
|
120
|
+
reference set this pass understands.
|
|
121
|
+
"""
|
|
122
|
+
if not isinstance(entries, list) or not entries:
|
|
123
|
+
return None
|
|
124
|
+
found = None
|
|
125
|
+
kinds = []
|
|
126
|
+
for entry in entries:
|
|
127
|
+
reference = _reference_class(entry)
|
|
128
|
+
kind = getattr(reference, "kind", None)
|
|
129
|
+
if not isinstance(kind, str):
|
|
130
|
+
return None
|
|
131
|
+
family = _family(getattr(reference, "__module__", ""))
|
|
132
|
+
if family is None:
|
|
133
|
+
return None
|
|
134
|
+
if found is None:
|
|
135
|
+
found = family
|
|
136
|
+
elif found != family:
|
|
137
|
+
return None
|
|
138
|
+
kinds.append(kind)
|
|
139
|
+
return (found, kinds) if found else None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _errors_for(kinds, family):
|
|
143
|
+
"""Every limit a set of reference kinds breaks, as messages."""
|
|
144
|
+
model_name, per_kind, total = _limits(family)
|
|
145
|
+
if per_kind is None and total is None:
|
|
146
|
+
return []
|
|
147
|
+
|
|
148
|
+
messages = []
|
|
149
|
+
for kind, limit in sorted((per_kind or {}).items()):
|
|
150
|
+
count = kinds.count(kind)
|
|
151
|
+
if count > limit:
|
|
152
|
+
messages.append(
|
|
153
|
+
f"{model_name} accepts at most {limit} "
|
|
154
|
+
f"{kind} reference{'s' if limit != 1 else ''}, got {count}."
|
|
155
|
+
)
|
|
156
|
+
if total is not None and len(kinds) > total:
|
|
157
|
+
messages.append(
|
|
158
|
+
f"{model_name} accepts at most {total} references in total, "
|
|
159
|
+
f"got {len(kinds)}."
|
|
160
|
+
)
|
|
161
|
+
if family in AUDIO_NEEDS_A_PICTURE and set(kinds) == {"audio"}:
|
|
162
|
+
messages.append(
|
|
163
|
+
"An audio reference has to be paired with at least one image or "
|
|
164
|
+
"video reference and cannot be used on its own."
|
|
165
|
+
)
|
|
166
|
+
return messages
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def reference_limit_errors(workflow_definition, source_indices=None):
|
|
170
|
+
"""Every reference list a pipeline would refuse, as [{path, message}].
|
|
171
|
+
|
|
172
|
+
The definition handed here has already been substituted and expanded, so a
|
|
173
|
+
`for_each` member's own references are checked as they will run;
|
|
174
|
+
`source_indices` maps each expanded step back to the step the author
|
|
175
|
+
wrote, and the member is named in the message - the same convention
|
|
176
|
+
subfolder_errors uses.
|
|
177
|
+
"""
|
|
178
|
+
steps = workflow_definition.get("steps")
|
|
179
|
+
if not isinstance(steps, list):
|
|
180
|
+
return []
|
|
181
|
+
|
|
182
|
+
errors = []
|
|
183
|
+
for index, step in enumerate(steps):
|
|
184
|
+
if not isinstance(step, dict):
|
|
185
|
+
continue
|
|
186
|
+
pipeline = step.get("pipeline")
|
|
187
|
+
arguments = pipeline.get("arguments") if isinstance(pipeline, dict) else None
|
|
188
|
+
if not isinstance(arguments, dict):
|
|
189
|
+
continue
|
|
190
|
+
source = (
|
|
191
|
+
source_indices[index]
|
|
192
|
+
if source_indices is not None and index < len(source_indices)
|
|
193
|
+
else index
|
|
194
|
+
)
|
|
195
|
+
name = step.get("name")
|
|
196
|
+
where = (
|
|
197
|
+
f" in member '{name}'"
|
|
198
|
+
if isinstance(name, str) and MEMBER_SEPARATOR in name
|
|
199
|
+
else ""
|
|
200
|
+
)
|
|
201
|
+
for key, value in arguments.items():
|
|
202
|
+
found = _kinds(value)
|
|
203
|
+
if found is None:
|
|
204
|
+
continue
|
|
205
|
+
family, kinds = found
|
|
206
|
+
for message in _errors_for(kinds, family):
|
|
207
|
+
errors.append(
|
|
208
|
+
{
|
|
209
|
+
"path": render_path(
|
|
210
|
+
("steps", source, "pipeline", "arguments", key)
|
|
211
|
+
),
|
|
212
|
+
"message": f"{message.rstrip('.')}{where}.",
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
return errors
|
dw/reference_names.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""The shape of an `asset:` / `prompt:` / `output:` name, checked for free.
|
|
2
|
+
|
|
3
|
+
An `output:` reference naming a file a `for_each` step wrote - the `@` in
|
|
4
|
+
`shot@opening_statement` - validated clean and then failed the job at run
|
|
5
|
+
time, after the queue, with a message that described a *valid* name and said
|
|
6
|
+
nothing about what it had objected to (#162). Two things were wrong with
|
|
7
|
+
that, and only one of them was the `@`: a name that can never resolve, in
|
|
8
|
+
any workspace, is not a run-time discovery. Its shape is a property of the
|
|
9
|
+
string alone.
|
|
10
|
+
|
|
11
|
+
So the shape is checked here, in `validation_errors`, which is what
|
|
12
|
+
`POST /api/validate`, `validate_workflow` and the pre-queue check all run -
|
|
13
|
+
while *existence* stays where it was. Whether a run id is still on disk
|
|
14
|
+
depends on the workspace and on what pruning has taken, and the definition's
|
|
15
|
+
own references (a template's `prompt:ltx2/hummingbird_garden`) are not the
|
|
16
|
+
caller's to answer for; the caller's `arguments` are separately resolved
|
|
17
|
+
against the workspace by the validate route.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .assets import ASSET_PREFIX
|
|
21
|
+
from .for_each import MEMBER_SEPARATOR, render_path
|
|
22
|
+
from .prompts import PROMPT_PREFIX
|
|
23
|
+
from .runs import OUTPUT_PREFIX
|
|
24
|
+
from .security import (
|
|
25
|
+
InvalidInputError,
|
|
26
|
+
validate_asset_reference,
|
|
27
|
+
validate_output_reference,
|
|
28
|
+
validate_prompt_reference,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Substitution and expansion run before this pass, so every string reaching
|
|
32
|
+
# it is literal. One still spelled with a deferred prefix is nothing this
|
|
33
|
+
# pass resolved, and the undeclared-variable pass owns that complaint
|
|
34
|
+
_UNRESOLVED_PREFIXES = ("variable:", "item:", "previous_result:", "gather:")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _output_name(reference):
|
|
38
|
+
"""The part of an `output:` reference the name rule applies to.
|
|
39
|
+
|
|
40
|
+
`latest` in the run-id position is expanded before the path is joined,
|
|
41
|
+
so it is checked as the ordinary segment it looks like.
|
|
42
|
+
"""
|
|
43
|
+
return reference.removeprefix(OUTPUT_PREFIX).strip()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_KINDS = (
|
|
47
|
+
(OUTPUT_PREFIX, validate_output_reference, _output_name),
|
|
48
|
+
(ASSET_PREFIX, validate_asset_reference, None),
|
|
49
|
+
(PROMPT_PREFIX, validate_prompt_reference, None),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def reference_name_errors(workflow_definition, source_indices=None):
|
|
54
|
+
"""Every reference in the definition whose *name* is malformed, as
|
|
55
|
+
[{path, message}].
|
|
56
|
+
|
|
57
|
+
Walks the substituted, expanded definition. `source_indices`, when
|
|
58
|
+
given, maps each expanded step back to the step the author wrote, so an
|
|
59
|
+
error inside a `for_each` member carries a path in their file and names
|
|
60
|
+
the member.
|
|
61
|
+
"""
|
|
62
|
+
steps = workflow_definition.get("steps")
|
|
63
|
+
if not isinstance(steps, list):
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
errors = []
|
|
67
|
+
for index, step in enumerate(steps):
|
|
68
|
+
if not isinstance(step, dict):
|
|
69
|
+
continue
|
|
70
|
+
source = (
|
|
71
|
+
source_indices[index]
|
|
72
|
+
if source_indices is not None and index < len(source_indices)
|
|
73
|
+
else index
|
|
74
|
+
)
|
|
75
|
+
name = step.get("name")
|
|
76
|
+
where = (
|
|
77
|
+
f" in member '{name}'"
|
|
78
|
+
if isinstance(name, str) and MEMBER_SEPARATOR in name
|
|
79
|
+
else ""
|
|
80
|
+
)
|
|
81
|
+
for path, value in _strings(step, ("steps", source)):
|
|
82
|
+
problem = reference_fault(value)
|
|
83
|
+
if problem is not None:
|
|
84
|
+
errors.append(
|
|
85
|
+
{"path": render_path(path), "message": f"{problem}{where}"}
|
|
86
|
+
)
|
|
87
|
+
return errors
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def reference_fault(value):
|
|
91
|
+
"""Why this string's reference name is malformed, or None.
|
|
92
|
+
|
|
93
|
+
None for anything that is not a reference, and for one still spelled
|
|
94
|
+
with a deferred prefix behind the reference prefix - `output:` on a
|
|
95
|
+
value substitution has not reached yet is not this pass's complaint.
|
|
96
|
+
"""
|
|
97
|
+
if not isinstance(value, str):
|
|
98
|
+
return None
|
|
99
|
+
for prefix, check, extract in _KINDS:
|
|
100
|
+
if not value.startswith(prefix):
|
|
101
|
+
continue
|
|
102
|
+
rest = value[len(prefix) :].strip()
|
|
103
|
+
if not rest or rest.startswith(_UNRESOLVED_PREFIXES):
|
|
104
|
+
return None
|
|
105
|
+
try:
|
|
106
|
+
check(extract(value) if extract else rest)
|
|
107
|
+
except InvalidInputError as e:
|
|
108
|
+
return str(e)
|
|
109
|
+
return None
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _strings(value, path):
|
|
114
|
+
"""Every string inside a step, paired with the path it sits at."""
|
|
115
|
+
if isinstance(value, str):
|
|
116
|
+
yield path, value
|
|
117
|
+
elif isinstance(value, list):
|
|
118
|
+
for index, item in enumerate(value):
|
|
119
|
+
yield from _strings(item, path + (index,))
|
|
120
|
+
elif isinstance(value, dict):
|
|
121
|
+
for key, item in value.items():
|
|
122
|
+
yield from _strings(item, path + (key,))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = ["reference_fault", "reference_name_errors"]
|