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.
- diffusers_workflow-0.4.0a3.dist-info/METADATA +310 -0
- diffusers_workflow-0.4.0a3.dist-info/RECORD +171 -0
- diffusers_workflow-0.4.0a3.dist-info/WHEEL +5 -0
- diffusers_workflow-0.4.0a3.dist-info/entry_points.txt +6 -0
- diffusers_workflow-0.4.0a3.dist-info/licenses/LICENSE +201 -0
- diffusers_workflow-0.4.0a3.dist-info/top_level.txt +1 -0
- dw/__init__.py +353 -0
- dw/arguments.py +906 -0
- dw/cache_blocks.json +16 -0
- dw/cache_blocks.py +145 -0
- dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
- dw/events.py +78 -0
- dw/hub_cache.py +289 -0
- dw/introspection.py +458 -0
- dw/log_setup.py +45 -0
- dw/pipeline_processors/chain.py +750 -0
- dw/pipeline_processors/config_objects.py +235 -0
- dw/pipeline_processors/pipeline.py +1687 -0
- dw/pipeline_processors/remote.py +18 -0
- dw/previous_results.py +259 -0
- dw/prompt_weighting.py +378 -0
- dw/repl.py +298 -0
- dw/repl_commands.py +808 -0
- dw/repl_worker.py +129 -0
- dw/result.py +850 -0
- dw/run.py +92 -0
- dw/schema.py +24 -0
- dw/security.py +379 -0
- dw/serve.py +70 -0
- dw/server/__init__.py +2 -0
- dw/server/app.py +588 -0
- dw/server/jobs.py +547 -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-CEh6hWi2.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-CExg3_mM.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-DH6orYh2.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-CbrMVW4Q.js +1 -0
- dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
- dw/server/ui/assets/html-YDNPZw2M.js +1 -0
- dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
- dw/server/ui/assets/htmlMode-B_zSGWO2.js +1 -0
- dw/server/ui/assets/index-B7-VcYS-.css +1 -0
- dw/server/ui/assets/index-D_EiPU3b.js +13 -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-dYuBvioq.js +1 -0
- dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
- dw/server/ui/assets/jsonMode-CUqLM39V.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-D6vxBzMv.js +1 -0
- dw/server/ui/assets/lspLanguageFeatures-1WJ2palX.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-SHQb6vmD.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-CP-s5rcP.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-x0_EGHq9.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-BZC4LQDP.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-BTfA6SbD.js +11 -0
- dw/server/ui/assets/twig-De2hgUGE.js +1 -0
- dw/server/ui/assets/typescript-CWA4MsNk.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-CWU0uvj5.js +1 -0
- dw/server/ui/assets/xml-KmfTm3rg.js +1 -0
- dw/server/ui/assets/yaml-nFO_dDS6.js +1 -0
- dw/server/ui/index.html +17 -0
- dw/settings.py +77 -0
- dw/step.py +132 -0
- dw/tasks/audio_utils.py +266 -0
- dw/tasks/background_remover.py +43 -0
- dw/tasks/borders.py +113 -0
- dw/tasks/concat_videos.py +80 -0
- dw/tasks/depth_estimator.py +54 -0
- dw/tasks/diffusion_upscale.py +109 -0
- dw/tasks/format_messages.py +24 -0
- dw/tasks/gather.py +139 -0
- dw/tasks/image_to_text.py +43 -0
- dw/tasks/image_utils.py +661 -0
- dw/tasks/interpolate_frames.py +227 -0
- dw/tasks/model_cache.py +39 -0
- dw/tasks/pair_audio.py +58 -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/task.py +474 -0
- dw/tasks/tensor_image.py +57 -0
- dw/tasks/text_generation.py +168 -0
- dw/tasks/text_sections.py +80 -0
- dw/tasks/upscale.py +203 -0
- dw/tasks/video_utils.py +154 -0
- dw/tasks/zoe_depth.py +71 -0
- dw/teacache.py +376 -0
- dw/teacache_models.json +99 -0
- dw/test.py +29 -0
- dw/type_helpers.py +68 -0
- dw/validate.py +43 -0
- dw/variables.py +153 -0
- dw/worker.py +517 -0
- dw/workflow.py +553 -0
- dw/workflow_schema.json +1157 -0
- dw/workflows/augment_prompt.json +65 -0
- dw/workflows/describe_image.json +58 -0
- dw/workflows/h3_context_ir.json +57 -0
- 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))
|