diffusers-workflow 0.4.0a3__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 (171) hide show
  1. diffusers_workflow-0.4.0a3.dist-info/METADATA +310 -0
  2. diffusers_workflow-0.4.0a3.dist-info/RECORD +171 -0
  3. diffusers_workflow-0.4.0a3.dist-info/WHEEL +5 -0
  4. diffusers_workflow-0.4.0a3.dist-info/entry_points.txt +6 -0
  5. diffusers_workflow-0.4.0a3.dist-info/licenses/LICENSE +201 -0
  6. diffusers_workflow-0.4.0a3.dist-info/top_level.txt +1 -0
  7. dw/__init__.py +353 -0
  8. dw/arguments.py +906 -0
  9. dw/cache_blocks.json +16 -0
  10. dw/cache_blocks.py +145 -0
  11. dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
  12. dw/events.py +78 -0
  13. dw/hub_cache.py +289 -0
  14. dw/introspection.py +458 -0
  15. dw/log_setup.py +45 -0
  16. dw/pipeline_processors/chain.py +750 -0
  17. dw/pipeline_processors/config_objects.py +235 -0
  18. dw/pipeline_processors/pipeline.py +1687 -0
  19. dw/pipeline_processors/remote.py +18 -0
  20. dw/previous_results.py +259 -0
  21. dw/prompt_weighting.py +378 -0
  22. dw/repl.py +298 -0
  23. dw/repl_commands.py +808 -0
  24. dw/repl_worker.py +129 -0
  25. dw/result.py +850 -0
  26. dw/run.py +92 -0
  27. dw/schema.py +24 -0
  28. dw/security.py +379 -0
  29. dw/serve.py +70 -0
  30. dw/server/__init__.py +2 -0
  31. dw/server/app.py +588 -0
  32. dw/server/jobs.py +547 -0
  33. dw/server/ui/assets/abap-08VXUWAP.js +1 -0
  34. dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
  35. dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  36. dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
  37. dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
  38. dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
  39. dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
  40. dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
  41. dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
  42. dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
  43. dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
  44. dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
  45. dw/server/ui/assets/css-DIMkf-bt.js +3 -0
  46. dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
  47. dw/server/ui/assets/cssMode-CEh6hWi2.js +1 -0
  48. dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
  49. dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
  50. dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  51. dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
  52. dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
  53. dw/server/ui/assets/editor.api-CExg3_mM.js +847 -0
  54. dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
  55. dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
  56. dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
  57. dw/server/ui/assets/freemarker2-DH6orYh2.js +3 -0
  58. dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  59. dw/server/ui/assets/go-C-y9NEjX.js +1 -0
  60. dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
  61. dw/server/ui/assets/handlebars-CbrMVW4Q.js +1 -0
  62. dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
  63. dw/server/ui/assets/html-YDNPZw2M.js +1 -0
  64. dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
  65. dw/server/ui/assets/htmlMode-B_zSGWO2.js +1 -0
  66. dw/server/ui/assets/index-B7-VcYS-.css +1 -0
  67. dw/server/ui/assets/index-D_EiPU3b.js +13 -0
  68. dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
  69. dw/server/ui/assets/java-BEtHBSE6.js +1 -0
  70. dw/server/ui/assets/javascript-dYuBvioq.js +1 -0
  71. dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
  72. dw/server/ui/assets/jsonMode-CUqLM39V.js +7 -0
  73. dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
  74. dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
  75. dw/server/ui/assets/less-B9JPFI3C.js +2 -0
  76. dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
  77. dw/server/ui/assets/liquid-D6vxBzMv.js +1 -0
  78. dw/server/ui/assets/lspLanguageFeatures-1WJ2palX.js +4 -0
  79. dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
  80. dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
  81. dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
  82. dw/server/ui/assets/mdx-SHQb6vmD.js +1 -0
  83. dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
  84. dw/server/ui/assets/monaco--ixms01u.css +1 -0
  85. dw/server/ui/assets/monaco-CP-s5rcP.js +56 -0
  86. dw/server/ui/assets/msdax-DauUninz.js +1 -0
  87. dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
  88. dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  89. dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
  90. dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  91. dw/server/ui/assets/perl-oz_6vUea.js +1 -0
  92. dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
  93. dw/server/ui/assets/php-nr791fC2.js +1 -0
  94. dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
  95. dw/server/ui/assets/postiats-43DmfD33.js +1 -0
  96. dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
  97. dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
  98. dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
  99. dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
  100. dw/server/ui/assets/python-x0_EGHq9.js +1 -0
  101. dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
  102. dw/server/ui/assets/r-BwWrilGY.js +1 -0
  103. dw/server/ui/assets/razor-BZC4LQDP.js +1 -0
  104. dw/server/ui/assets/redis-ClamHrr6.js +1 -0
  105. dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
  106. dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  107. dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
  108. dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
  109. dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
  110. dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
  111. dw/server/ui/assets/scheme-BeGwcela.js +1 -0
  112. dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
  113. dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
  114. dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
  115. dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
  116. dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  117. dw/server/ui/assets/sql-NEE52Syq.js +1 -0
  118. dw/server/ui/assets/st-DbInun42.js +1 -0
  119. dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
  120. dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  121. dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
  122. dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
  123. dw/server/ui/assets/tsMode-BTfA6SbD.js +11 -0
  124. dw/server/ui/assets/twig-De2hgUGE.js +1 -0
  125. dw/server/ui/assets/typescript-CWA4MsNk.js +1 -0
  126. dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
  127. dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
  128. dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
  129. dw/server/ui/assets/workers-CWU0uvj5.js +1 -0
  130. dw/server/ui/assets/xml-KmfTm3rg.js +1 -0
  131. dw/server/ui/assets/yaml-nFO_dDS6.js +1 -0
  132. dw/server/ui/index.html +17 -0
  133. dw/settings.py +77 -0
  134. dw/step.py +132 -0
  135. dw/tasks/audio_utils.py +266 -0
  136. dw/tasks/background_remover.py +43 -0
  137. dw/tasks/borders.py +113 -0
  138. dw/tasks/concat_videos.py +80 -0
  139. dw/tasks/depth_estimator.py +54 -0
  140. dw/tasks/diffusion_upscale.py +109 -0
  141. dw/tasks/format_messages.py +24 -0
  142. dw/tasks/gather.py +139 -0
  143. dw/tasks/image_to_text.py +43 -0
  144. dw/tasks/image_utils.py +661 -0
  145. dw/tasks/interpolate_frames.py +227 -0
  146. dw/tasks/model_cache.py +39 -0
  147. dw/tasks/pair_audio.py +58 -0
  148. dw/tasks/qr_code.py +19 -0
  149. dw/tasks/restore_faces.py +175 -0
  150. dw/tasks/rife_model.py +192 -0
  151. dw/tasks/segment.py +121 -0
  152. dw/tasks/task.py +474 -0
  153. dw/tasks/tensor_image.py +57 -0
  154. dw/tasks/text_generation.py +168 -0
  155. dw/tasks/text_sections.py +80 -0
  156. dw/tasks/upscale.py +203 -0
  157. dw/tasks/video_utils.py +154 -0
  158. dw/tasks/zoe_depth.py +71 -0
  159. dw/teacache.py +376 -0
  160. dw/teacache_models.json +99 -0
  161. dw/test.py +29 -0
  162. dw/type_helpers.py +68 -0
  163. dw/validate.py +43 -0
  164. dw/variables.py +153 -0
  165. dw/worker.py +517 -0
  166. dw/workflow.py +553 -0
  167. dw/workflow_schema.json +1157 -0
  168. dw/workflows/augment_prompt.json +65 -0
  169. dw/workflows/describe_image.json +58 -0
  170. dw/workflows/h3_context_ir.json +57 -0
  171. dw/workflows/test.json +31 -0
