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
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""What a variable's value is allowed to be, declared by the author.
|
|
2
|
+
|
|
3
|
+
`validate_workflow` passed `num_frames: 61` on an H3 template and answered
|
|
4
|
+
`valid: true`, naming `num_frames` in `checked_arguments` - so the answer
|
|
5
|
+
claimed to cover the caller's value. The run then spent 138.7 s loading the
|
|
6
|
+
weights and the turbo LoRA, entered the text encoder, and failed on a check
|
|
7
|
+
against two integers:
|
|
8
|
+
|
|
9
|
+
MiniMax-H3 generates between 5.0 and 15.0 seconds at 24 fps, so
|
|
10
|
+
`num_frames`, rounded up to the next `17 * n + 5` the video VAE can
|
|
11
|
+
encode, must be between 120 and 360, got 61 (rounded up to 73).
|
|
12
|
+
|
|
13
|
+
Every term in that message is a property of the model. None of it needed a
|
|
14
|
+
loaded pipeline - and nothing reachable over the API stated it, so a consumer
|
|
15
|
+
reading `num_frames: 124` with no range had no way to know the rule was
|
|
16
|
+
`17n + 5` from 124 (#96).
|
|
17
|
+
|
|
18
|
+
The rule is declared in the workflow, where `frame_snap` already puts the
|
|
19
|
+
same numbers for a chain step, and the engine holds no model knowledge of its
|
|
20
|
+
own (CLAUDE.md). One shape, not two: a `variable_constraints` entry takes
|
|
21
|
+
`frame_snap`'s field names, and a chain step's `frame_snap` may be the string
|
|
22
|
+
`"constraint:<variable>"` so a template states `17n + 5` once rather than
|
|
23
|
+
twice in one file.
|
|
24
|
+
|
|
25
|
+
A constraint key is a plain variable name, matched wherever a value by that
|
|
26
|
+
name sits: a top-level variable, or a field of an entry of a `for_each` list
|
|
27
|
+
where some step hands that field to a pipeline (#145). `dialogue-short` has
|
|
28
|
+
no top-level `num_frames` at all - it has a `shots` list whose entries carry
|
|
29
|
+
one - and the rule is the model's either way.
|
|
30
|
+
|
|
31
|
+
Checked in three places for the reasons the task-argument domains are
|
|
32
|
+
(dw/task_domains.py): statically in `validation_errors`, so a stored default
|
|
33
|
+
or a caller's argument outside the rule is a free refusal at the JSON path it
|
|
34
|
+
sits at; at run time before anything loads, where a value that arrived from
|
|
35
|
+
somewhere the static pass cannot see is snapped or refused; and reported
|
|
36
|
+
beside the variable's default by the catalog, which is the half that stops
|
|
37
|
+
the next consumer picking 61.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import logging
|
|
41
|
+
import math
|
|
42
|
+
import numbers
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger("dw")
|
|
45
|
+
|
|
46
|
+
CONSTRAINTS_KEY = "variable_constraints"
|
|
47
|
+
# What a chain step's `frame_snap` writes instead of repeating the numbers
|
|
48
|
+
CONSTRAINT_PREFIX = "constraint:"
|
|
49
|
+
# The fields a `frame_snap` block carries, which are the fields a constraint
|
|
50
|
+
# is checked on - the rest of a constraint says what to do about a violation
|
|
51
|
+
SNAP_FIELDS = ("modulus", "remainder", "min_frames", "max_frames")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def declared_constraints(definition):
|
|
55
|
+
"""The workflow's constraint block, or {}."""
|
|
56
|
+
constraints = definition.get(CONSTRAINTS_KEY)
|
|
57
|
+
return constraints if isinstance(constraints, dict) else {}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def entry_constraint_fields(definition):
|
|
61
|
+
"""Which declared constraints reach inside a list-driven variable, as
|
|
62
|
+
{variable: [constraint name, ...]}.
|
|
63
|
+
|
|
64
|
+
A bound is a property of the model the value is handed to, not of the
|
|
65
|
+
place the JSON put it: `dialogue-short` has no top-level `num_frames`
|
|
66
|
+
at all, it has a `shots` list whose entries carry one, and the same
|
|
67
|
+
`17 * n + 5` rule holds for each of them (#145). So a constraint key
|
|
68
|
+
stays a plain variable name and is matched wherever a value by that
|
|
69
|
+
name sits.
|
|
70
|
+
|
|
71
|
+
Matched by what a step *consumes* rather than by the key's spelling:
|
|
72
|
+
`list_fields` reads the `item:<field>` references the steps make, so a
|
|
73
|
+
constraint reaches an entry field only when some step hands that field
|
|
74
|
+
to a pipeline. An entry key nothing reads is already a warning
|
|
75
|
+
(`entry_field_warnings`) and is not silently bound here.
|
|
76
|
+
"""
|
|
77
|
+
from .for_each import list_fields
|
|
78
|
+
|
|
79
|
+
constraints = declared_constraints(definition)
|
|
80
|
+
if not constraints:
|
|
81
|
+
return {}
|
|
82
|
+
matched = {}
|
|
83
|
+
for variable, spec in list_fields(definition).items():
|
|
84
|
+
fields = spec.get("fields")
|
|
85
|
+
if not fields:
|
|
86
|
+
continue
|
|
87
|
+
names = sorted(field for field in fields if field in constraints)
|
|
88
|
+
if names:
|
|
89
|
+
matched[variable] = names
|
|
90
|
+
return matched
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def entry_targets(definition, values):
|
|
94
|
+
"""Every (variable, index, field name, entry) a list-entry constraint
|
|
95
|
+
applies to - the one walk the three checking layers share."""
|
|
96
|
+
for variable, names in sorted(entry_constraint_fields(definition).items()):
|
|
97
|
+
entries = values.get(variable)
|
|
98
|
+
if not isinstance(entries, list):
|
|
99
|
+
continue
|
|
100
|
+
for index, entry in enumerate(entries):
|
|
101
|
+
if not isinstance(entry, dict):
|
|
102
|
+
continue
|
|
103
|
+
for name in names:
|
|
104
|
+
if name in entry:
|
|
105
|
+
yield variable, index, name, entry
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def snap_block(constraint):
|
|
109
|
+
"""The `frame_snap`-shaped part of a constraint - what a chain step needs
|
|
110
|
+
when it names the constraint rather than repeating it."""
|
|
111
|
+
return {
|
|
112
|
+
key: constraint[key]
|
|
113
|
+
for key in SNAP_FIELDS
|
|
114
|
+
if isinstance(constraint, dict) and key in constraint
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _as_integer(value):
|
|
119
|
+
"""The value as an int, or None when this is not a number to check.
|
|
120
|
+
|
|
121
|
+
A string that is still a reference, a null, a list: not a violation, just
|
|
122
|
+
not something this pass can answer about.
|
|
123
|
+
"""
|
|
124
|
+
if isinstance(value, bool):
|
|
125
|
+
return None
|
|
126
|
+
if isinstance(value, numbers.Integral):
|
|
127
|
+
return int(value)
|
|
128
|
+
if isinstance(value, numbers.Real) and float(value).is_integer():
|
|
129
|
+
return int(value)
|
|
130
|
+
if isinstance(value, str):
|
|
131
|
+
try:
|
|
132
|
+
return int(value)
|
|
133
|
+
except ValueError:
|
|
134
|
+
return None
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def aligned(value, constraint):
|
|
139
|
+
"""The smallest value on the constraint's grid that is not below `value`,
|
|
140
|
+
ignoring the range, or None when there is no grid to align to.
|
|
141
|
+
|
|
142
|
+
The range is checked against *this* number rather than against what the
|
|
143
|
+
caller wrote, because that is what the pipeline does: diffusers'
|
|
144
|
+
`align_num_frames` snaps first and the duration bound then holds for the
|
|
145
|
+
aligned count, which is why 346 frames is refused (it becomes 362) and
|
|
146
|
+
108 is accepted (it becomes 124).
|
|
147
|
+
"""
|
|
148
|
+
number = _as_integer(value)
|
|
149
|
+
modulus = constraint.get("modulus")
|
|
150
|
+
if number is None or not modulus:
|
|
151
|
+
return None
|
|
152
|
+
remainder = constraint.get("remainder", 0) % modulus
|
|
153
|
+
steps = math.ceil((number - remainder) / modulus)
|
|
154
|
+
target = steps * modulus + remainder
|
|
155
|
+
while target < number:
|
|
156
|
+
target += modulus
|
|
157
|
+
return target
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def effective(value, constraint):
|
|
161
|
+
"""The number the run will actually use - `value` itself, or what
|
|
162
|
+
`snap: "up"` rounds it to. None when this is not a number to check."""
|
|
163
|
+
number = _as_integer(value)
|
|
164
|
+
if number is None:
|
|
165
|
+
return None
|
|
166
|
+
if constraint.get("snap") != "up":
|
|
167
|
+
return number
|
|
168
|
+
target = aligned(number, constraint)
|
|
169
|
+
return number if target is None else target
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def snapped(value, constraint):
|
|
173
|
+
"""What `value` becomes, or None when it is left alone.
|
|
174
|
+
|
|
175
|
+
Only `snap: "up"` snaps, and only off the grid: rounding is a change to
|
|
176
|
+
what the caller asked for, so it happens where the author said it should
|
|
177
|
+
and nowhere else. 61 frames on an H3 template is not a request for 124 -
|
|
178
|
+
it rounds to 73, which the range refuses; 130 is a request for 141 (#96).
|
|
179
|
+
"""
|
|
180
|
+
number = _as_integer(value)
|
|
181
|
+
target = effective(value, constraint)
|
|
182
|
+
if target is None or target == number:
|
|
183
|
+
return None
|
|
184
|
+
return target
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def violations(value, constraint):
|
|
188
|
+
"""Why `value` breaks `constraint`, as phrases, or [] when it does not.
|
|
189
|
+
|
|
190
|
+
Judged on the value the run would use, so a constraint that rounds is
|
|
191
|
+
only ever refused for a bound the rounded number still breaks.
|
|
192
|
+
"""
|
|
193
|
+
number = effective(value, constraint)
|
|
194
|
+
if number is None:
|
|
195
|
+
return []
|
|
196
|
+
problems = []
|
|
197
|
+
modulus = constraint.get("modulus")
|
|
198
|
+
remainder = constraint.get("remainder", 0)
|
|
199
|
+
if modulus and (number - remainder) % modulus != 0:
|
|
200
|
+
problems.append(f"must be {modulus} * n + {remainder}")
|
|
201
|
+
minimum = constraint.get("min_frames")
|
|
202
|
+
if minimum is not None and number < minimum:
|
|
203
|
+
problems.append(f"must be at least {minimum}")
|
|
204
|
+
maximum = constraint.get("max_frames")
|
|
205
|
+
if maximum is not None and number > maximum:
|
|
206
|
+
problems.append(f"must be at most {maximum}")
|
|
207
|
+
return problems
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _accepted(constraint):
|
|
211
|
+
"""The rule as a phrase a consumer can act on, which is the half of #96
|
|
212
|
+
that stops the next caller picking 61: the range and the step, together."""
|
|
213
|
+
parts = []
|
|
214
|
+
low, high = constraint.get("min_frames"), constraint.get("max_frames")
|
|
215
|
+
if low is not None and high is not None:
|
|
216
|
+
parts.append(f"{low} to {high}")
|
|
217
|
+
elif low is not None:
|
|
218
|
+
parts.append(f"{low} or more")
|
|
219
|
+
elif high is not None:
|
|
220
|
+
parts.append(f"{high} or less")
|
|
221
|
+
modulus = constraint.get("modulus")
|
|
222
|
+
if modulus:
|
|
223
|
+
parts.append(f"{modulus} * n + {constraint.get('remainder', 0)}")
|
|
224
|
+
return ", ".join(parts)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def refusal(name, value, constraint):
|
|
228
|
+
"""The one wording every layer uses for a value outside its constraint."""
|
|
229
|
+
problems = violations(value, constraint)
|
|
230
|
+
if not problems:
|
|
231
|
+
return None
|
|
232
|
+
reason = constraint.get("reason")
|
|
233
|
+
because = f" - {reason}" if reason else ""
|
|
234
|
+
target = snapped(value, constraint)
|
|
235
|
+
rounding = f" (rounds up to {target})" if target is not None else ""
|
|
236
|
+
return (
|
|
237
|
+
f"'{name}' is {value}{rounding}, which this workflow does not "
|
|
238
|
+
f"accept: {'; '.join(problems)}. Accepted: {_accepted(constraint)}{because}. "
|
|
239
|
+
f"The rule is declared on the workflow, so it is checked before "
|
|
240
|
+
f"anything loads rather than after the weights are in memory"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def snap_notice(name, value, constraint):
|
|
245
|
+
"""The wording for a value that will be rounded, or None.
|
|
246
|
+
|
|
247
|
+
A value the range still refuses after rounding is not a notice - it is a
|
|
248
|
+
refusal, and saying both would be two answers to one mistake.
|
|
249
|
+
"""
|
|
250
|
+
target = snapped(value, constraint)
|
|
251
|
+
if target is None or violations(value, constraint):
|
|
252
|
+
return None
|
|
253
|
+
reason = constraint.get("reason")
|
|
254
|
+
because = f" - {reason}" if reason else ""
|
|
255
|
+
return (
|
|
256
|
+
f"'{name}' is {value}, which this workflow rounds up to {target}"
|
|
257
|
+
f"{because}. The run generates {target}, not {value}"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def constraint_errors(definition, arguments=None, supplied=()):
|
|
262
|
+
"""Every declared constraint a value breaks, as [{path, message}].
|
|
263
|
+
|
|
264
|
+
A value the caller supplied is reported at `arguments.<name>`, where they
|
|
265
|
+
wrote it; a stored default at `variables.<name>`. A constraint that
|
|
266
|
+
declares `snap: "up"` and can reach a legal value is a warning rather
|
|
267
|
+
than an error - the run will round it, and saying so is the point.
|
|
268
|
+
"""
|
|
269
|
+
constraints = declared_constraints(definition)
|
|
270
|
+
if not constraints:
|
|
271
|
+
return []
|
|
272
|
+
variables = definition.get("variables") or {}
|
|
273
|
+
values = {**variables, **(arguments or {})}
|
|
274
|
+
errors = []
|
|
275
|
+
for name in sorted(constraints):
|
|
276
|
+
constraint = constraints[name]
|
|
277
|
+
if not isinstance(constraint, dict) or name not in values:
|
|
278
|
+
continue
|
|
279
|
+
message = refusal(name, values[name], constraint)
|
|
280
|
+
if message is None:
|
|
281
|
+
# Either legal, or legal once rounded - a value the workflow
|
|
282
|
+
# rounds is a warning (constraint_warnings), never an error
|
|
283
|
+
continue
|
|
284
|
+
where = "arguments" if name in (supplied or ()) else "variables"
|
|
285
|
+
errors.append({"path": f"{where}.{name}", "message": message})
|
|
286
|
+
for variable, index, name, entry in entry_targets(definition, values):
|
|
287
|
+
message = refusal(name, entry[name], constraints[name])
|
|
288
|
+
if message is None:
|
|
289
|
+
continue
|
|
290
|
+
where = "arguments" if variable in (supplied or ()) else "variables"
|
|
291
|
+
errors.append(
|
|
292
|
+
{"path": f"{where}.{variable}[{index}].{name}", "message": message}
|
|
293
|
+
)
|
|
294
|
+
return errors
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def constraint_warnings(definition, arguments=None):
|
|
298
|
+
"""Every value a declared constraint will round, as messages.
|
|
299
|
+
|
|
300
|
+
The silent half of #96: the engine already rounded `num_frames` up to the
|
|
301
|
+
VAE's grid with a `logger.warning`, which by the #82 rule does not exist
|
|
302
|
+
out there - a caller got a frame count they did not ask for and nothing
|
|
303
|
+
said so.
|
|
304
|
+
"""
|
|
305
|
+
constraints = declared_constraints(definition)
|
|
306
|
+
if not constraints:
|
|
307
|
+
return []
|
|
308
|
+
values = {**(definition.get("variables") or {}), **(arguments or {})}
|
|
309
|
+
notices = []
|
|
310
|
+
for name in sorted(constraints):
|
|
311
|
+
constraint = constraints[name]
|
|
312
|
+
if not isinstance(constraint, dict) or name not in values:
|
|
313
|
+
continue
|
|
314
|
+
notice = snap_notice(name, values[name], constraint)
|
|
315
|
+
if notice is not None:
|
|
316
|
+
notices.append(notice)
|
|
317
|
+
for variable, index, name, entry in entry_targets(definition, values):
|
|
318
|
+
notice = snap_notice(name, entry[name], constraints[name])
|
|
319
|
+
if notice is not None:
|
|
320
|
+
notices.append(f"{variable}[{index}]: {notice}")
|
|
321
|
+
return notices
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def apply_constraints(definition, variables):
|
|
325
|
+
"""Refuse or round the run's variable values, before anything loads.
|
|
326
|
+
|
|
327
|
+
The run-time half, and the backstop for everything the static pass cannot
|
|
328
|
+
see - an inline workflow, a value a parent workflow passed down. Rounds in
|
|
329
|
+
place and emits a warning for each rounded value, so a frame count the run
|
|
330
|
+
changed reaches the job's `warnings` rather than only the log; raises
|
|
331
|
+
ValueError for a value no rule can reach.
|
|
332
|
+
"""
|
|
333
|
+
from .events import emit_warning
|
|
334
|
+
|
|
335
|
+
constraints = declared_constraints(definition)
|
|
336
|
+
if not constraints or not isinstance(variables, dict):
|
|
337
|
+
return
|
|
338
|
+
for name in sorted(constraints):
|
|
339
|
+
constraint = constraints[name]
|
|
340
|
+
if not isinstance(constraint, dict) or name not in variables:
|
|
341
|
+
continue
|
|
342
|
+
value = variables[name]
|
|
343
|
+
notice = snap_notice(name, value, constraint)
|
|
344
|
+
if notice is not None:
|
|
345
|
+
variables[name] = snapped(value, constraint)
|
|
346
|
+
emit_warning(
|
|
347
|
+
notice, kind="value_snapped", variable=name, value=variables[name]
|
|
348
|
+
)
|
|
349
|
+
continue
|
|
350
|
+
message = refusal(name, value, constraint)
|
|
351
|
+
if message is not None:
|
|
352
|
+
raise ValueError(message)
|
|
353
|
+
# The same three answers for a value sitting in a list entry, where the
|
|
354
|
+
# rule is the model's all the same (#145)
|
|
355
|
+
for variable, index, name, entry in entry_targets(definition, variables):
|
|
356
|
+
constraint = constraints[name]
|
|
357
|
+
value = entry[name]
|
|
358
|
+
notice = snap_notice(name, value, constraint)
|
|
359
|
+
if notice is not None:
|
|
360
|
+
entry[name] = snapped(value, constraint)
|
|
361
|
+
emit_warning(
|
|
362
|
+
f"{variable}[{index}]: {notice}",
|
|
363
|
+
kind="value_snapped",
|
|
364
|
+
variable=f"{variable}[{index}].{name}",
|
|
365
|
+
value=entry[name],
|
|
366
|
+
)
|
|
367
|
+
continue
|
|
368
|
+
message = refusal(name, value, constraint)
|
|
369
|
+
if message is not None:
|
|
370
|
+
raise ValueError(f"{variable}[{index}]: {message}")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def resolve_constraint_references(definition):
|
|
374
|
+
"""Replace every `"frame_snap": "constraint:<name>"` with the declared
|
|
375
|
+
constraint's numbers, in place.
|
|
376
|
+
|
|
377
|
+
So a template states `17n + 5` once - on the variable, where the catalog
|
|
378
|
+
reports it and validation checks it - rather than twice, with a chain
|
|
379
|
+
step's copy free to drift from it. A name that is not declared is an
|
|
380
|
+
error rather than a silently absent constraint: a chain that snapped to
|
|
381
|
+
nothing would stitch segments the pipeline refuses.
|
|
382
|
+
"""
|
|
383
|
+
constraints = declared_constraints(definition)
|
|
384
|
+
|
|
385
|
+
def walk(node):
|
|
386
|
+
if isinstance(node, list):
|
|
387
|
+
for item in node:
|
|
388
|
+
walk(item)
|
|
389
|
+
return
|
|
390
|
+
if not isinstance(node, dict):
|
|
391
|
+
return
|
|
392
|
+
reference = node.get("frame_snap")
|
|
393
|
+
if isinstance(reference, str) and reference.startswith(CONSTRAINT_PREFIX):
|
|
394
|
+
name = reference[len(CONSTRAINT_PREFIX) :]
|
|
395
|
+
if name not in constraints:
|
|
396
|
+
raise ValueError(
|
|
397
|
+
f"'frame_snap': '{reference}' names no entry of this "
|
|
398
|
+
f"workflow's 'variable_constraints'. Declared: "
|
|
399
|
+
+ (", ".join(sorted(constraints)) or "<none>")
|
|
400
|
+
)
|
|
401
|
+
node["frame_snap"] = snap_block(constraints[name])
|
|
402
|
+
for value in node.values():
|
|
403
|
+
walk(value)
|
|
404
|
+
|
|
405
|
+
walk(definition)
|
|
406
|
+
return definition
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def constraint_reference_errors(definition):
|
|
410
|
+
"""`frame_snap: "constraint:x"` naming nothing declared, as
|
|
411
|
+
[{path, message}] - the validation-time form of the error
|
|
412
|
+
resolve_constraint_references raises."""
|
|
413
|
+
constraints = declared_constraints(definition)
|
|
414
|
+
errors = []
|
|
415
|
+
|
|
416
|
+
def walk(node, path):
|
|
417
|
+
if isinstance(node, list):
|
|
418
|
+
for index, item in enumerate(node):
|
|
419
|
+
walk(item, f"{path}[{index}]")
|
|
420
|
+
return
|
|
421
|
+
if not isinstance(node, dict):
|
|
422
|
+
return
|
|
423
|
+
for key, value in node.items():
|
|
424
|
+
where = f"{path}.{key}" if path else key
|
|
425
|
+
if (
|
|
426
|
+
key == "frame_snap"
|
|
427
|
+
and isinstance(value, str)
|
|
428
|
+
and value.startswith(CONSTRAINT_PREFIX)
|
|
429
|
+
and value[len(CONSTRAINT_PREFIX) :] not in constraints
|
|
430
|
+
):
|
|
431
|
+
errors.append(
|
|
432
|
+
{
|
|
433
|
+
"path": where,
|
|
434
|
+
"message": (
|
|
435
|
+
f"'{value}' names no entry of this workflow's "
|
|
436
|
+
f"'variable_constraints'. Declared: "
|
|
437
|
+
+ (", ".join(sorted(constraints)) or "<none>")
|
|
438
|
+
),
|
|
439
|
+
}
|
|
440
|
+
)
|
|
441
|
+
walk(value, where)
|
|
442
|
+
|
|
443
|
+
walk(definition, "")
|
|
444
|
+
return errors
|