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.
Files changed (260) hide show
  1. diffusers_workflow-0.4.0.dist-info/METADATA +318 -0
  2. diffusers_workflow-0.4.0.dist-info/RECORD +260 -0
  3. diffusers_workflow-0.4.0.dist-info/WHEEL +5 -0
  4. diffusers_workflow-0.4.0.dist-info/entry_points.txt +7 -0
  5. diffusers_workflow-0.4.0.dist-info/licenses/LICENSE +201 -0
  6. diffusers_workflow-0.4.0.dist-info/top_level.txt +2 -0
  7. dw/__init__.py +440 -0
  8. dw/adapter_compatibility.py +226 -0
  9. dw/arguments.py +1231 -0
  10. dw/assessment_rules.py +159 -0
  11. dw/assets.py +130 -0
  12. dw/cache_blocks.json +16 -0
  13. dw/cache_blocks.py +146 -0
  14. dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
  15. dw/content_types.py +150 -0
  16. dw/dissolve_frame_errors.py +121 -0
  17. dw/docs/ACCELERATION.md +352 -0
  18. dw/docs/AGENT_LOOP.md +95 -0
  19. dw/docs/DEPENDENCIES.md +91 -0
  20. dw/docs/IP_ADAPTER.md +109 -0
  21. dw/docs/LORAS.md +131 -0
  22. dw/docs/MCP.md +517 -0
  23. dw/docs/PROMPT_WEIGHTING.md +78 -0
  24. dw/docs/QUANTIZATION.md +230 -0
  25. dw/docs/RECIPES_24GB.md +201 -0
  26. dw/docs/RELEASING.md +195 -0
  27. dw/docs/REMOTE.md +140 -0
  28. dw/docs/REPL_COMMANDS.md +121 -0
  29. dw/docs/REPL_WORKER_GUIDE.md +51 -0
  30. dw/docs/SECURITY.md +272 -0
  31. dw/docs/SECURITY_QUICKREF.md +112 -0
  32. dw/docs/SERVER.md +679 -0
  33. dw/docs/TASKS.md +1741 -0
  34. dw/docs/TESTING.md +71 -0
  35. dw/docs/WORKFLOW_GUIDE.md +2038 -0
  36. dw/docs/WORKSPACES.md +316 -0
  37. dw/download_watch.py +335 -0
  38. dw/elision.py +306 -0
  39. dw/events.py +275 -0
  40. dw/for_each.py +409 -0
  41. dw/host_memory.py +258 -0
  42. dw/host_memory_projection.py +230 -0
  43. dw/hub_cache.py +432 -0
  44. dw/introspection.py +1228 -0
  45. dw/kernel_availability.py +208 -0
  46. dw/locations.py +599 -0
  47. dw/log_setup.py +45 -0
  48. dw/loudness.py +82 -0
  49. dw/media_audio.py +217 -0
  50. dw/media_frames.py +367 -0
  51. dw/media_info.py +297 -0
  52. dw/pipeline_processors/chain.py +821 -0
  53. dw/pipeline_processors/config_objects.py +237 -0
  54. dw/pipeline_processors/pipeline.py +2297 -0
  55. dw/pipeline_processors/remote.py +46 -0
  56. dw/plan.py +920 -0
  57. dw/previous_results.py +411 -0
  58. dw/probe_paths.py +59 -0
  59. dw/prompt_schema.json +48 -0
  60. dw/prompt_weighting.py +378 -0
  61. dw/prompts.py +159 -0
  62. dw/realize.py +250 -0
  63. dw/reference_limits.py +215 -0
  64. dw/reference_names.py +125 -0
  65. dw/repl.py +338 -0
  66. dw/repl_commands.py +836 -0
  67. dw/repl_worker.py +159 -0
  68. dw/result.py +1720 -0
  69. dw/result_fps.py +82 -0
  70. dw/run.py +162 -0
  71. dw/runs.py +768 -0
  72. dw/scalar_result_validation.py +97 -0
  73. dw/schema.py +283 -0
  74. dw/security.py +1038 -0
  75. dw/select_validation.py +115 -0
  76. dw/serve.py +277 -0
  77. dw/server/__init__.py +2 -0
  78. dw/server/app.py +4586 -0
  79. dw/server/assess.py +132 -0
  80. dw/server/catalog_shape.py +487 -0
  81. dw/server/enhancers.py +129 -0
  82. dw/server/exports.py +480 -0
  83. dw/server/guides.py +257 -0
  84. dw/server/jobs.py +1561 -0
  85. dw/server/mcp_mount.py +95 -0
  86. dw/server/netinfo.py +124 -0
  87. dw/server/observed_cost.py +379 -0
  88. dw/server/sysinfo.py +71 -0
  89. dw/server/ui/assets/abap-08VXUWAP.js +1 -0
  90. dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
  91. dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  92. dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
  93. dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
  94. dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
  95. dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
  96. dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
  97. dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
  98. dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
  99. dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
  100. dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
  101. dw/server/ui/assets/css-DIMkf-bt.js +3 -0
  102. dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
  103. dw/server/ui/assets/cssMode-CPznxfY8.js +1 -0
  104. dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
  105. dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
  106. dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  107. dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
  108. dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
  109. dw/server/ui/assets/editor.api-CpWcotrd.js +847 -0
  110. dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
  111. dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
  112. dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
  113. dw/server/ui/assets/freemarker2-CXtRM8N4.js +3 -0
  114. dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  115. dw/server/ui/assets/go-C-y9NEjX.js +1 -0
  116. dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
  117. dw/server/ui/assets/handlebars-N7x-6NMY.js +1 -0
  118. dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
  119. dw/server/ui/assets/html-PhsdjHSr.js +1 -0
  120. dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
  121. dw/server/ui/assets/htmlMode-Dgj0SEok.js +1 -0
  122. dw/server/ui/assets/index-3Vw6WAPW.css +1 -0
  123. dw/server/ui/assets/index-DgrYhQd9.js +43 -0
  124. dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
  125. dw/server/ui/assets/java-BEtHBSE6.js +1 -0
  126. dw/server/ui/assets/javascript-BJqN9Qhv.js +1 -0
  127. dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
  128. dw/server/ui/assets/jsonMode-DbM4SWSv.js +7 -0
  129. dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
  130. dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
  131. dw/server/ui/assets/less-B9JPFI3C.js +2 -0
  132. dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
  133. dw/server/ui/assets/liquid-BWr8lEc4.js +1 -0
  134. dw/server/ui/assets/lspLanguageFeatures-C1iGuDyZ.js +4 -0
  135. dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
  136. dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
  137. dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
  138. dw/server/ui/assets/mdx-DAdMi_0p.js +1 -0
  139. dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
  140. dw/server/ui/assets/monaco--ixms01u.css +1 -0
  141. dw/server/ui/assets/monaco-BGCeEqaw.js +56 -0
  142. dw/server/ui/assets/msdax-DauUninz.js +1 -0
  143. dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
  144. dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  145. dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
  146. dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  147. dw/server/ui/assets/perl-oz_6vUea.js +1 -0
  148. dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
  149. dw/server/ui/assets/php-nr791fC2.js +1 -0
  150. dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
  151. dw/server/ui/assets/postiats-43DmfD33.js +1 -0
  152. dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
  153. dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
  154. dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
  155. dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
  156. dw/server/ui/assets/python-Bcn70HdC.js +1 -0
  157. dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
  158. dw/server/ui/assets/r-BwWrilGY.js +1 -0
  159. dw/server/ui/assets/razor-D1HmNnby.js +1 -0
  160. dw/server/ui/assets/redis-ClamHrr6.js +1 -0
  161. dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
  162. dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  163. dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
  164. dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
  165. dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
  166. dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
  167. dw/server/ui/assets/scheme-BeGwcela.js +1 -0
  168. dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
  169. dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
  170. dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
  171. dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
  172. dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  173. dw/server/ui/assets/sql-NEE52Syq.js +1 -0
  174. dw/server/ui/assets/st-DbInun42.js +1 -0
  175. dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
  176. dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  177. dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
  178. dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
  179. dw/server/ui/assets/tsMode-D6u0XmOW.js +11 -0
  180. dw/server/ui/assets/twig-De2hgUGE.js +1 -0
  181. dw/server/ui/assets/typescript-BU6v-LMV.js +1 -0
  182. dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
  183. dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
  184. dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
  185. dw/server/ui/assets/workers-Cn7cTUKr.js +1 -0
  186. dw/server/ui/assets/xml--0LP2Lwk.js +1 -0
  187. dw/server/ui/assets/yaml-mpBg9jnt.js +1 -0
  188. dw/server/ui/index.html +17 -0
  189. dw/server/updater.py +192 -0
  190. dw/settings.py +98 -0
  191. dw/shot_span_preflight.py +116 -0
  192. dw/shots.py +359 -0
  193. dw/slice_preflight.py +148 -0
  194. dw/step.py +187 -0
  195. dw/step_cache.py +442 -0
  196. dw/subfolders.py +107 -0
  197. dw/task_domains.py +307 -0
  198. dw/tasks/assess.py +826 -0
  199. dw/tasks/audio_transcription.py +88 -0
  200. dw/tasks/audio_utils.py +1862 -0
  201. dw/tasks/background_remover.py +43 -0
  202. dw/tasks/borders.py +113 -0
  203. dw/tasks/compose_text.py +74 -0
  204. dw/tasks/concat_videos.py +300 -0
  205. dw/tasks/depth_estimator.py +54 -0
  206. dw/tasks/diffusion_upscale.py +109 -0
  207. dw/tasks/dissolve_videos.py +342 -0
  208. dw/tasks/format_messages.py +24 -0
  209. dw/tasks/gather.py +173 -0
  210. dw/tasks/grade.py +97 -0
  211. dw/tasks/image_to_text.py +43 -0
  212. dw/tasks/image_utils.py +764 -0
  213. dw/tasks/interpolate_frames.py +252 -0
  214. dw/tasks/judge.py +68 -0
  215. dw/tasks/model_cache.py +55 -0
  216. dw/tasks/pair_audio.py +268 -0
  217. dw/tasks/qr_code.py +19 -0
  218. dw/tasks/restore_faces.py +175 -0
  219. dw/tasks/rife_model.py +192 -0
  220. dw/tasks/segment.py +121 -0
  221. dw/tasks/select.py +111 -0
  222. dw/tasks/speech_generation.py +228 -0
  223. dw/tasks/stabilize.py +129 -0
  224. dw/tasks/task.py +920 -0
  225. dw/tasks/tensor_image.py +57 -0
  226. dw/tasks/text_generation.py +169 -0
  227. dw/tasks/text_sections.py +80 -0
  228. dw/tasks/upscale.py +203 -0
  229. dw/tasks/video_utils.py +624 -0
  230. dw/tasks/zoe_depth.py +71 -0
  231. dw/teacache.py +381 -0
  232. dw/teacache_models.json +99 -0
  233. dw/test.py +29 -0
  234. dw/type_helpers.py +231 -0
  235. dw/validate.py +68 -0
  236. dw/variable_constraints.py +444 -0
  237. dw/variables.py +443 -0
  238. dw/video_extensions.py +141 -0
  239. dw/vram_estimate.py +116 -0
  240. dw/worker.py +764 -0
  241. dw/workflow.py +2007 -0
  242. dw/workflow_schema.json +1346 -0
  243. dw/workflow_sources.py +383 -0
  244. dw/workflows/h3_context_ir.json +57 -0
  245. dw/workflows/test.json +31 -0
  246. dw/workspace.py +730 -0
  247. dw_mcp/__init__.py +6 -0
  248. dw_mcp/__main__.py +133 -0
  249. dw_mcp/assets.py +336 -0
  250. dw_mcp/authoring.py +114 -0
  251. dw_mcp/catalog.py +360 -0
  252. dw_mcp/client.py +486 -0
  253. dw_mcp/diagnose.py +371 -0
  254. dw_mcp/exports.py +84 -0
  255. dw_mcp/guides.py +35 -0
  256. dw_mcp/media.py +638 -0
  257. dw_mcp/models.py +97 -0
  258. dw_mcp/prompts.py +104 -0
  259. dw_mcp/server.py +1343 -0
  260. dw_mcp/workspaces.py +212 -0