dw/introspection.py ADDED
@@ -0,0 +1,458 @@
1
+ """Discover what diffusers exposes and what its pipelines accept.
2
+
3
+ This is the metadata layer a form-generating UI builds on: pipeline names
4
+ come from the installed diffusers (so a new release's pipelines appear with
5
+ no code change here), and a pipeline's argument schema comes from its
6
+ __call__ signature and docstring. Nothing here executes a pipeline.
7
+
8
+ Only bare class names resolved against the diffusers namespace - plus an
9
+ explicit allowlist of companion packages (sdnq) - are accepted from callers;
10
+ never arbitrary dotted import paths, which would let an HTTP client import
11
+ any module on the system.
12
+ """
13
+
14
+ import re
15
+ import inspect
16
+ import logging
17
+
18
+ logger = logging.getLogger("dw")
19
+
20
+ _NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
21
+
22
+ # Matches a diffusers-style docstring parameter header:
23
+ # prompt (`str` or `List[str]`, *optional*):
24
+ _DOC_PARAM_PATTERN = re.compile(r"^(\w+) \((.+?)\):\s*(.*)$")
25
+
26
+
27
+ # Companion packages whose classes workflows commonly name. Extending this
28
+ # is a deliberate act; nothing else outside diffusers ever resolves.
29
+ ALLOWED_MODULES = ("sdnq",)
30
+
31
+ # from_pretrained is **kwargs-based on ModelMixin, so these generic loading
32
+ # knobs are curated rather than discovered - merged with whatever a
33
+ # signature does name. No typo warnings are possible behind **kwargs.
34
+ COMPONENT_LOADING_KNOBS = [
35
+ {
36
+ "name": "torch_dtype",
37
+ "required": False,
38
+ "default": None,
39
+ "annotation": "torch.dtype",
40
+ "description": "Weight dtype to load as, e.g. torch.bfloat16.",
41
+ },
42
+ {
43
+ "name": "variant",
44
+ "required": False,
45
+ "default": None,
46
+ "annotation": "str",
47
+ "description": "Checkpoint variant to load, e.g. 'fp16'.",
48
+ },
49
+ {
50
+ "name": "subfolder",
51
+ "required": False,
52
+ "default": None,
53
+ "annotation": "str",
54
+ "description": "Subfolder of the repository the weights live in.",
55
+ },
56
+ {
57
+ "name": "revision",
58
+ "required": False,
59
+ "default": None,
60
+ "annotation": "str",
61
+ "description": "Git revision (branch, tag or commit) to load from.",
62
+ },
63
+ ]
64
+
65
+
66
+ def _filtered_exports(predicate):
67
+ """diffusers export names passing predicate - names only, no imports."""
68
+ import diffusers
69
+
70
+ return sorted(
71
+ name for name in dir(diffusers) if not name.startswith("_") and predicate(name)
72
+ )
73
+
74
+
75
+ def list_pipelines():
76
+ """Names of every pipeline class the installed diffusers exports.
77
+
78
+ Reads the export list without importing each pipeline's module -
79
+ diffusers is lazy and enumerating hundreds of classes must stay cheap.
80
+ """
81
+ return _filtered_exports(lambda name: name.endswith("Pipeline"))
82
+
83
+
84
+ def list_classes(kind):
85
+ """Class names of one kind, for UI pickers.
86
+
87
+ Enumerates the way list_pipelines does - suffix filters over the export
88
+ list, nothing imported. Autoencoders are models that don't carry the
89
+ Model suffix, so the model filter names them explicitly.
90
+ """
91
+ if kind == "pipelines":
92
+ return list_pipelines()
93
+ if kind == "models":
94
+ return _filtered_exports(
95
+ lambda name: name.endswith("Model") or "Autoencoder" in name
96
+ )
97
+ if kind == "schedulers":
98
+ return _filtered_exports(lambda name: name.endswith("Scheduler"))
99
+ if kind == "quantization":
100
+ names = _filtered_exports(lambda name: name.endswith("Config"))
101
+ import importlib.util
102
+
103
+ if importlib.util.find_spec("sdnq") is not None:
104
+ names.append("sdnq.SDNQConfig")
105
+ return names
106
+ raise ValueError(f"Unknown class kind: {kind!r}")
107
+
108
+
109
+ def load_allowed_class(name):
110
+ """Resolve a class name: bare against diffusers, or module.Class where
111
+ the module is on the explicit allowlist.
112
+
113
+ Raises:
114
+ ValueError: for a malformed name, a module outside the allowlist,
115
+ or a name the module does not export
116
+ """
117
+ module_name, _, class_name = (name or "").rpartition(".")
118
+ if module_name and module_name not in ALLOWED_MODULES:
119
+ raise ValueError(f"Module {module_name!r} is not on the allowlist")
120
+ if not _NAME_PATTERN.match(class_name):
121
+ raise ValueError(f"Not a valid class name: {name!r}")
122
+
123
+ import importlib
124
+
125
+ try:
126
+ module = importlib.import_module(module_name or "diffusers")
127
+ except ImportError as e:
128
+ raise ValueError(f"Could not import {module_name}: {e}")
129
+ try:
130
+ cls = getattr(module, class_name)
131
+ except AttributeError:
132
+ raise ValueError(
133
+ f"{module_name or 'diffusers'} exports no class named {class_name!r}"
134
+ )
135
+ if not isinstance(cls, type):
136
+ raise ValueError(f"{name!r} is not a class")
137
+ return cls
138
+
139
+
140
+ # The original, pipeline-flavored name - existing callers keep working
141
+ load_pipeline_class = load_allowed_class
142
+
143
+
144
+ def _json_safe_default(value):
145
+ if value is inspect.Parameter.empty:
146
+ return None
147
+ if isinstance(value, (str, int, float, bool)) or value is None:
148
+ return value
149
+ return repr(value)
150
+
151
+
152
+ def _parse_docstring_args(docstring):
153
+ """Parameter descriptions from a diffusers-style 'Args:' docstring block.
154
+
155
+ Best effort by design: a pipeline with an unusual docstring simply
156
+ yields fewer descriptions, never an error.
157
+ """
158
+ descriptions = {}
159
+ if not docstring:
160
+ return descriptions
161
+
162
+ lines = docstring.splitlines()
163
+ try:
164
+ start = next(
165
+ i
166
+ for i, line in enumerate(lines)
167
+ if line.strip() in ("Args:", "Parameters:")
168
+ )
169
+ except StopIteration:
170
+ return descriptions
171
+
172
+ current = None
173
+ parts = []
174
+ base_indent = None
175
+ for line in lines[start + 1 :]:
176
+ stripped = line.strip()
177
+ if not stripped:
178
+ continue
179
+ indent = len(line) - len(line.lstrip())
180
+ if base_indent is None:
181
+ base_indent = indent
182
+ if indent < base_indent:
183
+ break # left the Args block (Returns:, Examples:, ...)
184
+
185
+ header = _DOC_PARAM_PATTERN.match(stripped) if indent == base_indent else None
186
+ if header:
187
+ if current:
188
+ descriptions[current]["description"] = " ".join(parts).strip()
189
+ current = header.group(1)
190
+ descriptions[current] = {"doc_type": header.group(2)}
191
+ parts = [header.group(3)] if header.group(3) else []
192
+ elif current:
193
+ parts.append(stripped)
194
+ if current:
195
+ descriptions[current]["description"] = " ".join(parts).strip()
196
+ return descriptions
197
+
198
+
199
+ def _callable_parameters(target_callable):
200
+ """A callable's parameters as form-ready entries, merged with its
201
+ docstring's Args descriptions. Returns (parameters, accepts_kwargs)."""
202
+ signature = inspect.signature(target_callable)
203
+ documented = _parse_docstring_args(inspect.getdoc(target_callable))
204
+
205
+ parameters = []
206
+ accepts_kwargs = False
207
+ for parameter in signature.parameters.values():
208
+ if parameter.name == "self":
209
+ continue
210
+ if parameter.kind == inspect.Parameter.VAR_KEYWORD:
211
+ accepts_kwargs = True
212
+ continue
213
+ if parameter.kind == inspect.Parameter.VAR_POSITIONAL:
214
+ continue
215
+ entry = {
216
+ "name": parameter.name,
217
+ "required": parameter.default is inspect.Parameter.empty,
218
+ "default": _json_safe_default(parameter.default),
219
+ "annotation": (
220
+ None
221
+ if parameter.annotation is inspect.Parameter.empty
222
+ else str(parameter.annotation)
223
+ ),
224
+ }
225
+ entry.update(documented.get(parameter.name, {}))
226
+ parameters.append(entry)
227
+ return parameters, accepts_kwargs
228
+
229
+
230
+ def describe_class(name, target="call"):
231
+ """The argument schema of a class, for form generation.
232
+
233
+ target picks what gets inspected: 'call' reads __call__ (pipelines),
234
+ 'init' reads __init__ (quantization configs, schedulers, models), and
235
+ 'load' reads from_pretrained merged with the curated loading knobs -
236
+ from_pretrained hides everything behind **kwargs, so the knobs are the
237
+ honest answer there. Output shape is identical across targets, so one
238
+ arguments editor consumes all three. Scheduler classes additionally
239
+ report their compatibles list.
240
+ """
241
+ cls = load_allowed_class(name)
242
+ if target == "call":
243
+ target_callable = cls.__call__
244
+ elif target == "init":
245
+ target_callable = cls.__init__
246
+ elif target == "load":
247
+ target_callable = getattr(cls, "from_pretrained", cls.__init__)
248
+ else:
249
+ raise ValueError(f"Unknown inspection target: {target!r}")
250
+
251
+ parameters, accepts_kwargs = _callable_parameters(target_callable)
252
+
253
+ if target == "load":
254
+ named = {parameter["name"] for parameter in parameters}
255
+ parameters = [
256
+ knob for knob in COMPONENT_LOADING_KNOBS if knob["name"] not in named
257
+ ] + parameters
258
+ # the first positional of from_pretrained is the model path, which
259
+ # the editor's own model field carries
260
+ parameters = [
261
+ p for p in parameters if p["name"] != "pretrained_model_name_or_path"
262
+ ]
263
+
264
+ # The class's own docstring only - getdoc walks the MRO and would call
265
+ # every pipeline "Base class for all pipelines."
266
+ class_doc = inspect.cleandoc(cls.__dict__.get("__doc__") or "")
267
+ summary = class_doc.split("\n\n")[0].replace("\n", " ").strip()
268
+
269
+ description = {
270
+ "name": name,
271
+ "summary": summary,
272
+ "accepts_kwargs": accepts_kwargs,
273
+ "parameters": parameters,
274
+ }
275
+
276
+ compatibles = getattr(cls, "_compatibles", None)
277
+ if compatibles:
278
+ description["compatibles"] = sorted(
279
+ c if isinstance(c, str) else getattr(c, "__name__", str(c))
280
+ for c in compatibles
281
+ )
282
+ return description
283
+
284
+
285
+ def describe_pipeline(name):
286
+ """A pipeline's __call__ argument schema - describe_class's original."""
287
+ return describe_class(name, target="call")
288
+
289
+
290
+ def unknown_call_arguments(name, argument_names):
291
+ """The given argument names a pipeline's __call__ will reject.
292
+
293
+ Empty when the signature takes **kwargs (no name can be proven wrong)
294
+ or when the class cannot be resolved or inspected - this feeds warnings,
295
+ and a warning must never be wrong.
296
+ """
297
+ try:
298
+ cls = load_pipeline_class(name)
299
+ signature = inspect.signature(cls.__call__)
300
+ except (ValueError, TypeError):
301
+ return []
302
+ parameters = signature.parameters.values()
303
+ if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in parameters):
304
+ return []
305
+ known = {p.name for p in parameters}
306
+ return sorted(set(argument_names) - known)
307
+
308
+
309
+ def list_tasks():
310
+ """Every task command a workflow's task step can name."""
311
+ from .tasks.task import _COMMAND_REGISTRY, _VIDEO_PROCESSOR_COMMANDS
312
+ from .tasks.image_utils import available_processors
313
+
314
+ return {
315
+ "commands": sorted(_COMMAND_REGISTRY.keys()),
316
+ "image_processors": sorted(available_processors()),
317
+ "video_processors": list(_VIDEO_PROCESSOR_COMMANDS),
318
+ }
319
+
320
+
321
+ def _first_paragraph(docstring):
322
+ cleaned = inspect.cleandoc(docstring or "")
323
+ return cleaned.split("\n\n")[0].replace("\n", " ").strip()
324
+
325
+
326
+ def describe_task(command):
327
+ """A task command's argument schema, in describe_class's shape, so the
328
+ editor's one arguments form consumes both.
329
+
330
+ The schema is the registered implementation function's real signature -
331
+ the same function the dispatch forwards **arguments into - so it cannot
332
+ drift from the runtime. Parameters the dispatch supplies itself are
333
+ removed; 'device' is appended because every task accepts it (the
334
+ dispatch consumes it before the implementation is called). Raises
335
+ ValueError for a name that is not a task command.
336
+ """
337
+ import importlib
338
+
339
+ from .tasks.task import task_command_info
340
+
341
+ info = task_command_info(command)
342
+
343
+ device_parameter = {
344
+ "name": "device",
345
+ "required": False,
346
+ "default": None,
347
+ "annotation": None,
348
+ "description": "Device override for this task (e.g. cpu, cuda:1) - "
349
+ "keeps a helper model off the accelerator a pipeline is using",
350
+ }
351
+
352
+ if info["kind"] == "image_processor":
353
+ return {
354
+ "name": command,
355
+ "summary": f"'{command}' image processor (ControlNet preprocessor)",
356
+ "accepts_kwargs": True,
357
+ "parameters": [
358
+ {
359
+ "name": "image",
360
+ "required": True,
361
+ "default": None,
362
+ "annotation": None,
363
+ "description": "The image to process",
364
+ },
365
+ device_parameter,
366
+ ],
367
+ }
368
+
369
+ if info["implementation"] is None:
370
+ # Free-form by design (gather_inputs): any keys, passed through
371
+ from .tasks.task import _COMMAND_REGISTRY
372
+
373
+ handler = _COMMAND_REGISTRY.get(command)
374
+ return {
375
+ "name": command,
376
+ "summary": _first_paragraph(inspect.getdoc(handler)),
377
+ "accepts_kwargs": True,
378
+ "parameters": [],
379
+ }
380
+
381
+ module_name, _, function_name = info["implementation"].rpartition(".")
382
+ implementation = getattr(importlib.import_module(module_name), function_name)
383
+ parameters, accepts_kwargs = _callable_parameters(implementation)
384
+ parameters = [p for p in parameters if p["name"] not in info["provided"]]
385
+ if not any(p["name"] == "device" for p in parameters):
386
+ parameters.append(device_parameter)
387
+
388
+ summary = _first_paragraph(inspect.getdoc(implementation))
389
+ if not summary:
390
+ from .tasks.task import _COMMAND_REGISTRY
391
+
392
+ summary = _first_paragraph(inspect.getdoc(_COMMAND_REGISTRY.get(command)))
393
+
394
+ return {
395
+ "name": command,
396
+ "summary": summary,
397
+ "accepts_kwargs": accepts_kwargs,
398
+ "parameters": parameters,
399
+ }
400
+
401
+
402
+ def unknown_task_arguments(command, argument_names):
403
+ """The given argument names a task command will not accept.
404
+
405
+ Same never-wrong contract as unknown_call_arguments: empty when the
406
+ implementation takes **kwargs, when the command consumes a free-form
407
+ dict, or when the command cannot be described at all. 'device' is
408
+ always accepted - the dispatch consumes it before the implementation
409
+ runs.
410
+ """
411
+ try:
412
+ description = describe_task(command)
413
+ except Exception:
414
+ return []
415
+ if description["accepts_kwargs"]:
416
+ return []
417
+ known = {p["name"] for p in description["parameters"]} | {"device"}
418
+ return sorted(set(argument_names) - known)
419
+
420
+
421
+ def workflow_argument_warnings(workflow_definition):
422
+ """Best-effort pre-load check of a workflow's arguments.
423
+
424
+ For each pipeline step whose component_type is a bare diffusers class
425
+ name, reports argument names that class's __call__ does not accept - the
426
+ typo that today surfaces as a TypeError after the model has loaded.
427
+ Escaped ({...}) and dotted component types are left alone. Task steps
428
+ get the same check against their registered implementation's signature.
429
+ """
430
+ warnings = []
431
+ for step in workflow_definition.get("steps", []):
432
+ task = step.get("task")
433
+ if task and isinstance(task.get("arguments"), dict):
434
+ command = task.get("command")
435
+ if isinstance(command, str):
436
+ for argument_name in unknown_task_arguments(
437
+ command, task["arguments"].keys()
438
+ ):
439
+ warnings.append(
440
+ f"Step '{step.get('name')}': task '{command}' does not "
441
+ f"accept argument '{argument_name}'"
442
+ )
443
+ pipeline = step.get("pipeline")
444
+ if not pipeline:
445
+ continue
446
+ component_type = pipeline.get("configuration", {}).get("component_type")
447
+ if not isinstance(component_type, str) or not _NAME_PATTERN.match(
448
+ component_type
449
+ ):
450
+ continue
451
+ argument_names = list(pipeline.get("arguments", {}))
452
+ unknown = unknown_call_arguments(component_type, argument_names)
453
+ for argument_name in unknown:
454
+ warnings.append(
455
+ f"Step '{step.get('name')}': {component_type} does not accept "
456
+ f"argument '{argument_name}'"
457
+ )
458
+ return warnings
dw/log_setup.py ADDED
@@ -0,0 +1,45 @@
1
+ import logging
2
+ from concurrent_log_handler import ConcurrentRotatingFileHandler
3
+
4
+ LOG_LEVELS = {
5
+ "CRITICAL": logging.CRITICAL,
6
+ "ERROR": logging.ERROR,
7
+ "WARNING": logging.WARNING,
8
+ "INFO": logging.INFO,
9
+ "DEBUG": logging.DEBUG,
10
+ }
11
+
12
+ _FORMATTER = logging.Formatter(
13
+ fmt="%(asctime)s [%(levelname)s] (%(name)s) %(message)s",
14
+ datefmt="%Y-%m-%dT%H:%M:%S",
15
+ )
16
+
17
+
18
+ def setup_logging(log_path, log_level="INFO", log_to_console=False):
19
+ """Configure the 'dw' logger. Safe to call more than once - existing
20
+ handlers are replaced, not stacked, so a reconfiguring caller (the REPL
21
+ worker between runs) does not multiply every line."""
22
+ logger = logging.getLogger("dw")
23
+ logger.setLevel(LOG_LEVELS.get(log_level, logging.INFO))
24
+
25
+ for handler in list(logger.handlers):
26
+ logger.removeHandler(handler)
27
+ handler.close()
28
+
29
+ file_handler = ConcurrentRotatingFileHandler(
30
+ log_path, "a", maxBytes=50 * 1024 * 1024, backupCount=7
31
+ )
32
+ file_handler.setFormatter(_FORMATTER)
33
+ logger.addHandler(file_handler)
34
+
35
+ if log_to_console:
36
+ console_handler = logging.StreamHandler()
37
+ console_handler.setFormatter(_FORMATTER)
38
+ logger.addHandler(console_handler)
39
+
40
+ return logger
41
+
42
+
43
+ def set_log_level(log_level):
44
+ """Change the 'dw' logger's level without touching its handlers."""
45
+ logging.getLogger("dw").setLevel(LOG_LEVELS.get(log_level, logging.INFO))