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/tasks/task.py ADDED
@@ -0,0 +1,920 @@
1
+ import logging
2
+ from typing import Callable, Dict
3
+
4
+ from .. import resolve_device
5
+ from ..events import emit_log
6
+ from .qr_code import get_qrcode_image
7
+ from .image_utils import process_image
8
+ from .video_utils import process_video
9
+ from .gather import gather_images, gather_inputs, gather_videos
10
+ from .format_messages import (
11
+ format_chat_message,
12
+ batch_decode_post_process,
13
+ get_dict_value,
14
+ )
15
+
16
+ # The model-backed handlers (upscale, restore_faces, segment, interpolate_frames,
17
+ # image_to_text, text_generation, diffusion_upscale) are imported inside their
18
+ # handlers - at module scope their transformers/model imports add seconds to
19
+ # every startup for workflows that never run those tasks
20
+
21
+ logger = logging.getLogger("dw")
22
+
23
+
24
+ # Command registry: maps command names to handler functions
25
+ _COMMAND_REGISTRY: Dict[str, Callable] = {}
26
+
27
+ # What each command's arguments actually are. The handlers forward
28
+ # **arguments into an implementation function, so that function's signature
29
+ # is the command's argument schema - registering its dotted path here (a
30
+ # string, to preserve the lazy-import discipline) lets the introspection
31
+ # layer read the same signature the runtime calls. 'provided' names the
32
+ # parameters the dispatch supplies itself, which are not workflow arguments.
33
+ _COMMAND_INFO: Dict[str, dict] = {}
34
+
35
+
36
+ def register_command(
37
+ command_name: str,
38
+ implementation=None,
39
+ provided=(),
40
+ consumes_device=False,
41
+ returns="artifact",
42
+ summary=None,
43
+ parameter_descriptions=None,
44
+ ):
45
+ """
46
+ Decorator to register a command handler function.
47
+
48
+ Args:
49
+ command_name: The command name to register
50
+ implementation: Dotted path to the function whose signature defines
51
+ the command's arguments (None for a command that consumes a
52
+ free-form dict)
53
+ provided: Parameter names the dispatch supplies itself
54
+ consumes_device: True for a handler that calls task.device_for(arguments)
55
+ itself, to pick the accelerator a model-backed task runs on. False
56
+ (the default) is for a task that runs no model and forwards
57
+ arguments straight to its implementation - device is introspected
58
+ as a universal, always-safe-to-pass argument (dw/introspection.py),
59
+ so a non-consuming handler must drop it itself rather than let it
60
+ reach the implementation as an unexpected keyword argument (#185).
61
+ Some commands (e.g. gather_inputs) receive a non-dict argument
62
+ value, which never carries a device to drop
63
+ returns: "artifact" (the default - an image/video/audio/frames object
64
+ `Result.save` knows how to write) or "scalar" for a command whose
65
+ return value is a bare number with no file to save (`judge`). A
66
+ `result` block on a "scalar" command is refused in
67
+ `validation_errors` (dw/scalar_result_validation.py, #212) rather
68
+ than reaching `save_artifact` at run time, where a float has
69
+ nothing left identifying which command produced it
70
+ - or "json" for a command answering a JSON-safe dict of
71
+ measurements (the assessment probes, `dw/tasks/assess.py`): its
72
+ `result` may only be `application/json`, since any other content
73
+ type would explode the dict key by key into files
74
+ summary: Overrides the command's `get_task` summary, which otherwise
75
+ reads the implementation function's docstring. For a command
76
+ whose handler dispatches its implementation per video frame
77
+ (`_per_frame`), that docstring describes the single-frame
78
+ function rather than the command a caller invokes - same reason
79
+ `_VIDEO_PROCESSOR_INFO` overrides `get_first_frame`/
80
+ `get_last_frame` (#366, #383)
81
+ parameter_descriptions: Overrides one or more of the implementation's
82
+ per-parameter `get_task` descriptions by name, for the same
83
+ single-frame-vs-command reason as `summary`
84
+
85
+ Returns:
86
+ Decorator function
87
+ """
88
+
89
+ def decorator(func: Callable) -> Callable:
90
+ if consumes_device:
91
+ handler = func
92
+ else:
93
+
94
+ def handler(task, arguments, previous_pipelines, _func=func):
95
+ if isinstance(arguments, dict):
96
+ arguments.pop("device", None)
97
+ return _func(task, arguments, previous_pipelines)
98
+
99
+ _COMMAND_REGISTRY[command_name] = handler
100
+ info = {
101
+ "kind": "command",
102
+ "implementation": implementation,
103
+ "provided": tuple(provided),
104
+ "returns": returns,
105
+ }
106
+ if summary:
107
+ info["summary"] = summary
108
+ if parameter_descriptions:
109
+ info["parameter_descriptions"] = dict(parameter_descriptions)
110
+ _COMMAND_INFO[command_name] = info
111
+ logger.debug(f"Registered command handler: {command_name}")
112
+ return func
113
+
114
+ return decorator
115
+
116
+
117
+ def task_command_info(command_name):
118
+ """Where a task command's argument schema lives: a dict with 'kind'
119
+ ('command', 'image_processor' or 'video_processor'), 'implementation'
120
+ (dotted path or None for free-form), 'provided', and 'returns'
121
+ ('artifact', the default, or 'scalar' for a bare-number command like
122
+ `judge` - missing entirely for an image/video processor, which is
123
+ always artifact-shaped). Raises ValueError for a name that is not a
124
+ task command at all."""
125
+ info = _COMMAND_INFO.get(command_name)
126
+ if info is not None:
127
+ return info
128
+ if command_name in _VIDEO_PROCESSOR_INFO:
129
+ return _VIDEO_PROCESSOR_INFO[command_name]
130
+ from .image_utils import available_processors
131
+
132
+ if command_name in available_processors():
133
+ return {"kind": "image_processor", "implementation": None, "provided": ()}
134
+ raise ValueError(f"Unknown task command: '{command_name}'")
135
+
136
+
137
+ # Command handler functions
138
+ @register_command("qr_code", implementation="dw.tasks.qr_code.get_qrcode_image")
139
+ def _handle_qr_code(task, arguments, previous_pipelines):
140
+ """Generate QR code image"""
141
+ logger.debug("Generating QR code")
142
+ return get_qrcode_image(**arguments)
143
+
144
+
145
+ @register_command("gather_images", implementation="dw.tasks.gather.gather_images")
146
+ def _handle_gather_images(task, arguments, previous_pipelines):
147
+ """Gather multiple images"""
148
+ logger.debug("Gathering images")
149
+ return gather_images(**arguments)
150
+
151
+
152
+ @register_command("gather_videos", implementation="dw.tasks.gather.gather_videos")
153
+ def _handle_gather_videos(task, arguments, previous_pipelines):
154
+ """Gather multiple videos"""
155
+ logger.debug("Gathering videos")
156
+ return gather_videos(**arguments)
157
+
158
+
159
+ # gather_inputs passes its whole dict through unchanged - free-form by design
160
+ @register_command("gather_inputs")
161
+ def _handle_gather_inputs(task, arguments, previous_pipelines):
162
+ """Gather inputs from various sources"""
163
+ logger.debug("Gathering inputs")
164
+ return gather_inputs(arguments)
165
+
166
+
167
+ @register_command(
168
+ "concat_videos", implementation="dw.tasks.concat_videos.concat_videos"
169
+ )
170
+ def _handle_concat_videos(task, arguments, previous_pipelines):
171
+ """Concatenate videos - and the audio generated with them - into one"""
172
+ logger.debug("Concatenating videos")
173
+ from .concat_videos import concat_videos
174
+
175
+ return concat_videos(**arguments)
176
+
177
+
178
+ @register_command(
179
+ "dissolve_videos", implementation="dw.tasks.dissolve_videos.dissolve_videos"
180
+ )
181
+ def _handle_dissolve_videos(task, arguments, previous_pipelines):
182
+ """Join videos with cross-dissolves, fading the whole from and to a colour"""
183
+ logger.debug("Dissolving videos")
184
+ from .dissolve_videos import dissolve_videos
185
+
186
+ return dissolve_videos(**arguments)
187
+
188
+
189
+ @register_command("fade_audio", implementation="dw.tasks.audio_utils.fade_audio")
190
+ def _handle_fade_audio(task, arguments, previous_pipelines):
191
+ """Fade an audio track in from silence and out to it"""
192
+ logger.debug("Fading audio")
193
+ from .audio_utils import fade_audio
194
+
195
+ return fade_audio(**arguments)
196
+
197
+
198
+ @register_command(
199
+ "normalize_audio", implementation="dw.tasks.audio_utils.normalize_audio"
200
+ )
201
+ def _handle_normalize_audio(task, arguments, previous_pipelines):
202
+ """Scale an audio track so its peak sits at a given level"""
203
+ logger.debug("Normalizing audio")
204
+ from .audio_utils import normalize_audio
205
+
206
+ return normalize_audio(**arguments)
207
+
208
+
209
+ @register_command("slice_audio", implementation="dw.tasks.audio_utils.slice_audio")
210
+ def _handle_slice_audio(task, arguments, previous_pipelines):
211
+ """Cut a time- or frame-aligned slice out of an audio track"""
212
+ logger.debug("Slicing audio")
213
+ from .audio_utils import slice_audio
214
+
215
+ return slice_audio(**arguments)
216
+
217
+
218
+ @register_command("gain_audio", implementation="dw.tasks.audio_utils.gain_audio")
219
+ def _handle_gain_audio(task, arguments, previous_pipelines):
220
+ """Apply a gain to a time- or frame-aligned region of an audio track"""
221
+ logger.debug("Gaining audio region")
222
+ from .audio_utils import gain_audio
223
+
224
+ return gain_audio(**arguments)
225
+
226
+
227
+ @register_command(
228
+ "resample_audio", implementation="dw.tasks.audio_utils.resample_audio"
229
+ )
230
+ def _handle_resample_audio(task, arguments, previous_pipelines):
231
+ """Resample an audio track to a different sample rate"""
232
+ logger.debug("Resampling audio")
233
+ from .audio_utils import resample_audio
234
+
235
+ return resample_audio(**arguments)
236
+
237
+
238
+ @register_command("video_frames", implementation="dw.tasks.video_utils.frames_as_array")
239
+ def _handle_video_frames(task, arguments, previous_pipelines):
240
+ """The frames of a generated video, as one array a later step can condition on"""
241
+ logger.debug("Extracting video frames")
242
+ from .video_utils import frames_as_array
243
+
244
+ return frames_as_array(**arguments)
245
+
246
+
247
+ @register_command("loop_frames", implementation="dw.tasks.video_utils.loop_frames")
248
+ def _handle_loop_frames(task, arguments, previous_pipelines):
249
+ """Repeat a still or a short clip into a run of a given length"""
250
+ logger.debug("Looping frames")
251
+ from .video_utils import loop_frames
252
+
253
+ return loop_frames(**arguments)
254
+
255
+
256
+ @register_command("frame_grid", implementation="dw.tasks.video_utils.frame_grid")
257
+ def _handle_frame_grid(task, arguments, previous_pipelines):
258
+ """Tile evenly sampled frames of a video into one contact-sheet image"""
259
+ logger.debug("Building frame grid")
260
+ from .video_utils import frame_grid
261
+
262
+ return frame_grid(**arguments)
263
+
264
+
265
+ @register_command("pair_audio", implementation="dw.tasks.pair_audio.pair_audio")
266
+ def _handle_pair_audio(task, arguments, previous_pipelines):
267
+ """Pair a video's frames with an audio track generated beside them"""
268
+ logger.debug("Pairing audio with video")
269
+ from .pair_audio import pair_audio
270
+
271
+ return pair_audio(**arguments)
272
+
273
+
274
+ @register_command(
275
+ "crossfade_audio", implementation="dw.tasks.audio_utils.crossfade_audio"
276
+ )
277
+ def _handle_crossfade_audio(task, arguments, previous_pipelines):
278
+ """Join audio tracks with an equal-power crossfade"""
279
+ logger.debug("Crossfading audio")
280
+ from .audio_utils import crossfade_audio
281
+
282
+ return crossfade_audio(**arguments)
283
+
284
+
285
+ @register_command("loop_audio", implementation="dw.tasks.audio_utils.loop_audio")
286
+ def _handle_loop_audio(task, arguments, previous_pipelines):
287
+ """Loop a short recording into a bed of a given length"""
288
+ logger.debug("Looping audio")
289
+ from .audio_utils import loop_audio
290
+
291
+ return loop_audio(**arguments)
292
+
293
+
294
+ @register_command(
295
+ "stabilize_video", implementation="dw.tasks.stabilize.stabilize_video"
296
+ )
297
+ def _handle_stabilize_video(task, arguments, previous_pipelines):
298
+ """Remove a generated clip's accumulated framing drift"""
299
+ logger.debug("Stabilizing video")
300
+ from .stabilize import stabilize_video
301
+
302
+ return stabilize_video(**arguments)
303
+
304
+
305
+ @register_command("mix_audio", implementation="dw.tasks.audio_utils.mix_audio")
306
+ def _handle_mix_audio(task, arguments, previous_pipelines):
307
+ """Layer audio tracks on top of one another, rather than end to end"""
308
+ logger.debug("Mixing audio")
309
+ from .audio_utils import mix_audio
310
+
311
+ return mix_audio(**arguments)
312
+
313
+
314
+ @register_command(
315
+ "compress_audio", implementation="dw.tasks.audio_utils.compress_audio"
316
+ )
317
+ def _handle_compress_audio(task, arguments, previous_pipelines):
318
+ """Shape a track's dynamics with a compressor, limiter or gate"""
319
+ logger.debug("Compressing audio")
320
+ from .audio_utils import compress_audio
321
+
322
+ return compress_audio(**arguments)
323
+
324
+
325
+ @register_command("filter_audio", implementation="dw.tasks.audio_utils.filter_audio")
326
+ def _handle_filter_audio(task, arguments, previous_pipelines):
327
+ """Run a track through a single lowpass/highpass/bandpass/notch filter"""
328
+ logger.debug("Filtering audio")
329
+ from .audio_utils import filter_audio
330
+
331
+ return filter_audio(**arguments)
332
+
333
+
334
+ @register_command("analyze_audio", implementation="dw.tasks.audio_utils.analyze_audio")
335
+ def _handle_analyze_audio(task, arguments, previous_pipelines):
336
+ """Measure a track's levels and spectral balance without changing it"""
337
+ logger.debug("Analyzing audio")
338
+ from .audio_utils import analyze_audio
339
+
340
+ return analyze_audio(**arguments)
341
+
342
+
343
+ @register_command(
344
+ "analyze_shots", implementation="dw.tasks.assess.analyze_shots", returns="json"
345
+ )
346
+ def _handle_analyze_shots(task, arguments, previous_pipelines):
347
+ """Measure each shot of a cut's soundtrack and how far apart they sit"""
348
+ from .assess import analyze_shots
349
+
350
+ return analyze_shots(**arguments)
351
+
352
+
353
+ @register_command(
354
+ "analyze_seams", implementation="dw.tasks.assess.analyze_seams", returns="json"
355
+ )
356
+ def _handle_analyze_seams(task, arguments, previous_pipelines):
357
+ """Measure every seam of a cut - level step, hole, click, frame jump"""
358
+ from .assess import analyze_seams
359
+
360
+ return analyze_seams(**arguments)
361
+
362
+
363
+ @register_command(
364
+ "analyze_sync_drift",
365
+ implementation="dw.tasks.assess.analyze_sync_drift",
366
+ returns="json",
367
+ )
368
+ def _handle_analyze_sync_drift(task, arguments, previous_pipelines):
369
+ """Measure how far a cut's soundtrack sits from its picture"""
370
+ from .assess import analyze_sync_drift
371
+
372
+ return analyze_sync_drift(**arguments)
373
+
374
+
375
+ @register_command("compose_text", implementation="dw.tasks.compose_text.compose_text")
376
+ def _handle_compose_text(task, arguments, previous_pipelines):
377
+ """Join parts written once into one block of text"""
378
+ logger.debug("Composing text")
379
+ from .compose_text import compose_text
380
+
381
+ return compose_text(**arguments)
382
+
383
+
384
+ @register_command("select", implementation="dw.tasks.select.select")
385
+ def _handle_select(task, arguments, previous_pipelines):
386
+ """Reduce a list of candidates to one by a deterministic rule"""
387
+ logger.debug("Selecting")
388
+ from .select import select
389
+
390
+ return select(**arguments)
391
+
392
+
393
+ @register_command(
394
+ "format_chat_message", implementation="dw.tasks.format_messages.format_chat_message"
395
+ )
396
+ def _handle_format_chat_message(task, arguments, previous_pipelines):
397
+ """Format chat message for LLM input"""
398
+ logger.debug("Formatting chat message")
399
+ return format_chat_message(**arguments)
400
+
401
+
402
+ @register_command(
403
+ "get_dict_value", implementation="dw.tasks.format_messages.get_dict_value"
404
+ )
405
+ def _handle_get_dict_value(task, arguments, previous_pipelines):
406
+ """Extract value from dictionary"""
407
+ logger.debug("Getting dictionary value")
408
+ return get_dict_value(**arguments)
409
+
410
+
411
+ def _per_frame(image, process):
412
+ """Run an image command over a video, frame by frame.
413
+
414
+ A video bound to an image argument - an AudioVideo from a generation,
415
+ concat or dissolve step, or a frame array from video_frames - is processed
416
+ one frame at a time and comes back as one video artifact, its soundtrack
417
+ carried through untouched. A single image is processed as itself.
418
+ """
419
+ from ..result import AudioVideo
420
+ from ..shots import carried_shots
421
+ from .video_utils import frames_as_pil_list, is_video
422
+
423
+ if not is_video(image):
424
+ return process(image)
425
+ frames = [process(frame) for frame in frames_as_pil_list(image)]
426
+ audio = getattr(image, "audio", None)
427
+ sample_rate = getattr(image, "sample_rate", None)
428
+ # One frame out per frame in, so the shot boundaries carry through too
429
+ return AudioVideo(
430
+ frames,
431
+ audio,
432
+ sample_rate,
433
+ fps=getattr(image, "fps", None),
434
+ shots=carried_shots(image),
435
+ )
436
+
437
+
438
+ @register_command(
439
+ "upscale", implementation="dw.tasks.upscale.upscale_image", consumes_device=True
440
+ )
441
+ def _handle_upscale(task, arguments, previous_pipelines):
442
+ """Upscale an image using a spandrel-compatible super-resolution model"""
443
+ logger.debug("Upscaling image")
444
+ image = arguments.pop("image")
445
+ model_name = arguments.pop("model_name")
446
+ from .upscale import upscale_image
447
+
448
+ device = task.device_for(arguments)
449
+ return _per_frame(
450
+ image,
451
+ lambda frame: upscale_image(frame, model_name, device=device, **arguments),
452
+ )
453
+
454
+
455
+ @register_command(
456
+ "diffusion_upscale",
457
+ implementation="dw.tasks.diffusion_upscale.diffusion_upscale",
458
+ consumes_device=True,
459
+ )
460
+ def _handle_diffusion_upscale(task, arguments, previous_pipelines):
461
+ """Upscale an image using a diffusion-based upscale pipeline"""
462
+ logger.debug("Diffusion upscaling image")
463
+ image = arguments.pop("image")
464
+ from .diffusion_upscale import diffusion_upscale
465
+
466
+ device = task.device_for(arguments)
467
+ return _per_frame(
468
+ image, lambda frame: diffusion_upscale(frame, device=device, **arguments)
469
+ )
470
+
471
+
472
+ @register_command(
473
+ "restore_faces",
474
+ implementation="dw.tasks.restore_faces.restore_faces",
475
+ consumes_device=True,
476
+ )
477
+ def _handle_restore_faces(task, arguments, previous_pipelines):
478
+ """Restore faces in an image using a spandrel-compatible face restoration model"""
479
+ logger.debug("Restoring faces")
480
+ image = arguments.pop("image")
481
+ model_name = arguments.pop("model_name")
482
+ from .restore_faces import restore_faces
483
+
484
+ device = task.device_for(arguments)
485
+ return _per_frame(
486
+ image,
487
+ lambda frame: restore_faces(frame, model_name, device=device, **arguments),
488
+ )
489
+
490
+
491
+ @register_command(
492
+ "grade",
493
+ implementation="dw.tasks.grade.grade_image",
494
+ summary=(
495
+ "Adjust exposure, contrast, saturation and white balance of an "
496
+ "image or a video."
497
+ ),
498
+ parameter_descriptions={
499
+ "media": (
500
+ "Image or video to grade. An image is a PIL Image; a video is a "
501
+ "file path or an asset:/output: reference, read with its audio "
502
+ "and graded frame by frame, keeping its frame rate and audio "
503
+ "unchanged."
504
+ ),
505
+ },
506
+ )
507
+ def _handle_grade(task, arguments, previous_pipelines):
508
+ """Adjust exposure, contrast, saturation and white balance of an image or a video"""
509
+ logger.debug("Grading media")
510
+ media = arguments.pop("media")
511
+ from .grade import grade_image
512
+
513
+ if isinstance(media, str):
514
+ import os
515
+
516
+ from ..security import ALLOWED_VIDEO_EXTENSIONS
517
+ from .video_utils import load_audio_video
518
+
519
+ if os.path.splitext(media)[1].lower() in ALLOWED_VIDEO_EXTENSIONS:
520
+ media = load_audio_video(media)
521
+ else:
522
+ from ..arguments import fetch_image
523
+
524
+ media = fetch_image(media)
525
+
526
+ defaults = {
527
+ "exposure": 0.0,
528
+ "contrast": 1.0,
529
+ "saturation": 1.0,
530
+ "temperature": 0.0,
531
+ "tint": 0.0,
532
+ }
533
+ applied = {
534
+ name: arguments.get(name, default)
535
+ for name, default in defaults.items()
536
+ if arguments.get(name, default) != default
537
+ }
538
+ emit_log(
539
+ f"grade: applied {applied}"
540
+ if applied
541
+ else "grade: no adjustment (all identity)",
542
+ command="grade",
543
+ **applied,
544
+ )
545
+ return _per_frame(media, lambda frame: grade_image(frame, **arguments))
546
+
547
+
548
+ @register_command(
549
+ "segment", implementation="dw.tasks.segment.segment_image", consumes_device=True
550
+ )
551
+ def _handle_segment(task, arguments, previous_pipelines):
552
+ """Segment objects in an image using text prompt"""
553
+ logger.debug("Segmenting image")
554
+ image = arguments.pop("image")
555
+ prompt = arguments.pop("prompt")
556
+ from .segment import segment_image
557
+
558
+ device = task.device_for(arguments)
559
+ return _per_frame(
560
+ image, lambda frame: segment_image(frame, prompt, device=device, **arguments)
561
+ )
562
+
563
+
564
+ @register_command(
565
+ "interpolate_frames",
566
+ implementation="dw.tasks.interpolate_frames.interpolate_frames",
567
+ consumes_device=True,
568
+ )
569
+ def _handle_interpolate_frames(task, arguments, previous_pipelines):
570
+ """Interpolate video frames to increase frame rate"""
571
+ logger.debug("Interpolating frames")
572
+ video = arguments.pop("video")
573
+ from .interpolate_frames import interpolate_frames
574
+
575
+ return interpolate_frames(video, device=task.device_for(arguments), **arguments)
576
+
577
+
578
+ @register_command(
579
+ "image_to_text",
580
+ implementation="dw.tasks.image_to_text.image_to_text",
581
+ consumes_device=True,
582
+ )
583
+ def _handle_image_to_text(task, arguments, previous_pipelines):
584
+ """Generate text caption from an image"""
585
+ logger.debug("Captioning image")
586
+ image = arguments.pop("image")
587
+ from .image_to_text import image_to_text
588
+
589
+ return image_to_text(image, device=task.device_for(arguments), **arguments)
590
+
591
+
592
+ @register_command(
593
+ "judge",
594
+ implementation="dw.tasks.judge.judge",
595
+ consumes_device=True,
596
+ returns="scalar",
597
+ )
598
+ def _handle_judge(task, arguments, previous_pipelines):
599
+ """Score an image against a rubric with a vision-language model"""
600
+ logger.debug("Judging")
601
+ image = arguments.pop("image")
602
+ from .judge import judge
603
+
604
+ return judge(image, device=task.device_for(arguments), **arguments)
605
+
606
+
607
+ @register_command(
608
+ "transcribe_audio",
609
+ implementation="dw.tasks.audio_transcription.transcribe_audio",
610
+ consumes_device=True,
611
+ )
612
+ def _handle_transcribe_audio(task, arguments, previous_pipelines):
613
+ """Transcribe spoken audio to text"""
614
+ logger.debug("Transcribing audio")
615
+ audio = arguments.pop("audio")
616
+ from .audio_transcription import transcribe_audio
617
+
618
+ return transcribe_audio(audio, device=task.device_for(arguments), **arguments)
619
+
620
+
621
+ @register_command(
622
+ "text_generation",
623
+ implementation="dw.tasks.text_generation.generate_text",
624
+ consumes_device=True,
625
+ )
626
+ def _handle_text_generation(task, arguments, previous_pipelines):
627
+ """Generate text from a prompt using a local LLM"""
628
+ logger.debug("Generating text")
629
+ prompt = arguments.pop("prompt")
630
+ from .text_generation import generate_text
631
+
632
+ return generate_text(prompt, device=task.device_for(arguments), **arguments)
633
+
634
+
635
+ @register_command(
636
+ "generate_speech",
637
+ implementation="dw.tasks.speech_generation.generate_speech",
638
+ consumes_device=True,
639
+ )
640
+ def _handle_speech_generation(task, arguments, previous_pipelines):
641
+ """Speak a line of text with a local text-to-speech model"""
642
+ logger.debug("Generating speech")
643
+ if ("text" in arguments) == ("messages" in arguments):
644
+ raise ValueError(
645
+ "generate_speech needs exactly one of 'text' (the line to speak) "
646
+ "or 'messages' (chat-templated input for a model such as VibeVoice)"
647
+ )
648
+ text = arguments.pop("text", None)
649
+ from .speech_generation import generate_speech
650
+
651
+ return generate_speech(
652
+ text,
653
+ device=task.device_for(arguments),
654
+ seed=task.seed_for(arguments),
655
+ **arguments,
656
+ )
657
+
658
+
659
+ @register_command(
660
+ "extract_sections", implementation="dw.tasks.text_sections.extract_sections"
661
+ )
662
+ def _handle_extract_sections(task, arguments, previous_pipelines):
663
+ """Reduce generated text to a known set of labelled sections"""
664
+ logger.debug("Extracting sections")
665
+ from .text_sections import extract_sections
666
+
667
+ return extract_sections(**arguments)
668
+
669
+
670
+ @register_command(
671
+ "batch_decode_post_process",
672
+ implementation="dw.tasks.format_messages.batch_decode_post_process",
673
+ provided=("processor",),
674
+ )
675
+ def _handle_batch_decode(task, arguments, previous_pipelines):
676
+ """Batch decode post-processing with pipeline reference"""
677
+ logger.debug("Performing batch decode post-processing")
678
+ pipeline_reference = task.task_definition["pipeline_reference"]
679
+ if pipeline_reference not in previous_pipelines:
680
+ raise KeyError(
681
+ f"Pipeline reference '{pipeline_reference}' not found in previous pipelines. "
682
+ f"Available pipelines: {list(previous_pipelines.keys())}"
683
+ )
684
+ processor = previous_pipelines[pipeline_reference].pipeline
685
+ return batch_decode_post_process(processor, **arguments)
686
+
687
+
688
+ def _handle_image_processing(task, arguments, previous_pipelines):
689
+ """Handle image processing commands"""
690
+ logger.debug("Processing image")
691
+ device = task.device_for(arguments)
692
+ return _per_frame(
693
+ arguments.pop("image"),
694
+ lambda frame: process_image(frame, task.command, device, arguments),
695
+ )
696
+
697
+
698
+ def _handle_video_processing(task, arguments, previous_pipelines):
699
+ """Handle video processing commands"""
700
+ logger.debug("Processing video")
701
+ device = task.device_for(arguments)
702
+ return process_video(
703
+ arguments.pop("video"),
704
+ task.command,
705
+ device,
706
+ arguments,
707
+ )
708
+
709
+
710
+ # Command names process_video (video_utils.py) accepts, with the function
711
+ # whose signature carries their arguments. video_utils dispatches via a plain
712
+ # if-chain, so keep this in sync with the branches in process_video().
713
+ # get_first/last_frame pin frame_index themselves, so it is 'provided'; they
714
+ # share get_frame's implementation and so would share its generic docstring
715
+ # summary too (#366) - 'summary' overrides that per command.
716
+ _VIDEO_PROCESSOR_INFO = {
717
+ "get_frame": {
718
+ "kind": "video_processor",
719
+ "implementation": "dw.tasks.video_utils.get_frame",
720
+ "provided": (),
721
+ },
722
+ "get_first_frame": {
723
+ "kind": "video_processor",
724
+ "implementation": "dw.tasks.video_utils.get_frame",
725
+ "provided": ("frame_index",),
726
+ "summary": "The first frame of a video, as a PIL image.",
727
+ },
728
+ "get_last_frame": {
729
+ "kind": "video_processor",
730
+ "implementation": "dw.tasks.video_utils.get_frame",
731
+ "provided": ("frame_index",),
732
+ "summary": "The last frame of a video, as a PIL image.",
733
+ },
734
+ }
735
+ _VIDEO_PROCESSOR_COMMANDS = sorted(_VIDEO_PROCESSOR_INFO)
736
+
737
+
738
+ class Task:
739
+ """
740
+ Represents a task that can be executed as part of a workflow.
741
+ Tasks are atomic operations like image processing, data gathering, or message formatting.
742
+ """
743
+
744
+ def __init__(self, task_definition, device, seed=None):
745
+ """
746
+ Initialize task with its configuration and device settings.
747
+
748
+ Args:
749
+ task_definition: Dictionary containing task configuration and parameters
750
+ device: Device to run task on (e.g., 'cuda', 'mps', 'cpu')
751
+ seed: The workflow/step-resolved seed, when one was set - None for
752
+ an unseeded run. Only a handler that calls seed_for(arguments)
753
+ consumes it; most tasks run no generator and ignore it
754
+ """
755
+ self.task_definition = task_definition
756
+ self.device = device
757
+ self.seed = seed
758
+ logger.debug(f"Initialized task: {self.name} for device: {device}")
759
+
760
+ @property
761
+ def name(self):
762
+ """Get task name from command property"""
763
+ return self.command
764
+
765
+ def device_for(self, arguments):
766
+ """Get the device this task runs on, consuming any override in its arguments.
767
+
768
+ A task can pin itself to a device - a captioning model on the CPU while the GPU
769
+ holds a pipeline, for instance. The argument is removed either way so it does
770
+ not reach the command as a duplicate.
771
+
772
+ Args:
773
+ arguments: Arguments for this run of the task
774
+
775
+ Returns:
776
+ Device identifier the task should run on
777
+ """
778
+ return resolve_device(arguments.pop("device", self.device))
779
+
780
+ def seed_for(self, arguments):
781
+ """Get the seed this task run should use, consuming any override in
782
+ its arguments.
783
+
784
+ A task step reproducible the way a pipeline step is: the
785
+ workflow/step-resolved seed by default, an explicit `seed` in the
786
+ step's own arguments taking precedence - and the argument is removed
787
+ either way so it does not reach the command as a duplicate. None
788
+ means no seed was ever set anywhere, so the task should run exactly
789
+ as it always did - unseeded and non-reproducible.
790
+
791
+ Args:
792
+ arguments: Arguments for this run of the task
793
+ """
794
+ return arguments.pop("seed", self.seed)
795
+
796
+ @property
797
+ def argument_template(self):
798
+ """
799
+ Get argument template for this task.
800
+
801
+ Returns:
802
+ Dictionary of arguments from inputs or arguments section
803
+ """
804
+ # A task will either be an input array or a dictionary of arguments
805
+ if "inputs" in self.task_definition:
806
+ logger.debug("Using inputs as argument template")
807
+ return self.task_definition["inputs"]
808
+
809
+ logger.debug("Using arguments as argument template")
810
+ return self.task_definition["arguments"]
811
+
812
+ @property
813
+ def command(self):
814
+ """Get command name or 'unknown' if not specified"""
815
+ return self.task_definition.get("command", "unknown")
816
+
817
+ def _check_required_arguments(self, arguments):
818
+ """Refuse a task whose required arguments are not all present, in the
819
+ validator's wording rather than Python's."""
820
+ if not isinstance(arguments, dict):
821
+ # An 'inputs' list template - consumed whole, no names to miss
822
+ return
823
+ from ..introspection import (
824
+ missing_task_argument_message,
825
+ missing_task_arguments,
826
+ )
827
+
828
+ missing = missing_task_arguments(self.command, arguments.keys())
829
+ if missing:
830
+ message = missing_task_argument_message(self.command, missing)
831
+ raise ValueError(message[0].upper() + message[1:])
832
+
833
+ def run(self, arguments, previous_pipelines={}):
834
+ """
835
+ Execute the task with given arguments using the command registry.
836
+
837
+ Args:
838
+ arguments: Dictionary of arguments for task execution
839
+ previous_pipelines: Dictionary of previously created pipelines
840
+
841
+ Returns:
842
+ Task output based on command type
843
+
844
+ Raises:
845
+ ValueError: If command is unknown
846
+ KeyError: If required arguments or pipeline references are missing
847
+ """
848
+ logger.debug(f"Running task: {self.command}")
849
+ logger.debug(f"Task arguments: {arguments}")
850
+
851
+ try:
852
+ # Cooperative cancellation reaches task steps too - without this
853
+ # a cancel during a long task waits for the whole task to finish
854
+ from ..events import emit_phase, get_context
855
+
856
+ get_context().check_cancelled()
857
+ # A task reports nothing of its own - a captioning model loading
858
+ # and decoding is otherwise indistinguishable from a hang
859
+ emit_phase("task", detail=self.command)
860
+
861
+ # A required argument that never arrived - because it was left
862
+ # out, or because a variable or an earlier step resolved to
863
+ # nothing - used to reach Python and come back as
864
+ # "resample_audio() missing 1 required positional argument:
865
+ # 'audio'", which names the calling convention rather than the
866
+ # workflow. The static pass in validation_errors refuses the
867
+ # literal case first; this is the backstop it cannot see (#141)
868
+ self._check_required_arguments(arguments)
869
+
870
+ # Look up command in registry
871
+ if self.command in _COMMAND_REGISTRY:
872
+ handler = _COMMAND_REGISTRY[self.command]
873
+ return handler(self, arguments, previous_pipelines)
874
+
875
+ # Not a registered command - check whether it names an image or
876
+ # video processor instead. Imported lazily here to preserve
877
+ # image_utils' lazy-import discipline for callers that never
878
+ # touch image processing.
879
+ from .image_utils import available_processors
880
+
881
+ if self.command in available_processors():
882
+ return _handle_image_processing(self, arguments, previous_pipelines)
883
+
884
+ if self.command in _VIDEO_PROCESSOR_COMMANDS:
885
+ return _handle_video_processing(self, arguments, previous_pipelines)
886
+
887
+ # Unknown command - not in the registry, and not a known image or
888
+ # video processor name either
889
+ error_msg = (
890
+ f"Unknown task command: '{self.command}'. "
891
+ f"Registered commands: {sorted(_COMMAND_REGISTRY.keys())}. "
892
+ f"Image processors: {available_processors()}. "
893
+ f"Video processors: {_VIDEO_PROCESSOR_COMMANDS}"
894
+ )
895
+ logger.error(error_msg)
896
+ raise ValueError(error_msg)
897
+
898
+ except KeyError as e:
899
+ # Missing required arguments or pipeline references
900
+ logger.error(
901
+ f"Missing required data for task {self.command}: {e}", exc_info=True
902
+ )
903
+ raise
904
+ except (ValueError, TypeError) as e:
905
+ # Invalid arguments or type mismatches
906
+ logger.error(
907
+ f"Invalid arguments for task {self.command}: {e}", exc_info=True
908
+ )
909
+ raise
910
+ except (OSError, IOError) as e:
911
+ # File operations, resource loading errors
912
+ logger.error(f"I/O error in task {self.command}: {e}", exc_info=True)
913
+ raise
914
+ except Exception as e:
915
+ # Catch-all for unexpected errors
916
+ logger.error(
917
+ f"Unexpected error ({type(e).__name__}) executing task {self.command}: {e}",
918
+ exc_info=True,
919
+ )
920
+ raise