dw/task_domains.py ADDED
@@ -0,0 +1,307 @@
1
+ """What a task command's numeric arguments are allowed to be.
2
+
3
+ A task command's arguments are whatever its implementation's signature takes
4
+ (`describe_task` in dw/introspection.py reads them off it), and the signature
5
+ carries no domain: nothing in `slice_audio(num_frames=...)` says a frame count
6
+ cannot be negative. So `validate_workflow` had nothing to check against, and a
7
+ number outside its domain reached the command - where, if the command happened
8
+ to guard it, the run failed on that step, and if it did not, Python's own
9
+ semantics answered instead. `num_frames: -10` became the Python slice
10
+ `audio[0:-13333]` and returned the track minus its last ten frames, reported
11
+ `succeeded` with no warnings (#139); `target_sample_rate: 0` fell through to a
12
+ 44100 Hz default and re-headered the samples unresampled, 38% off in duration
13
+ (#140).
14
+
15
+ The domains are declared here, once, and checked in two places for two
16
+ reasons: statically in `validation_errors`, so a literal out-of-domain number
17
+ is a free pre-flight error at the JSON path the author wrote it at rather than
18
+ a spent run; and at run time by the command itself, since a value that arrives
19
+ from a `variable:`, a `previous_result:` or a computation is not a literal and
20
+ this pass can never see it. The static pass therefore only ever refuses what
21
+ the command would refuse anyway - it is the earlier of two answers, not a
22
+ second opinion.
23
+
24
+ Only numbers whose domain is not a judgement call are listed. Whether a level
25
+ in dBFS or a gain is the *right* one for a mix is the command's business, and
26
+ a command that wants to refuse something subtler does it in its own body -
27
+ but a value's documented range is not a judgement call either: grade's
28
+ temperature and tint are only defined from -1.0 to 1.0, and a value outside
29
+ it is not a bolder version of the effect, just an unmodelled one (#349).
30
+ """
31
+
32
+ import logging
33
+ import numbers
34
+
35
+ from .for_each import MEMBER_SEPARATOR, render_path
36
+
37
+ logger = logging.getLogger("dw")
38
+
39
+ # Above zero, and zero-or-above. A count of frames to cut is the first kind -
40
+ # a zero-length slice is not a slice - and an offset to start at is the
41
+ # second, since the head of a track is a legitimate place to begin
42
+ POSITIVE = "positive"
43
+ NON_NEGATIVE = "non_negative"
44
+ # A level that cannot exceed full scale - peak_dbfs's own long-standing rule
45
+ # (0 is full scale, positive is not a level any of these commands can reach),
46
+ # shared here with normalize_audio's target_lufs
47
+ NON_POSITIVE = "non_positive"
48
+ # A value documented as a scale from -1.0 to 1.0 - grade's temperature and
49
+ # tint, whose linear interpolation is only defined inside that range; outside
50
+ # it the same formula still runs and produces a value, just not the one the
51
+ # documented scale promised
52
+ CLOSED_UNIT = "closed_unit"
53
+
54
+ _DOMAIN_TEXT = {
55
+ POSITIVE: "above zero",
56
+ NON_NEGATIVE: "zero or above",
57
+ NON_POSITIVE: "at or below full scale (0)",
58
+ CLOSED_UNIT: "between -1.0 and 1.0",
59
+ }
60
+
61
+ _DOMAIN_REASON = {
62
+ CLOSED_UNIT: (
63
+ "A value outside that range is refused rather than extrapolated - "
64
+ "the documented scale is only defined inside it"
65
+ ),
66
+ }
67
+ # Shared by POSITIVE, NON_NEGATIVE and NON_POSITIVE, which span both counts/
68
+ # rates (audio, frame) and multipliers (grade's contrast, saturation) - kept
69
+ # neutral rather than naming either, since a wording specific to one reads as
70
+ # nonsense on the other (#383)
71
+ _DEFAULT_REASON = (
72
+ "A value outside that range is refused rather than interpreted - it "
73
+ "would otherwise produce a plausible-looking result outside the "
74
+ "documented range"
75
+ )
76
+
77
+ # command -> argument -> domain. Every entry here is pinned to a real command
78
+ # and a real parameter of it by tests/test_task_domains.py, so a renamed
79
+ # argument cannot leave a domain checking nothing
80
+ TASK_ARGUMENT_DOMAINS = {
81
+ "slice_audio": {
82
+ "start_seconds": NON_NEGATIVE,
83
+ "duration_seconds": POSITIVE,
84
+ "start_frame": NON_NEGATIVE,
85
+ "num_frames": POSITIVE,
86
+ "fps": POSITIVE,
87
+ "sample_rate": POSITIVE,
88
+ },
89
+ "gain_audio": {
90
+ "start_seconds": NON_NEGATIVE,
91
+ "duration_seconds": POSITIVE,
92
+ "start_frame": NON_NEGATIVE,
93
+ "num_frames": POSITIVE,
94
+ "fps": POSITIVE,
95
+ "sample_rate": POSITIVE,
96
+ },
97
+ "resample_audio": {
98
+ "target_sample_rate": POSITIVE,
99
+ "sample_rate": POSITIVE,
100
+ },
101
+ "loop_audio": {
102
+ "duration_seconds": POSITIVE,
103
+ "target_frames": POSITIVE,
104
+ "fps": POSITIVE,
105
+ "crossfade_ms": NON_NEGATIVE,
106
+ "sample_rate": POSITIVE,
107
+ },
108
+ "fade_audio": {
109
+ "fade_in_ms": NON_NEGATIVE,
110
+ "fade_out_ms": NON_NEGATIVE,
111
+ "sample_rate": POSITIVE,
112
+ },
113
+ "normalize_audio": {
114
+ "sample_rate": POSITIVE,
115
+ "target_lufs": NON_POSITIVE,
116
+ },
117
+ "crossfade_audio": {
118
+ "crossfade_ms": NON_NEGATIVE,
119
+ "sample_rate": POSITIVE,
120
+ },
121
+ "mix_audio": {"gains": NON_NEGATIVE, "sample_rate": POSITIVE},
122
+ "pair_audio": {"sample_rate": POSITIVE},
123
+ "concat_videos": {
124
+ "trim_frames": NON_NEGATIVE,
125
+ "crossfade_ms": NON_NEGATIVE,
126
+ "audio_bleed_ms": NON_NEGATIVE,
127
+ "seam_fade_ms": NON_NEGATIVE,
128
+ "fps": POSITIVE,
129
+ "sample_rate": POSITIVE,
130
+ },
131
+ "loop_frames": {"num_frames": POSITIVE},
132
+ "frame_grid": {"count": POSITIVE, "columns": POSITIVE, "tile_width": POSITIVE},
133
+ "dissolve_videos": {
134
+ "dissolve_frames": NON_NEGATIVE,
135
+ "fade_in_frames": NON_NEGATIVE,
136
+ "fade_out_frames": NON_NEGATIVE,
137
+ "fps": POSITIVE,
138
+ },
139
+ "compress_audio": {
140
+ "ratio": POSITIVE,
141
+ "attack_ms": NON_NEGATIVE,
142
+ "release_ms": NON_NEGATIVE,
143
+ "sample_rate": POSITIVE,
144
+ },
145
+ "filter_audio": {
146
+ "cutoff_hz": POSITIVE,
147
+ "sample_rate": POSITIVE,
148
+ },
149
+ "analyze_audio": {"sample_rate": POSITIVE},
150
+ "grade": {
151
+ "contrast": NON_NEGATIVE,
152
+ "saturation": NON_NEGATIVE,
153
+ "temperature": CLOSED_UNIT,
154
+ "tint": CLOSED_UNIT,
155
+ },
156
+ }
157
+
158
+
159
+ def in_domain(value, domain):
160
+ """Whether a number satisfies a domain. Anything unmeasurable is True -
161
+ a value this cannot read is not this check's to refuse."""
162
+ number = as_number(value)
163
+ if number is None:
164
+ return True
165
+ if domain == POSITIVE:
166
+ return number > 0
167
+ if domain == NON_POSITIVE:
168
+ return number <= 0
169
+ if domain == CLOSED_UNIT:
170
+ return -1.0 <= number <= 1.0
171
+ return number >= 0
172
+
173
+
174
+ def as_number(value):
175
+ """A value as a float if it is one, else None.
176
+
177
+ A workflow variable declared null carries no type, so a number supplied
178
+ for it on the command line arrives as a string - the tasks coerce those
179
+ (`_as_number` in audio_utils), so this reads them too. Booleans are not
180
+ numbers here whatever Python thinks, and a `variable:`/`item:`/
181
+ `previous_result:` string is somebody else's complaint.
182
+ """
183
+ if isinstance(value, bool) or value is None:
184
+ return None
185
+ # numbers.Real rather than (int, float): an fps is coerced to a Fraction
186
+ # before it is checked, so an exact 24000/1001 stays exact
187
+ if isinstance(value, numbers.Real):
188
+ return float(value)
189
+ if isinstance(value, str):
190
+ try:
191
+ return float(value)
192
+ except ValueError:
193
+ return None
194
+ return None
195
+
196
+
197
+ def _domain_candidates(value):
198
+ """(index, item) pairs to check - one per element of a list argument
199
+ (mix_audio's 'gains', one multiplier per track), else the value itself
200
+ paired with no index."""
201
+ if isinstance(value, list):
202
+ return list(enumerate(value))
203
+ return [(None, value)]
204
+
205
+
206
+ def domain_violation(command, name, value, domain):
207
+ """(index, message) for the first out-of-domain element, or None if all
208
+ are fine. index is None when the argument itself is the scalar checked
209
+ rather than one entry of a list.
210
+
211
+ Shared by the static pass and the commands' own run-time guards so the
212
+ two cannot word the same refusal differently.
213
+ """
214
+ for index, item in _domain_candidates(value):
215
+ if in_domain(item, domain):
216
+ continue
217
+ label = f"{name}[{index}]" if index is not None else name
218
+ reason = _DOMAIN_REASON.get(domain, _DEFAULT_REASON)
219
+ message = (
220
+ f"{command} needs '{label}' {_DOMAIN_TEXT[domain]}, got {item!r}. {reason}"
221
+ )
222
+ return index, message
223
+ return None
224
+
225
+
226
+ def domain_error(command, name, value, domain):
227
+ """The message for one out-of-domain argument, or None if it is fine."""
228
+ violation = domain_violation(command, name, value, domain)
229
+ return None if violation is None else violation[1]
230
+
231
+
232
+ def check_argument(command, name, value):
233
+ """Raise ValueError if a task argument is outside its declared domain.
234
+
235
+ What a command calls on the value it was actually handed, after its own
236
+ coercion - the value may have come from a variable or an earlier step,
237
+ which the static pass never sees.
238
+ """
239
+ domain = TASK_ARGUMENT_DOMAINS.get(command, {}).get(name)
240
+ if domain is None:
241
+ return
242
+ message = domain_error(command, name, value, domain)
243
+ if message is not None:
244
+ raise ValueError(message)
245
+
246
+
247
+ def check_arguments(command, **values):
248
+ """check_argument over several of a command's arguments, in order."""
249
+ for name, value in values.items():
250
+ check_argument(command, name, value)
251
+
252
+
253
+ def task_argument_errors(workflow_definition, source_indices=None):
254
+ """Every task argument outside its declared domain, as [{path, message}].
255
+
256
+ The definition handed here has already been substituted and expanded, so
257
+ a `for_each` member's own arguments are checked as they will run;
258
+ `source_indices` maps each expanded step back to the step the author
259
+ wrote, and the member is named in the message - the convention
260
+ subfolder_errors and reference_limit_errors both follow.
261
+ """
262
+ steps = workflow_definition.get("steps")
263
+ if not isinstance(steps, list):
264
+ return []
265
+
266
+ errors = []
267
+ for index, step in enumerate(steps):
268
+ if not isinstance(step, dict):
269
+ continue
270
+ task = step.get("task")
271
+ if not isinstance(task, dict):
272
+ continue
273
+ command = task.get("command")
274
+ arguments = task.get("arguments")
275
+ if not isinstance(command, str) or not isinstance(arguments, dict):
276
+ continue
277
+ domains = TASK_ARGUMENT_DOMAINS.get(command)
278
+ if not domains:
279
+ continue
280
+ source = (
281
+ source_indices[index]
282
+ if source_indices is not None and index < len(source_indices)
283
+ else index
284
+ )
285
+ name = step.get("name")
286
+ where = (
287
+ f" in member '{name}'"
288
+ if isinstance(name, str) and MEMBER_SEPARATOR in name
289
+ else ""
290
+ )
291
+ for key, domain in domains.items():
292
+ if key not in arguments:
293
+ continue
294
+ violation = domain_violation(command, key, arguments[key], domain)
295
+ if violation is None:
296
+ continue
297
+ element_index, message = violation
298
+ path = ("steps", source, "task", "arguments", key)
299
+ if element_index is not None:
300
+ path = path + (element_index,)
301
+ errors.append(
302
+ {
303
+ "path": render_path(path),
304
+ "message": f"{message}{where}.",
305
+ }
306
+ )
307
+ return errors