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/assessment_rules.py ADDED
@@ -0,0 +1,159 @@
1
+ """The rules the assessment probes read their measurements against (#387).
2
+
3
+ A probe (`dw/tasks/assess.py`) measures a finished file and reports every
4
+ number it took; a rule here names one of those numbers and the threshold
5
+ past which it is worth a look, and a crossing becomes a `finding` in the
6
+ probe's answer. Findings are places to look, not verdicts: nothing in the
7
+ engine acts on one, no run fails for one, and a finding a person has looked
8
+ at and accepted is simply left alone. That is the authority rule, and it is
9
+ why the thresholds sit in one table - a number an agent is told to trust
10
+ has to be one somebody can find, argue with and change in one place.
11
+
12
+ Each entry names the probe that reports the field, the field (a key of the
13
+ probe's per-shot or per-seam record, or of its answer itself -
14
+ `tests/test_assessment_rules.py` pins every one to a real probe field so a
15
+ rename cannot leave a rule reading nothing), how the value is compared,
16
+ the threshold, the severity and what a crossing says. `magnitude` compares
17
+ the value's absolute size, for a signed measurement whose direction is not
18
+ the problem. `shot_level_spread` shares its threshold with the run-time
19
+ `LEVEL_SPREAD_WARN_DB`, so the join-time warning and the after-the-fact
20
+ probe cannot disagree about the same cut.
21
+
22
+ Two rules carry a guard the table alone cannot express, applied by the
23
+ probe and named here in `unless` so it is written down beside the number:
24
+ `seam_hole` holds only while both sides of the seam are voiced, and
25
+ `seam_frame_jump` does not fire at a seam whose incoming shot is marked
26
+ `hard_cut: true` - a cut meant as a cut.
27
+
28
+ The thresholds were settled against real runs on the server in stage D
29
+ (#386); `docs/WORKFLOW_GUIDE.md` and `docs/TASKS.md` quote every one, and
30
+ `tests/test_assessment_rules.py` pins those quotes to this table.
31
+ """
32
+
33
+ from .tasks.audio_utils import LEVEL_SPREAD_WARN_DB
34
+
35
+ SEVERITIES = ("info", "warn")
36
+
37
+ # How loud both sides of a seam have to be for a quiet join to be a hole
38
+ # rather than a pause the shots themselves hold
39
+ HOLE_VOICED_DBFS = -30.0
40
+
41
+ COMPARATORS = {
42
+ ">": lambda value, threshold: value > threshold,
43
+ ">=": lambda value, threshold: value >= threshold,
44
+ "<": lambda value, threshold: value < threshold,
45
+ }
46
+
47
+ RULES = (
48
+ {
49
+ "name": "shot_level_spread",
50
+ "probe": "analyze_shots",
51
+ "field": "rms_range_db",
52
+ "comparator": ">=",
53
+ "threshold": LEVEL_SPREAD_WARN_DB,
54
+ "severity": "warn",
55
+ "says": "the shots sit this far apart in level - a jump a listener hears at the cut",
56
+ },
57
+ {
58
+ "name": "seam_level_step",
59
+ "probe": "analyze_seams",
60
+ "field": "level_step_db",
61
+ "comparator": ">",
62
+ "threshold": 3.0,
63
+ "severity": "warn",
64
+ "says": "the shots either side of the seam sit this far apart in level",
65
+ },
66
+ {
67
+ "name": "seam_click",
68
+ "probe": "analyze_seams",
69
+ "field": "click_db",
70
+ "comparator": ">",
71
+ "threshold": 12.0,
72
+ "severity": "warn",
73
+ "says": "the join peaks this far above the audio either side of it - an audible click",
74
+ },
75
+ {
76
+ "name": "seam_hole",
77
+ "probe": "analyze_seams",
78
+ "field": "floor_dbfs",
79
+ "comparator": "<",
80
+ "threshold": -50.0,
81
+ "severity": "warn",
82
+ "says": "the track drops out at the join while both sides are voiced",
83
+ "unless": f"either side's rms is at or below {HOLE_VOICED_DBFS} dBFS",
84
+ },
85
+ {
86
+ "name": "seam_frame_jump",
87
+ "probe": "analyze_seams",
88
+ "field": "jump_ratio",
89
+ "comparator": ">",
90
+ # Above every intended cut the stage D field runs measured (#386):
91
+ # seven ordinary cuts between generated shots read 9.9-20.3, dissolves
92
+ # about 1.8. At the original 8 the rule fired on every cut
93
+ "threshold": 25.0,
94
+ "severity": "info",
95
+ "says": "the picture changes this many times more across the seam than inside either shot",
96
+ "unless": "the incoming shot is marked hard_cut: true",
97
+ },
98
+ {
99
+ "name": "sync_drift",
100
+ "probe": "analyze_sync_drift",
101
+ "field": "end_offset_ms",
102
+ "comparator": ">",
103
+ "threshold": 40.0,
104
+ "severity": "warn",
105
+ "magnitude": True,
106
+ "says": "by this shot's end the audio sits this far off the picture",
107
+ },
108
+ {
109
+ "name": "sync_length",
110
+ "probe": "analyze_sync_drift",
111
+ "field": "length_delta_ms",
112
+ "comparator": ">",
113
+ "threshold": 40.0,
114
+ "severity": "warn",
115
+ "magnitude": True,
116
+ "says": "the soundtrack and the picture differ in length by this much",
117
+ },
118
+ )
119
+
120
+ RULES_BY_NAME = {rule["name"]: rule for rule in RULES}
121
+
122
+
123
+ def rules_for(probe):
124
+ """The rules one probe's measurements are read against, in table order."""
125
+ return [rule for rule in RULES if rule["probe"] == probe]
126
+
127
+
128
+ def crosses(rule, value):
129
+ """Whether a measured value crosses a rule's threshold. None never does -
130
+ a measurement that could not be taken (a silent window, no soundtrack)
131
+ is not a finding."""
132
+ if value is None:
133
+ return False
134
+ if rule.get("magnitude"):
135
+ value = abs(value)
136
+ return COMPARATORS[rule["comparator"]](value, rule["threshold"])
137
+
138
+
139
+ def finding(rule, value, at):
140
+ """A crossing as the probe reports it."""
141
+ return {
142
+ "rule": rule["name"],
143
+ "severity": rule["severity"],
144
+ "at": at,
145
+ "value": value,
146
+ "threshold": rule["threshold"],
147
+ "says": rule["says"],
148
+ }
149
+
150
+
151
+ __all__ = [
152
+ "HOLE_VOICED_DBFS",
153
+ "RULES",
154
+ "RULES_BY_NAME",
155
+ "SEVERITIES",
156
+ "crosses",
157
+ "finding",
158
+ "rules_for",
159
+ ]
dw/assets.py ADDED
@@ -0,0 +1,130 @@
1
+ """The asset library: input media a workflow references by name.
2
+
3
+ A workflow's media paths resolve against the workflow file's own directory,
4
+ which means a workflow that reads anything has to keep that thing beside it -
5
+ the reason generated media ends up gitignored inside a source tree. An
6
+ 'asset:name' reference is rooted at the asset library instead, the way
7
+ 'prompt:name' is rooted at the prompt library, so the same reference means the
8
+ same file from every workflow and neither has to live next to the other.
9
+
10
+ A reference resolves to a path, not to a value: 'asset:frames/iris.jpg'
11
+ becomes the absolute path of that file, and whatever would have loaded a path
12
+ written there loads it unchanged.
13
+ """
14
+
15
+ import contextvars
16
+ import logging
17
+ import os
18
+
19
+ from .security import validate_asset_reference, validate_path
20
+ from .workspace import ASSETS_SUBDIR, discover_library, library_fallbacks
21
+
22
+ logger = logging.getLogger("dw")
23
+
24
+ # The prefix marking a value as a reference to a stored asset
25
+ ASSET_PREFIX = "asset:"
26
+
27
+ # Set by an entry point from --asset-dir, and inherited by a spawned worker,
28
+ # the way DW_PROMPT_DIR is
29
+ ASSET_DIR_ENV_VAR = "DW_ASSET_DIR"
30
+
31
+
32
+ # The asset library of the run in progress. A server holds several
33
+ # workspaces and each has its own assets, so this cannot be a process-wide
34
+ # environment variable there the way the prompt library can - there is one
35
+ # prompt library, shared, but assets belong to a workspace. Set per job by
36
+ # the worker; unset for the CLI and REPL, which have one workspace per
37
+ # process and read the environment below
38
+ _active_asset_dir = contextvars.ContextVar("dw_asset_dir", default=None)
39
+
40
+
41
+ def activate_asset_dir(directory):
42
+ """Make an asset library the active one; returns a token for deactivate."""
43
+ return _active_asset_dir.set(directory)
44
+
45
+
46
+ def deactivate_asset_dir(token):
47
+ _active_asset_dir.reset(token)
48
+
49
+
50
+ def get_asset_dir(base_dir=None):
51
+ """The directory 'asset:' references are rooted at.
52
+
53
+ A library activated for this run wins outright - that is the server
54
+ telling the worker which workspace's assets this job uses. Otherwise
55
+ discovery mirrors the prompt library's - see workspace.discover_library
56
+ for the shared precedence (DW_ASSET_DIR, then a named workspace, then
57
+ ./assets, then a walk up from base_dir, then the workspace's assets/ as
58
+ the fallback).
59
+
60
+ Args:
61
+ base_dir: The workflow file's directory, when one anchors the search
62
+ """
63
+ active = _active_asset_dir.get()
64
+ if active:
65
+ return active
66
+
67
+ return discover_library(ASSETS_SUBDIR, ASSET_DIR_ENV_VAR, base_dir)
68
+
69
+
70
+ def is_asset_reference(value):
71
+ """Whether a value references a file in the asset library."""
72
+ return isinstance(value, str) and value.startswith(ASSET_PREFIX)
73
+
74
+
75
+ def asset_search_path(asset_dir=None, base_dir=None):
76
+ """Every directory an 'asset:' reference is looked for in, in order.
77
+
78
+ The workspace's own library first, then the read-only ones an entry
79
+ point put on the path (workspace.library_fallbacks - the assets a
80
+ --examples-dir tree brings with it), so an example workflow reaches the
81
+ media it ships with while an upload still lands in the workspace.
82
+
83
+ Args:
84
+ asset_dir: The first directory; defaults to get_asset_dir()
85
+ base_dir: The workflow file's directory, anchoring discovery when no
86
+ asset directory is configured
87
+ """
88
+ primary = asset_dir or get_asset_dir(base_dir)
89
+ return [primary] + library_fallbacks(ASSETS_SUBDIR, primary)
90
+
91
+
92
+ def resolve_asset_reference(reference, asset_dir=None, base_dir=None):
93
+ """Resolve an 'asset:' reference to the file it names.
94
+
95
+ Args:
96
+ reference: The 'asset:name.ext' or 'asset:folder/name.ext' string
97
+ asset_dir: Directory the name is rooted at; defaults to get_asset_dir()
98
+ base_dir: The workflow file's directory, anchoring discovery when no
99
+ asset directory is configured
100
+
101
+ Returns:
102
+ The validated absolute path of the asset file
103
+
104
+ Raises:
105
+ InvalidInputError: If the name is not a valid asset name
106
+ PathTraversalError: If the name escapes the asset directory
107
+ ValueError: If no file exists under that name in any directory on
108
+ the search path
109
+ """
110
+ name = validate_asset_reference(reference.removeprefix(ASSET_PREFIX).strip())
111
+ roots = asset_search_path(asset_dir, base_dir)
112
+ for root in roots:
113
+ # Confined to the library it was found in: the name is joined onto a
114
+ # directory, so the containment check is what makes a name a name
115
+ # rather than a path
116
+ path = validate_path(os.path.join(root, name), root)
117
+ if os.path.isfile(path):
118
+ logger.debug(f"Resolved {reference} to {path}")
119
+ return path
120
+ searched = ", ".join(roots)
121
+ raise ValueError(
122
+ f"Asset '{name}' not found in {searched} - an 'asset:' reference "
123
+ f"names a file in the asset library, with its extension, like "
124
+ f"'asset:iris.jpg' or 'asset:gyre/frame_1.jpg'"
125
+ )
126
+
127
+
128
+ def fetch_asset(reference, asset_dir=None, base_dir=None):
129
+ """The path an 'asset:' reference names, for whatever loads paths."""
130
+ return resolve_asset_reference(reference, asset_dir, base_dir)
dw/cache_blocks.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "$comment": "Transformer block metadata for diffusers' cache hooks. first_block, mag and layer_skip look their block class up in diffusers.hooks._helpers.TransformerBlockRegistry and raise if it is missing. Blocks listed here are registered on demand so those cache types work on models diffusers has not registered upstream. Fields mirror diffusers.hooks._helpers.TransformerBlockMetadata, plus dw's own encoder_hidden_states_argument_name. Verify against the block's forward before adding an entry: return_hidden_states_index is the position of hidden_states in the returned tuple, and is ignored when the block returns a bare tensor; return_encoder_hidden_states_index is null when the block does not return a second stream; hidden_states_argument_name must name the forward parameter carrying hidden_states; encoder_hidden_states_argument_name names the forward parameter the second returned stream comes from, and is only needed when that is not literally 'encoder_hidden_states' - see dw/cache_blocks.py.",
3
+ "blocks": {
4
+ "diffusers.models.transformers.transformer_minimax_h3.MiniMaxH3TransformerBlock": {
5
+ "return_hidden_states_index": 0,
6
+ "return_encoder_hidden_states_index": null,
7
+ "hidden_states_argument_name": "hidden_states"
8
+ },
9
+ "diffusers.models.transformers.transformer_ltx2.LTX2VideoTransformerBlock": {
10
+ "return_hidden_states_index": 0,
11
+ "return_encoder_hidden_states_index": 1,
12
+ "hidden_states_argument_name": "hidden_states",
13
+ "encoder_hidden_states_argument_name": "audio_hidden_states"
14
+ }
15
+ }
16
+ }
dw/cache_blocks.py ADDED
@@ -0,0 +1,146 @@
1
+ """Register transformer block metadata diffusers' cache hooks are missing.
2
+
3
+ The first_block, mag and layer_skip cache types look a model's transformer block
4
+ class up in diffusers.hooks._helpers.TransformerBlockRegistry and raise
5
+ ValueError when it is absent, so those caches are unavailable on any model
6
+ diffusers has not registered upstream - even though the model itself supports
7
+ enable_cache(). The blocks in cache_blocks.json fill that gap; entries become
8
+ redundant, not wrong, once diffusers registers the same class itself.
9
+ """
10
+
11
+ import json
12
+ import logging
13
+ from pathlib import Path
14
+
15
+ from .type_helpers import load_type_from_name
16
+
17
+ logger = logging.getLogger("dw")
18
+
19
+
20
+ _REGISTRY_PATH = Path(__file__).parent / "cache_blocks.json"
21
+
22
+ # Registration walks every entry, so do it once rather than per pipeline load
23
+ _registered = False
24
+
25
+ # The registry field that is dw's own rather than one of TransformerBlockMetadata's -
26
+ # see _dual_stream_metadata_class for what it is for
27
+ ARGUMENT_REMAP_KEY = "encoder_hidden_states_argument_name"
28
+
29
+ # One subclass per metadata class, built on first use - the class it derives from
30
+ # only exists once diffusers has been imported
31
+ _dual_stream_classes = {}
32
+
33
+
34
+ def _load_registry():
35
+ """Load the block metadata registry from the JSON file."""
36
+ with open(_REGISTRY_PATH) as f:
37
+ return json.load(f).get("blocks", {})
38
+
39
+
40
+ def _dual_stream_metadata_class(metadata_class):
41
+ """A metadata class that can name the block's second stream itself.
42
+
43
+ A block that returns two streams has the second one read back out of its
44
+ forward arguments when the cache skips it, and diffusers looks that argument
45
+ up under the fixed name 'encoder_hidden_states' - the only two-stream shape it
46
+ registers upstream is text beside image. LTX-2's blocks return video beside
47
+ *audio* while also taking an 'encoder_hidden_states' of their own (the text
48
+ conditioning), so the fixed name silently reads the wrong tensor and feeds the
49
+ text embeddings back as the audio stream. Remapping the identifier is what
50
+ makes first_block caching correct on those blocks rather than merely quiet.
51
+
52
+ Args:
53
+ metadata_class: diffusers' TransformerBlockMetadata
54
+
55
+ Returns:
56
+ A subclass reading the second stream from the argument the entry names
57
+ """
58
+ if metadata_class not in _dual_stream_classes:
59
+
60
+ class DualStreamMetadata(metadata_class):
61
+ # Overridden per instance from the registry entry; the default keeps
62
+ # the class behaving exactly like the one it derives from
63
+ encoder_hidden_states_argument_name = "encoder_hidden_states"
64
+
65
+ def _get_parameter_from_args_kwargs(self, identifier, args=(), kwargs=None):
66
+ if identifier == "encoder_hidden_states":
67
+ identifier = self.encoder_hidden_states_argument_name
68
+ return super()._get_parameter_from_args_kwargs(identifier, args, kwargs)
69
+
70
+ _dual_stream_classes[metadata_class] = DualStreamMetadata
71
+
72
+ return _dual_stream_classes[metadata_class]
73
+
74
+
75
+ def build_metadata(metadata_class, entry):
76
+ """Build the metadata object one registry entry describes.
77
+
78
+ Args:
79
+ metadata_class: diffusers' TransformerBlockMetadata
80
+ entry: One block's fields from cache_blocks.json
81
+
82
+ Returns:
83
+ An instance of metadata_class, or of the dual-stream subclass when the
84
+ entry names the argument its second returned stream comes from
85
+ """
86
+ remapped = entry.get(ARGUMENT_REMAP_KEY)
87
+ fields = {k: v for k, v in entry.items() if k != ARGUMENT_REMAP_KEY}
88
+
89
+ if remapped is None:
90
+ return metadata_class(**fields)
91
+
92
+ metadata = _dual_stream_metadata_class(metadata_class)(**fields)
93
+ metadata.encoder_hidden_states_argument_name = remapped
94
+ return metadata
95
+
96
+
97
+ def register_cache_blocks():
98
+ """Register any known-missing transformer blocks with diffusers.
99
+
100
+ Safe to call repeatedly and before any cache type - blocks diffusers already
101
+ knows are left alone, and a block whose class the installed diffusers does
102
+ not have is skipped rather than raising.
103
+ """
104
+ global _registered
105
+ if _registered:
106
+ return
107
+
108
+ try:
109
+ from diffusers.hooks._helpers import (
110
+ TransformerBlockMetadata,
111
+ TransformerBlockRegistry,
112
+ )
113
+ except ImportError:
114
+ # A diffusers without the cache hooks has nothing to register against
115
+ logger.debug(
116
+ "diffusers cache hook helpers unavailable, skipping block registration"
117
+ )
118
+ _registered = True
119
+ return
120
+
121
+ for class_name, metadata in _load_registry().items():
122
+ try:
123
+ # A server-owned registry name, registered, never constructed
124
+ block_class = load_type_from_name(class_name, constructed=False)
125
+ except (ImportError, AttributeError):
126
+ # The installed diffusers predates this model - its blocks are not
127
+ # missing from the registry, they do not exist
128
+ logger.debug(f"Cache block '{class_name}' not in this diffusers, skipping")
129
+ continue
130
+
131
+ # TransformerBlockRegistry.get raises rather than returning None, and an
132
+ # upstream registration is the more authoritative one either way
133
+ try:
134
+ TransformerBlockRegistry.get(block_class)
135
+ logger.debug(f"Cache block '{class_name}' already registered, leaving it")
136
+ continue
137
+ except ValueError:
138
+ pass
139
+
140
+ logger.debug(f"Registering cache block metadata for '{class_name}'")
141
+ TransformerBlockRegistry.register(
142
+ model_class=block_class,
143
+ metadata=build_metadata(TransformerBlockMetadata, metadata),
144
+ )
145
+
146
+ _registered = True