diffusers-workflow 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diffusers_workflow-0.4.0.dist-info/METADATA +318 -0
- diffusers_workflow-0.4.0.dist-info/RECORD +260 -0
- diffusers_workflow-0.4.0.dist-info/WHEEL +5 -0
- diffusers_workflow-0.4.0.dist-info/entry_points.txt +7 -0
- diffusers_workflow-0.4.0.dist-info/licenses/LICENSE +201 -0
- diffusers_workflow-0.4.0.dist-info/top_level.txt +2 -0
- dw/__init__.py +440 -0
- dw/adapter_compatibility.py +226 -0
- dw/arguments.py +1231 -0
- dw/assessment_rules.py +159 -0
- dw/assets.py +130 -0
- dw/cache_blocks.json +16 -0
- dw/cache_blocks.py +146 -0
- dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
- dw/content_types.py +150 -0
- dw/dissolve_frame_errors.py +121 -0
- dw/docs/ACCELERATION.md +352 -0
- dw/docs/AGENT_LOOP.md +95 -0
- dw/docs/DEPENDENCIES.md +91 -0
- dw/docs/IP_ADAPTER.md +109 -0
- dw/docs/LORAS.md +131 -0
- dw/docs/MCP.md +517 -0
- dw/docs/PROMPT_WEIGHTING.md +78 -0
- dw/docs/QUANTIZATION.md +230 -0
- dw/docs/RECIPES_24GB.md +201 -0
- dw/docs/RELEASING.md +195 -0
- dw/docs/REMOTE.md +140 -0
- dw/docs/REPL_COMMANDS.md +121 -0
- dw/docs/REPL_WORKER_GUIDE.md +51 -0
- dw/docs/SECURITY.md +272 -0
- dw/docs/SECURITY_QUICKREF.md +112 -0
- dw/docs/SERVER.md +679 -0
- dw/docs/TASKS.md +1741 -0
- dw/docs/TESTING.md +71 -0
- dw/docs/WORKFLOW_GUIDE.md +2038 -0
- dw/docs/WORKSPACES.md +316 -0
- dw/download_watch.py +335 -0
- dw/elision.py +306 -0
- dw/events.py +275 -0
- dw/for_each.py +409 -0
- dw/host_memory.py +258 -0
- dw/host_memory_projection.py +230 -0
- dw/hub_cache.py +432 -0
- dw/introspection.py +1228 -0
- dw/kernel_availability.py +208 -0
- dw/locations.py +599 -0
- dw/log_setup.py +45 -0
- dw/loudness.py +82 -0
- dw/media_audio.py +217 -0
- dw/media_frames.py +367 -0
- dw/media_info.py +297 -0
- dw/pipeline_processors/chain.py +821 -0
- dw/pipeline_processors/config_objects.py +237 -0
- dw/pipeline_processors/pipeline.py +2297 -0
- dw/pipeline_processors/remote.py +46 -0
- dw/plan.py +920 -0
- dw/previous_results.py +411 -0
- dw/probe_paths.py +59 -0
- dw/prompt_schema.json +48 -0
- dw/prompt_weighting.py +378 -0
- dw/prompts.py +159 -0
- dw/realize.py +250 -0
- dw/reference_limits.py +215 -0
- dw/reference_names.py +125 -0
- dw/repl.py +338 -0
- dw/repl_commands.py +836 -0
- dw/repl_worker.py +159 -0
- dw/result.py +1720 -0
- dw/result_fps.py +82 -0
- dw/run.py +162 -0
- dw/runs.py +768 -0
- dw/scalar_result_validation.py +97 -0
- dw/schema.py +283 -0
- dw/security.py +1038 -0
- dw/select_validation.py +115 -0
- dw/serve.py +277 -0
- dw/server/__init__.py +2 -0
- dw/server/app.py +4586 -0
- dw/server/assess.py +132 -0
- dw/server/catalog_shape.py +487 -0
- dw/server/enhancers.py +129 -0
- dw/server/exports.py +480 -0
- dw/server/guides.py +257 -0
- dw/server/jobs.py +1561 -0
- dw/server/mcp_mount.py +95 -0
- dw/server/netinfo.py +124 -0
- dw/server/observed_cost.py +379 -0
- dw/server/sysinfo.py +71 -0
- dw/server/ui/assets/abap-08VXUWAP.js +1 -0
- dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
- dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
- dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
- dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
- dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
- dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
- dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
- dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
- dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
- dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
- dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
- dw/server/ui/assets/css-DIMkf-bt.js +3 -0
- dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
- dw/server/ui/assets/cssMode-CPznxfY8.js +1 -0
- dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
- dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
- dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
- dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
- dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
- dw/server/ui/assets/editor.api-CpWcotrd.js +847 -0
- dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
- dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
- dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
- dw/server/ui/assets/freemarker2-CXtRM8N4.js +3 -0
- dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
- dw/server/ui/assets/go-C-y9NEjX.js +1 -0
- dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
- dw/server/ui/assets/handlebars-N7x-6NMY.js +1 -0
- dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
- dw/server/ui/assets/html-PhsdjHSr.js +1 -0
- dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
- dw/server/ui/assets/htmlMode-Dgj0SEok.js +1 -0
- dw/server/ui/assets/index-3Vw6WAPW.css +1 -0
- dw/server/ui/assets/index-DgrYhQd9.js +43 -0
- dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
- dw/server/ui/assets/java-BEtHBSE6.js +1 -0
- dw/server/ui/assets/javascript-BJqN9Qhv.js +1 -0
- dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
- dw/server/ui/assets/jsonMode-DbM4SWSv.js +7 -0
- dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
- dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
- dw/server/ui/assets/less-B9JPFI3C.js +2 -0
- dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
- dw/server/ui/assets/liquid-BWr8lEc4.js +1 -0
- dw/server/ui/assets/lspLanguageFeatures-C1iGuDyZ.js +4 -0
- dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
- dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
- dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
- dw/server/ui/assets/mdx-DAdMi_0p.js +1 -0
- dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
- dw/server/ui/assets/monaco--ixms01u.css +1 -0
- dw/server/ui/assets/monaco-BGCeEqaw.js +56 -0
- dw/server/ui/assets/msdax-DauUninz.js +1 -0
- dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
- dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
- dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
- dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
- dw/server/ui/assets/perl-oz_6vUea.js +1 -0
- dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
- dw/server/ui/assets/php-nr791fC2.js +1 -0
- dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
- dw/server/ui/assets/postiats-43DmfD33.js +1 -0
- dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
- dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
- dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
- dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
- dw/server/ui/assets/python-Bcn70HdC.js +1 -0
- dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
- dw/server/ui/assets/r-BwWrilGY.js +1 -0
- dw/server/ui/assets/razor-D1HmNnby.js +1 -0
- dw/server/ui/assets/redis-ClamHrr6.js +1 -0
- dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
- dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
- dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
- dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
- dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
- dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
- dw/server/ui/assets/scheme-BeGwcela.js +1 -0
- dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
- dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
- dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
- dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
- dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
- dw/server/ui/assets/sql-NEE52Syq.js +1 -0
- dw/server/ui/assets/st-DbInun42.js +1 -0
- dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
- dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
- dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
- dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
- dw/server/ui/assets/tsMode-D6u0XmOW.js +11 -0
- dw/server/ui/assets/twig-De2hgUGE.js +1 -0
- dw/server/ui/assets/typescript-BU6v-LMV.js +1 -0
- dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
- dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
- dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
- dw/server/ui/assets/workers-Cn7cTUKr.js +1 -0
- dw/server/ui/assets/xml--0LP2Lwk.js +1 -0
- dw/server/ui/assets/yaml-mpBg9jnt.js +1 -0
- dw/server/ui/index.html +17 -0
- dw/server/updater.py +192 -0
- dw/settings.py +98 -0
- dw/shot_span_preflight.py +116 -0
- dw/shots.py +359 -0
- dw/slice_preflight.py +148 -0
- dw/step.py +187 -0
- dw/step_cache.py +442 -0
- dw/subfolders.py +107 -0
- dw/task_domains.py +307 -0
- dw/tasks/assess.py +826 -0
- dw/tasks/audio_transcription.py +88 -0
- dw/tasks/audio_utils.py +1862 -0
- dw/tasks/background_remover.py +43 -0
- dw/tasks/borders.py +113 -0
- dw/tasks/compose_text.py +74 -0
- dw/tasks/concat_videos.py +300 -0
- dw/tasks/depth_estimator.py +54 -0
- dw/tasks/diffusion_upscale.py +109 -0
- dw/tasks/dissolve_videos.py +342 -0
- dw/tasks/format_messages.py +24 -0
- dw/tasks/gather.py +173 -0
- dw/tasks/grade.py +97 -0
- dw/tasks/image_to_text.py +43 -0
- dw/tasks/image_utils.py +764 -0
- dw/tasks/interpolate_frames.py +252 -0
- dw/tasks/judge.py +68 -0
- dw/tasks/model_cache.py +55 -0
- dw/tasks/pair_audio.py +268 -0
- dw/tasks/qr_code.py +19 -0
- dw/tasks/restore_faces.py +175 -0
- dw/tasks/rife_model.py +192 -0
- dw/tasks/segment.py +121 -0
- dw/tasks/select.py +111 -0
- dw/tasks/speech_generation.py +228 -0
- dw/tasks/stabilize.py +129 -0
- dw/tasks/task.py +920 -0
- dw/tasks/tensor_image.py +57 -0
- dw/tasks/text_generation.py +169 -0
- dw/tasks/text_sections.py +80 -0
- dw/tasks/upscale.py +203 -0
- dw/tasks/video_utils.py +624 -0
- dw/tasks/zoe_depth.py +71 -0
- dw/teacache.py +381 -0
- dw/teacache_models.json +99 -0
- dw/test.py +29 -0
- dw/type_helpers.py +231 -0
- dw/validate.py +68 -0
- dw/variable_constraints.py +444 -0
- dw/variables.py +443 -0
- dw/video_extensions.py +141 -0
- dw/vram_estimate.py +116 -0
- dw/worker.py +764 -0
- dw/workflow.py +2007 -0
- dw/workflow_schema.json +1346 -0
- dw/workflow_sources.py +383 -0
- dw/workflows/h3_context_ir.json +57 -0
- dw/workflows/test.json +31 -0
- dw/workspace.py +730 -0
- dw_mcp/__init__.py +6 -0
- dw_mcp/__main__.py +133 -0
- dw_mcp/assets.py +336 -0
- dw_mcp/authoring.py +114 -0
- dw_mcp/catalog.py +360 -0
- dw_mcp/client.py +486 -0
- dw_mcp/diagnose.py +371 -0
- dw_mcp/exports.py +84 -0
- dw_mcp/guides.py +35 -0
- dw_mcp/media.py +638 -0
- dw_mcp/models.py +97 -0
- dw_mcp/prompts.py +104 -0
- dw_mcp/server.py +1343 -0
- dw_mcp/workspaces.py +212 -0
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
|