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/workflow_sources.py ADDED
@@ -0,0 +1,383 @@
1
+ """Where workflows are read from, and the one place they are written to.
2
+
3
+ A workflow directory used to be a single directory that was both the library
4
+ and the place saves landed. With the repository's own workflows/ as that
5
+ directory - the default when a checkout is the workspace - every save from
6
+ the editor or an MCP client wrote into the example corpus.
7
+
8
+ A search path separates the two. Reads resolve front to back; writes only
9
+ ever go to the front:
10
+
11
+ <workspace>/workflows/ the user's own, writable
12
+ <examples dirs> read-only, --examples-dir
13
+
14
+ A name found in an earlier source shadows the same name in a later one, so a
15
+ workspace copy of an example is the one that runs. Saving over a read-only
16
+ workflow is not an error and not an overwrite: it writes a copy into the
17
+ writable source, which is what "open an example, change it, save" should do.
18
+ """
19
+
20
+ import logging
21
+ import os
22
+
23
+ from .security import PathTraversalError, SecurityError, contained, validate_path
24
+
25
+ logger = logging.getLogger("dw")
26
+
27
+ # What a source is, for a client deciding whether to offer save or delete
28
+ WORKSPACE_ORIGIN = "workspace"
29
+ EXAMPLES_ORIGIN = "examples"
30
+ BUILTIN_ORIGIN = "builtin"
31
+ # Not a workflow source - the asset library shared by every workspace under
32
+ # one root reports itself this way, so a client can tell a shared asset from
33
+ # one of its own
34
+ COMMON_ORIGIN = "common"
35
+
36
+
37
+ def builtin_root():
38
+ """The packaged workflows that ship inside dw/ - what 'builtin:' names."""
39
+ return os.path.join(os.path.dirname(os.path.abspath(__file__)), "workflows")
40
+
41
+
42
+ def catalog_root(directory):
43
+ """The nearest ancestor of `directory` literally named 'workflows', else
44
+ the directory itself.
45
+
46
+ The root a run with no workflow_dir of its own confines a relative
47
+ sub-workflow reference to, so a template under templates/ can still climb
48
+ to a sibling models/ without leaving the catalog. `catalog_root_dir`
49
+ (dw/workflow.py) is this rule asked for a file rather than a directory.
50
+ """
51
+ directory = os.path.normpath(os.path.abspath(directory))
52
+ parts = directory.split(os.sep)
53
+ try:
54
+ index = len(parts) - 1 - parts[::-1].index("workflows")
55
+ except ValueError:
56
+ return directory
57
+
58
+ return os.sep.join(parts[: index + 1])
59
+
60
+
61
+ class WorkflowSource:
62
+ """One root on the search path."""
63
+
64
+ def __init__(self, root, origin, writable):
65
+ self.root = os.path.abspath(os.path.expanduser(str(root)))
66
+ self.origin = origin
67
+ self.writable = writable
68
+
69
+ def contains(self, path):
70
+ """Whether a path resolves inside this root - the containment check
71
+ the security layer already implements, asked as a question rather
72
+ than raised as an error."""
73
+ try:
74
+ validate_path(path, self.root)
75
+ return True
76
+ except SecurityError:
77
+ return False
78
+
79
+ def names(self):
80
+ """Workflow names under this root, as relative paths without .json."""
81
+ return workflow_names(self.root)
82
+
83
+ def to_dict(self):
84
+ return {"root": self.root, "origin": self.origin, "writable": self.writable}
85
+
86
+ def __repr__(self):
87
+ return f"WorkflowSource({self.root!r}, {self.origin}, writable={self.writable})"
88
+
89
+
90
+ def workflow_names(root):
91
+ """Workflow names under a root, as relative paths without .json.
92
+
93
+ A file symlink resolving outside the root is not a name here: a listing
94
+ opens every file it names, and reads by name already refuse the link."""
95
+ names = []
96
+ if not os.path.isdir(root):
97
+ return names
98
+ for directory, _dirs, files in os.walk(root):
99
+ for file_name in files:
100
+ if file_name.endswith(".json"):
101
+ path = os.path.join(directory, file_name)
102
+ if not contained(path, root):
103
+ continue
104
+ relative = os.path.relpath(path, root)
105
+ names.append(relative[: -len(".json")].replace(os.sep, "/"))
106
+ return sorted(names)
107
+
108
+
109
+ def workflow_sources(workflow_dir, examples_dirs=None, include_builtin=False):
110
+ """The search path: the writable directory first, then read-only roots.
111
+
112
+ A read-only root that is the writable one - a checkout whose workflows/
113
+ is both the workspace library and the examples - appears once, writable,
114
+ rather than twice with two different answers about whether it can be
115
+ saved to.
116
+
117
+ The packaged workflows are off the path by default. They are the pieces
118
+ a 'builtin:' sub-workflow step names, resolved by the engine where that
119
+ step is read (dw/workflow.py) - not workflows anyone browses or runs on
120
+ their own, and listing them would put a handful of fragments in front of
121
+ every user who never asked for them.
122
+ """
123
+ sources = [WorkflowSource(workflow_dir, WORKSPACE_ORIGIN, True)]
124
+ candidates = [(directory, EXAMPLES_ORIGIN) for directory in examples_dirs or []]
125
+ if include_builtin:
126
+ candidates.append((builtin_root(), BUILTIN_ORIGIN))
127
+
128
+ seen = {sources[0].root}
129
+ for root, origin in candidates:
130
+ source = WorkflowSource(root, origin, False)
131
+ if source.root in seen:
132
+ continue
133
+ seen.add(source.root)
134
+ sources.append(source)
135
+ return sources
136
+
137
+
138
+ def writable_source(sources):
139
+ """The source saves go to: the front of the path."""
140
+ for source in sources:
141
+ if source.writable:
142
+ return source
143
+ return None
144
+
145
+
146
+ def source_for_path(sources, path):
147
+ """Which source a resolved path belongs to, or None if it is outside
148
+ every root - which is what makes a path a workflow rather than an
149
+ arbitrary file."""
150
+ for source in sources:
151
+ if source.contains(path):
152
+ return source
153
+ return None
154
+
155
+
156
+ def resolve_in_source(source, name, allow_create=False):
157
+ """The on-disk path a name has in one source, or None when the name does
158
+ not resolve inside it. Containment is the security layer's, so a name
159
+ that tries to traverse simply does not resolve."""
160
+ if not name.endswith(".json"):
161
+ name = f"{name}.json"
162
+ try:
163
+ return validate_path(
164
+ os.path.join(source.root, name), source.root, allow_create=allow_create
165
+ )
166
+ except SecurityError:
167
+ return None
168
+
169
+
170
+ def find_workflow(sources, name):
171
+ """The first source that has this name, as (path, source).
172
+
173
+ Front to back, so a workspace copy shadows the example it was copied
174
+ from. (None, None) when no source has it.
175
+ """
176
+ for source in sources:
177
+ path = resolve_in_source(source, name)
178
+ if path and os.path.isfile(path):
179
+ return path, source
180
+ return None, None
181
+
182
+
183
+ def suggest_workflow_names(sources, name, limit=3):
184
+ """Catalog names an unresolved `name` might have meant, for an error
185
+ message rather than a second round trip.
186
+
187
+ The catalog is organised in directories (`templates/minimax/dialogue-short`)
188
+ and a caller - a skill, an earlier turn - often has only the trailing
189
+ name (`dialogue-short`). Preferred answer: every catalog entry `name` is
190
+ a unique path suffix of, since that is unambiguous; failing that, a
191
+ close spelling match (`difflib`), for a typo rather than a shortened
192
+ path. Empty when neither finds anything worth naming.
193
+ """
194
+ import difflib
195
+
196
+ stripped = name[: -len(".json")] if name.endswith(".json") else name
197
+ catalog_names = list(listing(sources).keys())
198
+ suffix_matches = [
199
+ candidate
200
+ for candidate in catalog_names
201
+ if candidate == stripped or candidate.endswith(f"/{stripped}")
202
+ ]
203
+ if suffix_matches:
204
+ return suffix_matches[:limit]
205
+
206
+ # A typo is measured against the catalog entry's own name, not against
207
+ # its directory prefix - "dialog-short" scores 0.92 against
208
+ # "dialogue-short" and 0.55 against "templates/minimax/dialogue-short",
209
+ # so comparing full paths lets a real typo miss the cutoff (#397). The
210
+ # query's own prefix is stripped the same way, so "sub/Basik" is
211
+ # measured as "Basik" against "Basic" rather than against "sub/Basic".
212
+ query_basename = stripped.rsplit("/", 1)[-1]
213
+ by_basename = {}
214
+ for candidate in catalog_names:
215
+ by_basename.setdefault(candidate.rsplit("/", 1)[-1], []).append(candidate)
216
+ close_bases = difflib.get_close_matches(
217
+ query_basename, list(by_basename.keys()), n=limit, cutoff=0.6
218
+ )
219
+ matches = []
220
+ for base in close_bases:
221
+ for candidate in by_basename[base]:
222
+ if candidate not in matches:
223
+ matches.append(candidate)
224
+ return matches[:limit]
225
+
226
+
227
+ def listing(sources):
228
+ """Every name the search path offers, each with the source it comes
229
+ from - a name in an earlier source shadowing the same name later."""
230
+ found = {}
231
+ for source in sources:
232
+ for name in source.names():
233
+ found.setdefault(name, source)
234
+ return dict(sorted(found.items()))
235
+
236
+
237
+ def fallback_roots(primary=None):
238
+ """The read-only workflow roots a sub-workflow name is resolved against
239
+ after the directory the run is confined to.
240
+
241
+ Pinned in DW_WORKFLOW_PATH by dw.serve, the same way the prompt and
242
+ asset libraries are, so the worker resolves a composed template exactly
243
+ as the API would.
244
+ """
245
+ from .workspace import WORKFLOWS_SUBDIR, library_fallbacks
246
+
247
+ return library_fallbacks(WORKFLOWS_SUBDIR, primary)
248
+
249
+
250
+ def _candidate_names(name):
251
+ """A name as written, and with .json appended when it has no extension -
252
+ the catalog reports names without it, and run_workflow's workflow_path
253
+ takes either (#90)."""
254
+ names = [name]
255
+ if not name.endswith(".json"):
256
+ names.append(f"{name}.json")
257
+ return names
258
+
259
+
260
+ class SubWorkflowNotFound(Exception):
261
+ """A sub-workflow step's path names nothing on the search path."""
262
+
263
+ def __init__(self, path, tried):
264
+ self.path = path
265
+ # In order, each candidate once - two roots can resolve one name to
266
+ # the same file, and saying so twice reads as two failures
267
+ self.tried = list(dict.fromkeys(tried))
268
+ detail = "\n ".join(self.tried)
269
+ super().__init__(
270
+ f"Sub-workflow '{path}' could not be resolved. It is read as a "
271
+ "catalog name from list_workflows (with or without .json), or a "
272
+ "path relative to the workflow that names it. Looked in:"
273
+ f"\n {detail}"
274
+ )
275
+
276
+
277
+ def _describe_roots(roots):
278
+ """Roots this resolution consulted, for a refusal message - never empty
279
+ text, since a run with none configured still owes an answer."""
280
+ return ", ".join(roots) if roots else "(no workflow sources configured)"
281
+
282
+
283
+ def resolve_sub_workflow(path, base_dir, confine_to):
284
+ """Where a sub-workflow step's `path` resolves to, and the root the
285
+ child is confined to, as (path, root).
286
+
287
+ Order, first hit wins:
288
+
289
+ 1. relative to the directory of the workflow that names it - which is
290
+ how a template reaches '../models/x.json', and stays first so an
291
+ existing composition keeps meaning what it did
292
+ 2. the same, with '.json' supplied
293
+ 3. the run's own workflow root (a workspace's workflows/), by catalog
294
+ name, with or without '.json'
295
+ 4. each read-only root on the search path, the same way - which is
296
+ what lets a stored template be composed rather than copied (#90)
297
+
298
+ An absolute path is taken as written and confined to whichever root
299
+ holds it, so the sandbox still refuses one that belongs to no source.
300
+
301
+ A relative path that climbs out of the root it will be handed back with is
302
+ a PathTraversalError rather than a SubWorkflowNotFound - a refusal, not a
303
+ name that was absent. Otherwise raises SubWorkflowNotFound, naming every
304
+ candidate it looked at.
305
+ """
306
+ roots = []
307
+ if confine_to:
308
+ roots.append(os.path.abspath(os.path.expanduser(str(confine_to))))
309
+ for root in fallback_roots(roots[0] if roots else None):
310
+ if root not in roots:
311
+ roots.append(root)
312
+
313
+ tried = []
314
+ if os.path.isabs(path):
315
+ candidate = os.path.normpath(path)
316
+ tried.append(candidate)
317
+ for root in roots:
318
+ source = WorkflowSource(root, EXAMPLES_ORIGIN, False)
319
+ if source.contains(candidate) and os.path.isfile(candidate):
320
+ return candidate, root
321
+ if confine_to:
322
+ # A real confinement boundary was named and nothing on the
323
+ # search path holds this candidate - refuse here, naming every
324
+ # root this resolution consulted, rather than handing an
325
+ # unqualified path back to validate_workflow_path for a refusal
326
+ # that names only the rejected path and not where it looked
327
+ # (#422)
328
+ raise PathTraversalError(
329
+ f"Path outside every workflow source: {candidate}. Looked in: "
330
+ + _describe_roots(roots)
331
+ )
332
+ # Unconfined (a bare CLI run naming no workflow_dir) - hand it back
333
+ # as before and let validate_workflow_path's base=None passthrough
334
+ # decide, since there is no boundary to report roots for
335
+ return candidate, confine_to
336
+
337
+ if base_dir:
338
+ # The root this candidate would be handed back with: the caller's
339
+ # confinement when it named one, else the catalog root the run itself
340
+ # would confine to. Containment is checked before the stat, so a name
341
+ # that climbs out of the catalog is never even looked at - which is
342
+ # what the callers' own validate_workflow_path caught a step too late,
343
+ # leaving this `os.path.isfile` the dw/path-injection query's only
344
+ # unguarded sink. A climb out is refused rather than reported as
345
+ # absent, the distinction the search path below keeps: the
346
+ # PathTraversalError propagates to the caller
347
+ root = confine_to or catalog_root(base_dir)
348
+ search_roots = [root] + [r for r in roots if r != root]
349
+ for name in _candidate_names(path):
350
+ # normpath first: validate_path refuses a '..' outright, so a
351
+ # climb that stays inside the root has to be collapsed before it
352
+ # is judged. Its return value is what gets stat'ed - and being
353
+ # the validator's own, it is contained by construction
354
+ try:
355
+ candidate = validate_path(
356
+ os.path.normpath(os.path.join(base_dir, name)),
357
+ root,
358
+ allow_create=True,
359
+ )
360
+ except PathTraversalError as e:
361
+ # Same refusal, naming the search path rather than only the
362
+ # resolved (rejected) path (#422)
363
+ raise PathTraversalError(
364
+ f"{e} Looked in: {_describe_roots(search_roots)}"
365
+ ) from e
366
+ tried.append(candidate)
367
+ if os.path.isfile(candidate):
368
+ return candidate, confine_to
369
+
370
+ for root in roots:
371
+ source = WorkflowSource(root, EXAMPLES_ORIGIN, False)
372
+ # allow_create, because what is being asked is where the name would
373
+ # land rather than whether something is there - None means the name
374
+ # traverses out of the root, and the file check is the next line
375
+ candidate = resolve_in_source(source, path, allow_create=True)
376
+ if candidate is None:
377
+ tried.append(f"{os.path.join(root, path)} (outside the root)")
378
+ continue
379
+ tried.append(candidate)
380
+ if os.path.isfile(candidate):
381
+ return candidate, root
382
+
383
+ raise SubWorkflowNotFound(path, tried)
@@ -0,0 +1,57 @@
1
+ {
2
+ "id": "h3_context_ir",
3
+ "description": "Rewrites a user's video idea into a MiniMax-H3 Context-IR prompt. Ref2VA has no instruction block above its six sections, so a Ref2VA caller should pass keep_preamble false; the other tasks put a required instruction line there and keep it.",
4
+ "variables": {
5
+ "prompt": "Task: T2VA. Duration: 5.17 seconds. Idea: a red fox trotting through a snowy pine forest at dawn.",
6
+ "image": null,
7
+ "system_prompt": "You rewrite a user's video idea into a MiniMax-H3 Context-IR prompt, the format MiniMax's two prompt-writing guides define (docs/VIDEO_PROMPT_WRITING_GUIDE_base_en.md and docs/VIDEO_PROMPT_WRITING_GUIDE_ref_en.md in the MiniMaxAI/MiniMax-H3 model repository; this text follows them and is checked against them). Output only the rewritten prompt - no preamble, no commentary, no markdown fences, no explanation of what you did.\n\nThe user message states the task (T2VA, I2VA, FL2VA, L2VA or Ref2VA), the duration in seconds, and the idea. It may also state a continuity mode - \"Continuity: standalone\" or \"Continuity: continuation\" - which is standalone when absent. Structure the output as an optional instruction block, one blank line, then the three core fields.\n\nINSTRUCTION BLOCK, by task:\n- T2VA: none. Begin directly with the core fields.\n- I2VA: exactly \"For the target video, at 0.00 seconds into the target video, <Picture 1> (from [Shot 1]) is fully referenced.\"\n- FL2VA: exactly \"How the reference pictures align with the target video - Picture 1 (from Shot 1) aligns with the 0.00-second mark of the target video; Picture 2 (from Shot N) aligns with the S.SS-second mark of the target video.\" where S.SS is the duration to two decimal places and N is the index of the final shot.\n- L2VA: exactly \"How the reference pictures align with the target video - <Picture 1> (from [Shot N]) aligns with the S.SS-second mark of the target video.\"\n- Ref2VA: none of the above. Ref2VA has its own six-section layout, given below.\n\nCORE FIELDS for T2VA, I2VA, FL2VA and L2VA, in this order, separated by blank lines:\nintegrated_multimodal_description: the timeline - visual style, initial composition, subject appearance and position, scene and key props, actions and reactions, shot changes, spoken language and synchronized diegetic sound. This field is always written in full, whatever the brief says about sound or music; a silent brief still gets its complete visual timeline here.\noverall_soundscape: ambient sound, physical action sounds and non-verbal human sounds across the whole video. If the brief says there is no sound, this field is exactly one line reading \"overall_soundscape: N/A\" - not a description of silence.\nnon_diegetic_music: background score the audience hears and the characters cannot. If the brief says there is no music, this field is exactly one line reading \"non_diegetic_music: N/A\". Never describe silence in words and never invent audio a silent brief did not ask for.\n\nREF2VA uses six sections instead, in this order, separated by blank lines:\nsubject_definitions: one line per referenced item. <Subject N> is reusable visible content - a person, animal, object, scene, costume, style, action. <Picture N> is a reference image acting as a keyframe or composition anchor; an image that only defines a subject is cited inside that subject's line instead of getting one of its own. <Video N> is a whole-video relationship - an edit source, a continuation point, or a temporal-structure reference. <Audio N> is an audio asset that is copied or referenced; when it fixes a speaker's voice, name that speaker's ID in its line (\"<Audio 1> is the voice-timbre reference for <Subject 1> (S1).\"). One subject may draw on several assets (\"whose appearance comes from <Picture 1> and whose walking motion comes from <Video 1>\"), and an image that plans shots is a storyboard reference (\"<Picture 3> is a storyboard reference for [Shot 1] and [Shot 2], defining their viewpoint, subject placement, and shot order.\"). A label keeps the same meaning in every later section. The references appear in the order the request passes them, and each label type is numbered independently within its own category: the first video is <Video 1> whether or not pictures precede it, and a video reference does not by itself produce an <Audio N> - an audio label exists only for an audio asset the request passes.\nsummary: a square-bracketed task-type prefix followed by one short paragraph on the target video and its main reference relationships. The prefix combines, with \" + \" and without repeats, only the types the references actually play: \"keyframe completion\" (an image is a concrete first, last or key frame), \"reference generation\" (an image, video or audio guides a character, scene, style, action, camera, or storyboard without being a concrete frame or an edited source), \"video editing\" (a source video is directly modified), \"video continuation\" (new content continues or extends a source video), \"audio reuse\" (all or part of an audio signal is copied into the soundtrack), \"audio reference\" (only an audio's style, timbre, content, beat or continuity is followed). A video that only lends camera, cuts or rhythm is reference generation, not editing or continuation. A video edit opens with \"The target video is an edited version of <Video N>.\"\nretention_analysis: one line per referenced item, stating its relationship marker and what that means for it. Visible content (<Subject N>, <Picture N>, <Video N>) uses exactly one of: fully_preserved (its defined role is kept whole), partially_preserved (still used, some defined characteristics changed or only partly kept), attribute_transfer (its characteristics move onto a different identifiable subject), weak_reference (only broad similarity of style, category, composition or atmosphere). Audio (<Audio N>) uses exactly one of: fully_copy (the whole signal is the whole final soundtrack), partially_copy (part of the timeline or selected layers are copied, or sounds are added or replaced around it), reference (nothing copied; timbre, rhythm, music style, dialogue content or texture followed), weak_reference (only broad similarity of category or atmosphere). Write the entry as \"<Subject 1> (appears in [Shot 1], [Shot 3]): fully_preserved - ...\", \"<Picture 2> ([Shot 1] first frame): fully_preserved - ...\", \"<Audio 1>: partially_copy - ...\". Choose the marker within the role subject_definitions gave the label; new actions, backgrounds or events in the target video are not losses of fidelity.\ndetailed_description: one or two sentences establishing the overall style before [Shot 1] (\"The target video is in a realistic photographic style with soft window light.\"), then the same shot-by-shot timeline the other tasks put in integrated_multimodal_description, as detailed and explicit as possible - composition, appearance, environment and lighting, actions and state changes, camera movement, sound, and the points where referenced content actually takes effect. A generation task runs 350-500 words here, spread across the shots by their information load; dialogue-dense content fits the whole spoken timeline first. Never reduce it to a plot summary or a list of reference relationships.\noverall_soundscape: as above.\nnon_diegetic_music: as above.\n\nCONTINUITY MODES. These are this workflow engine's convention for writing one segment of a chained take and are not part of Context-IR; the user message states one when it applies, and standalone is assumed when it does not. They decide how the clip opens:\n- standalone, the default: the clip is a complete piece. [Shot 1] establishes the scene, and when a reference image is supplied [Shot 1] opens on that image - its framing, lighting and style.\n- continuation: the clip is one segment cut out of a longer unbroken take, and a <Video N> reference holds the frames immediately before it. Write one continuous shot - no cuts, and no later timestamped shots. [Shot 1] opens already in progress, on the framing, shot size, camera position, pose and lighting that <Video N> ends on, and carries them on from there. A supplied reference image constrains subject identity, wardrobe, set and palette alone and leaves the opening composition to <Video N>; the summary prefix includes \"video continuation\", and in retention_analysis the video reference is fully_preserved (it is the continuation point) and the subject the image defines is fully_preserved in its identity role. The clip begins mid-action and ends mid-action: it establishes nothing and resolves nothing. The stated duration is this segment's own, not that of the finished video it belongs to.\n\n\nRULES for the shot-by-shot body, whichever field carries it:\n- Open [Shot 1] with the overall style and initial composition, which in continuation mode is the one <Video N> ends on (in Ref2VA the style sentence sits just before [Shot 1] instead of inside it). Styles include Cinematic, live-action, 2D-animated, 3D CG, claymation, watercolor, vintage film. Derive it from the reference image when there is one, otherwise from the user's text.\n- Do not timestamp the first shot. Every later shot opens with a strictly increasing cut time inside the duration: \"[Shot 2] At 00:03.500, the camera cuts to ...\". A cut must introduce new information; when only framing or angle changes, use camera motion instead. FL2VA favours a single continuous shot unless the user asks otherwise, and continuation mode allows no cut at all.\n- Write camera motion as natural English inside the shot, as motion type plus optional amplitude and speed. Motion types: Zoom In, Zoom Out, Push In, Pull Out, Pan Left, Pan Right, Truck Left, Truck Right, Tilt Up, Tilt Down, Pedestal Up, Pedestal Down, Arc Shot, Tracking Shot, Static Shot, Shake Slightly, Shake Strongly, POV, Roll Clockwise, Roll Counterclockwise. Amplitude: \"with small amplitude\", \"with large amplitude\". Speed: \"at slow speed\", \"at fast speed\". Omit amplitude and speed when they are medium and normal.\n- Give every subject who speaks, sings or vocalises off-screen a stable ID - (S1), (S2), and (S1,S2) for unison - kept across shots. Characters who never vocalise get no ID. The first time a speaker appears, establish identity: character type, age, gender, on- or off-screen, pitch, timbre, speaking rate, accent.\n- Put spoken and sung content inside <d>[Language] ...</d> and nothing else. Identity, action and delivery stay outside the tag. Preserve the user's wording and punctuation verbatim; never translate or rewrite supplied lines. Invent dialogue only when the user asked for speech without supplying any.\n- Dialogue fidelity: write [unclear] for a span that cannot be made out; standardise punctuation and end every <d> block with terminal punctuation before </d>; never write speaker IDs such as (Sx) inside retention_analysis, which describes references, not speech; when an <Audio N> is referenced for a speaker's timbre only, do not carry its original words over - the dialogue is what the brief supplies.\n- Voiceover uses the exact phrase \"says in an off-screen voiceover\", and the text right after every voiceover <d> block states that the character's lips remain completely closed.\n- Use <scenetrans> at both connecting points of a line that continues across a cut, and <cutoff> when speech is truncated by the end of the video.\n- Put text actually visible on screen in double quotation marks, verbatim and untranslated.\n- When a reference image is supplied, the description must match what is actually in it - subject, setting, lighting and style - and in standalone mode [Shot 1] must open on that image's framing rather than on an invented scene; in continuation mode the opening framing is <Video N>'s and the image constrains the rest. Never describe content the image contradicts.\n- Every detail must be something visible or audible, in plain words. No negative phrasing - H3 is guidance-distilled and has no negative prompt, so say what is there, never what is not.",
8
+ "model_name": "Qwen/Qwen3-4B-Instruct-2507",
9
+ "max_new_tokens": 1600,
10
+ "device": "cpu",
11
+ "sections": [
12
+ "subject_definitions",
13
+ "summary",
14
+ "retention_analysis",
15
+ "detailed_description",
16
+ "integrated_multimodal_description",
17
+ "overall_soundscape",
18
+ "non_diegetic_music"
19
+ ],
20
+ "keep_preamble": true
21
+ },
22
+ "steps": [
23
+ {
24
+ "name": "context_ir",
25
+ "task": {
26
+ "command": "text_generation",
27
+ "arguments": {
28
+ "prompt": "variable:prompt",
29
+ "system_prompt": "variable:system_prompt",
30
+ "model_name": "variable:model_name",
31
+ "max_new_tokens": "variable:max_new_tokens",
32
+ "device": "variable:device",
33
+ "image": "variable:image"
34
+ }
35
+ },
36
+ "result": {
37
+ "content_type": "text/plain",
38
+ "save": false
39
+ }
40
+ },
41
+ {
42
+ "name": "trim",
43
+ "task": {
44
+ "command": "extract_sections",
45
+ "arguments": {
46
+ "text": "previous_result:context_ir",
47
+ "sections": "variable:sections",
48
+ "keep_preamble": "variable:keep_preamble"
49
+ }
50
+ },
51
+ "result": {
52
+ "content_type": "text/plain",
53
+ "save": false
54
+ }
55
+ }
56
+ ]
57
+ }
dw/workflows/test.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "variables": {
3
+ "prompt": "an apple",
4
+ "num_images_per_prompt": 1
5
+ },
6
+ "id": "test_job",
7
+ "steps": [
8
+ {
9
+ "name": "main",
10
+ "pipeline": {
11
+ "configuration": {
12
+ "component_type": "StableDiffusionPipeline"
13
+ },
14
+ "from_pretrained_arguments": {
15
+ "model_name": "stable-diffusion-v1-5/stable-diffusion-v1-5",
16
+ "torch_dtype": "torch.float16"
17
+ },
18
+ "arguments": {
19
+ "prompt": "variable:prompt",
20
+ "num_inference_steps": 25,
21
+ "num_images_per_prompt": "variable:num_images_per_prompt"
22
+ }
23
+ },
24
+ "result": {
25
+ "content_type": "image/jpeg",
26
+ "embed_metadata": true,
27
+ "file_base_name": "test_image"
28
+ }
29
+ }
30
+ ]
31
+ }