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_mcp/catalog.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Read-only tools: everything an agent can look at without spending GPU
|
|
2
|
+
time. Each is a pass-through - the API's shapes are already the ones the
|
|
3
|
+
web UI consumes, and reshaping them here would only add a second thing to
|
|
4
|
+
keep in sync."""
|
|
5
|
+
|
|
6
|
+
from dw_mcp.client import api_path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def list_workflows(
|
|
10
|
+
client, shape=None, traits=None, configures=None, include_models=False
|
|
11
|
+
):
|
|
12
|
+
"""Workflow names the server can reach, in the compact view: summary,
|
|
13
|
+
shape, traits, cost, output kinds and variable names per workflow -
|
|
14
|
+
what choosing one needs and nothing that reading one needs. Templates
|
|
15
|
+
only unless `include_models` or `configures` asks for the model configs
|
|
16
|
+
of one template. `get_workflow` has the full definition.
|
|
17
|
+
|
|
18
|
+
Called with no filter at all, each entry is cut to its summary and
|
|
19
|
+
shape (`view: "summary"`), because the whole catalog in full detail is
|
|
20
|
+
~6.8k tokens for a question that is really "which shape do I want"
|
|
21
|
+
(#101). Pass `shape` - which the instructions ask for anyway - and the
|
|
22
|
+
entries come back whole.
|
|
23
|
+
|
|
24
|
+
`cost_basis` in the answer says what a `cost` is: `curated` means a
|
|
25
|
+
maintainer measured it once, on the devices the entry names, and wrote
|
|
26
|
+
it into the workflow. Nothing derives one from this server's own job
|
|
27
|
+
history, so `cost: null` means nobody wrote a figure down - not that
|
|
28
|
+
the run is cheap, and not that this box has never run it.
|
|
29
|
+
|
|
30
|
+
`observed_minutes` / `observed_runs`, when present, are the other kind
|
|
31
|
+
of number: what *this* box's own finished runs of that workflow actually
|
|
32
|
+
took, cold (model load included, so comparable to a curated `cost`), and
|
|
33
|
+
how many runs stand behind the figure. Derived, never a substitute for
|
|
34
|
+
`cost` - a maintainer's claim on a named card and this machine's last
|
|
35
|
+
week are different things. `get_workflow(variables_only=true)` carries
|
|
36
|
+
the whole block: the cold/warm split, the argument values the figure is
|
|
37
|
+
for, and `since`."""
|
|
38
|
+
params = {"view": "compact"}
|
|
39
|
+
if shape:
|
|
40
|
+
params["shape"] = shape
|
|
41
|
+
if traits:
|
|
42
|
+
params["traits"] = (
|
|
43
|
+
",".join(traits) if isinstance(traits, (list, tuple)) else traits
|
|
44
|
+
)
|
|
45
|
+
if configures:
|
|
46
|
+
params["configures"] = configures
|
|
47
|
+
if include_models:
|
|
48
|
+
params["include_models"] = "true"
|
|
49
|
+
answer = client.get_json("/api/workflows", params=params)
|
|
50
|
+
if not (shape or traits or configures or include_models):
|
|
51
|
+
return _summarised(answer)
|
|
52
|
+
return answer
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# What a summarised entry keeps: enough to choose a shape to ask about,
|
|
56
|
+
# nothing that reading one needs
|
|
57
|
+
SUMMARY_FIELDS = ("summary", "shape")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _summarised(answer):
|
|
61
|
+
"""The unfiltered listing, each entry cut to its summary and shape.
|
|
62
|
+
|
|
63
|
+
The whole catalog in full compact detail is ~6.8k tokens - variables,
|
|
64
|
+
traits, cost and list fields for every workflow on the server, when
|
|
65
|
+
the question the call answers is "which of these is the shape I
|
|
66
|
+
want" (#101). Asking with a `shape` still gets the full entries, and
|
|
67
|
+
the note says so, so nothing is unreachable.
|
|
68
|
+
"""
|
|
69
|
+
details = answer.get("details")
|
|
70
|
+
if not isinstance(details, dict):
|
|
71
|
+
return answer
|
|
72
|
+
summarised = {
|
|
73
|
+
name: {key: detail.get(key) for key in SUMMARY_FIELDS if key in detail}
|
|
74
|
+
for name, detail in details.items()
|
|
75
|
+
if isinstance(detail, dict)
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
**answer,
|
|
79
|
+
"details": summarised,
|
|
80
|
+
"view": "summary",
|
|
81
|
+
"note": (
|
|
82
|
+
"Summarised because no `shape` was given: each entry is its "
|
|
83
|
+
"summary and shape only. Call list_workflows(shape=...) for "
|
|
84
|
+
"the full entries - variables, traits, cost and list fields - "
|
|
85
|
+
"of the shape you want."
|
|
86
|
+
),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_workflow(client, name, variables_only=False):
|
|
91
|
+
"""One workflow's full JSON definition.
|
|
92
|
+
|
|
93
|
+
With variables_only=True, just its variables and what they default to -
|
|
94
|
+
confirming that a stored workflow's audio_bleed_ms is 1800 otherwise
|
|
95
|
+
means pulling the whole definition, quantization blocks and all, to
|
|
96
|
+
read one integer (2026-09-11). Long defaults come back cut to their
|
|
97
|
+
first 200 characters, with the names of the cut ones in `truncated`,
|
|
98
|
+
including strings inside a list default, named like `shots[0].prompt`.
|
|
99
|
+
"""
|
|
100
|
+
if variables_only:
|
|
101
|
+
return client.get_json(api_path("api", "workflows", name, "variables"))
|
|
102
|
+
return client.get_json(api_path("api", "workflows", name))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_schema(client, section=None):
|
|
106
|
+
"""The workflow JSON schema every definition is validated against.
|
|
107
|
+
|
|
108
|
+
Whole it is ~8.6k tokens, so name the part you need: `section` takes
|
|
109
|
+
`steps`, `pipelines`, `tasks`, `result`, `variables` or
|
|
110
|
+
`configuration` and answers that fragment plus `elsewhere`, which says
|
|
111
|
+
which section holds each definition it still references."""
|
|
112
|
+
params = {"section": section} if section else None
|
|
113
|
+
return client.get_json("/api/schema", params=params)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def list_pipelines(client):
|
|
117
|
+
"""Every diffusers pipeline class this install exports."""
|
|
118
|
+
return client.get_json("/api/pipelines")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_pipeline_signature(client, name):
|
|
122
|
+
"""A pipeline's real __call__ arguments - check before proposing a fix."""
|
|
123
|
+
return client.get_json(api_path("api", "pipelines", name))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def list_classes(client, kind):
|
|
127
|
+
"""Class names of one kind: pipelines, models, schedulers, quantization."""
|
|
128
|
+
return client.get_json("/api/classes", params={"kind": kind})
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_class(client, name, target="init"):
|
|
132
|
+
"""A class's argument schema. target: init, call, or load."""
|
|
133
|
+
return client.get_json(api_path("api", "classes", name), params={"target": target})
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def list_tasks(client):
|
|
137
|
+
"""Every task command a workflow's task step can name."""
|
|
138
|
+
return client.get_json("/api/tasks")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_task(client, command):
|
|
142
|
+
"""A task command's argument schema."""
|
|
143
|
+
return client.get_json(api_path("api", "tasks", command))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def list_models(client):
|
|
147
|
+
"""What the Hugging Face hub cache holds, largest repo first."""
|
|
148
|
+
return client.get_json("/api/models")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_memory(client):
|
|
152
|
+
"""Worker VRAM/RAM stats - the first thing to check on an OOM."""
|
|
153
|
+
return client.get_json("/api/memory")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def clear_memory(client):
|
|
157
|
+
"""Drop every loaded pipeline and the step cache, freeing VRAM/RAM
|
|
158
|
+
immediately rather than waiting for the next job to evict one model
|
|
159
|
+
for another. Refused with a 409 while a job is running or queued -
|
|
160
|
+
the queue is FIFO, so retry once it finishes rather than expecting
|
|
161
|
+
this call to wait for it."""
|
|
162
|
+
return client.post_json("/api/memory/clear")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_health(client):
|
|
166
|
+
"""Server liveness, plus what answered: version, device, whether a
|
|
167
|
+
model process is currently resident, the job running now and how many
|
|
168
|
+
are queued. `worker_alive: false` is the normal idle state on a server
|
|
169
|
+
that hasn't run a job yet - not a degraded server."""
|
|
170
|
+
return client.get_json("/api/health")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def get_server_info(client):
|
|
174
|
+
"""What this installation can do and where it keeps things: the
|
|
175
|
+
accelerator, the dw version, the workflow/output/prompt directories,
|
|
176
|
+
and how the server is reached."""
|
|
177
|
+
return client.get_json("/api/server")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def list_jobs(client, limit=20, status=None, workspace=None):
|
|
181
|
+
"""The live queue plus recent history, newest first and bounded.
|
|
182
|
+
|
|
183
|
+
Bounded because the unbounded answer was a dead tool: a server with a
|
|
184
|
+
few months of history spilled 176 entries past the client's tool-result
|
|
185
|
+
limit, and the call failed before a single id could be read
|
|
186
|
+
(2026-09-11). Newest first for the same reason the limit exists - the
|
|
187
|
+
job worth looking at is almost always the last one.
|
|
188
|
+
|
|
189
|
+
`total` is what matched before the cut, so a caller can tell a bounded
|
|
190
|
+
answer from a complete one; raise `limit` or narrow with `status` /
|
|
191
|
+
`workspace` to see the rest."""
|
|
192
|
+
params = {}
|
|
193
|
+
if limit is not None:
|
|
194
|
+
params["limit"] = limit
|
|
195
|
+
if status:
|
|
196
|
+
params["status"] = (
|
|
197
|
+
",".join(status) if isinstance(status, (list, tuple)) else status
|
|
198
|
+
)
|
|
199
|
+
if workspace:
|
|
200
|
+
params["workspace"] = workspace
|
|
201
|
+
body = client.get_json("/api/jobs", params=params or None)
|
|
202
|
+
# The API answers oldest first - the order the web UI's list renders in.
|
|
203
|
+
# An agent reads the top of a tool result, so the newest job belongs there
|
|
204
|
+
jobs = list(reversed(body.get("jobs") or []))
|
|
205
|
+
total = body.get("total", len(jobs))
|
|
206
|
+
answer = {"jobs": jobs, "returned": len(jobs), "total": total}
|
|
207
|
+
if total > len(jobs):
|
|
208
|
+
answer["truncated"] = True
|
|
209
|
+
answer["next"] = (
|
|
210
|
+
f"{total - len(jobs)} older jobs were not listed - raise `limit`, "
|
|
211
|
+
"or narrow with `status` or `workspace`."
|
|
212
|
+
)
|
|
213
|
+
return answer
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def list_gallery(
|
|
217
|
+
client,
|
|
218
|
+
limit=50,
|
|
219
|
+
subfolder=None,
|
|
220
|
+
only_orphans=False,
|
|
221
|
+
workspace=None,
|
|
222
|
+
folder=None,
|
|
223
|
+
version=None,
|
|
224
|
+
media=False,
|
|
225
|
+
):
|
|
226
|
+
"""Generated media in the output directory, newest first. `subfolder`
|
|
227
|
+
narrows to one in-run subfolder ('final', 'intermediate', '' for files
|
|
228
|
+
at a run's root); None means every file. `folder` narrows to one
|
|
229
|
+
workflow and `version` to one run's ordinal, so the two together list
|
|
230
|
+
the run a person calls "v4".
|
|
231
|
+
|
|
232
|
+
`only_orphans=True` inverts the call: instead of files, it returns run
|
|
233
|
+
directories holding nothing but their own bookkeeping (manifest.json,
|
|
234
|
+
workflow.json, job.json) as `runs`, each `{name, mtime}` - a run whose
|
|
235
|
+
output was deleted before `delete_output` could remove it by name, or
|
|
236
|
+
one that failed before writing anything. A run that wrote any file at
|
|
237
|
+
all, a text-shape prompt or a utility's side output included, is not
|
|
238
|
+
listed. `subfolder` does not apply in this mode. `name` is exactly what
|
|
239
|
+
`delete_output` accepts, so clearing one is list, then delete (#170).
|
|
240
|
+
|
|
241
|
+
Each file entry also carries `label`, a bare display basename for a UI
|
|
242
|
+
grid - it is not a valid reference on its own (two runs can write the
|
|
243
|
+
same basename) and is not accepted by `get_gallery_metadata` or
|
|
244
|
+
`delete_output`. Pass `name` to those, not `label`.
|
|
245
|
+
|
|
246
|
+
`version` is that run's ordinal among the workflow's runs, and `run_id`
|
|
247
|
+
the run it came from. The version is what to quote to a person - the web
|
|
248
|
+
UI labels the same file `v5` - and is stable: it is assigned when the
|
|
249
|
+
run opens and a deleted sibling leaves a gap rather than renumbering
|
|
250
|
+
what is left - as does a run that failed, or reused every step from
|
|
251
|
+
the cache, and so wrote nothing to list. Null under the flat output
|
|
252
|
+
layout, which has no runs.
|
|
253
|
+
|
|
254
|
+
`media=True` adds `duration_seconds` to each audio/video entry in the
|
|
255
|
+
page returned, probed the way `get_gallery_metadata` measures a file -
|
|
256
|
+
enough to pick between two takes without one metadata call per
|
|
257
|
+
candidate. Off by default; a plain call carries no `duration_seconds`."""
|
|
258
|
+
params = {"limit": limit}
|
|
259
|
+
if subfolder is not None:
|
|
260
|
+
params["subfolder"] = subfolder
|
|
261
|
+
if folder is not None:
|
|
262
|
+
params["folder"] = folder
|
|
263
|
+
if version is not None:
|
|
264
|
+
params["version"] = version
|
|
265
|
+
if only_orphans:
|
|
266
|
+
params["only_orphans"] = "true"
|
|
267
|
+
if media:
|
|
268
|
+
params["media"] = "true"
|
|
269
|
+
return client.get_json("/api/gallery", params=params, workspace=workspace)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def get_gallery_metadata(client, name, envelope=False, workspace=None):
|
|
273
|
+
"""Metadata embedded in a saved file: the full workflow that made it -
|
|
274
|
+
the exact workflow, arguments and seed, so a result can be reproduced
|
|
275
|
+
or a failed run's definition edited and re-run - plus the job that
|
|
276
|
+
produced it when history remembers one, plus for audio and video what
|
|
277
|
+
the file holds - duration, sample rate, channels, fps, size, peak and
|
|
278
|
+
mean level in dBFS.
|
|
279
|
+
|
|
280
|
+
Only an image (PNG/JPEG/WebP) carries embedded metadata; `metadata` is
|
|
281
|
+
always null for audio and video, since neither format has a slot this
|
|
282
|
+
writer uses. `job` is the fallback recipe when one is known - `next`
|
|
283
|
+
then names `get_job_workflow(job_id)`, which reads the run's realized
|
|
284
|
+
workflow instead. A kept asset (`source: "asset"`) has no job at all,
|
|
285
|
+
so nothing on the server remembers which run made it; `next` says so
|
|
286
|
+
rather than pretending a lookup exists.
|
|
287
|
+
|
|
288
|
+
`name` is a gallery name - the `name` field `list_gallery` reports, not
|
|
289
|
+
its `label` (a display-only basename that is not a valid reference) -
|
|
290
|
+
or an 'asset:' reference to read an input asset the same way (#127); the
|
|
291
|
+
same numbers, `job` null, and `source` saying which of the two answered.
|
|
292
|
+
|
|
293
|
+
With envelope=True the soundtrack's level is reported second by second
|
|
294
|
+
as well, which is what locates something in a track rather than only
|
|
295
|
+
measuring the whole of it. Opt-in: it is one number per second per
|
|
296
|
+
measure, and the default answer has to stay small."""
|
|
297
|
+
body = client.get_json(
|
|
298
|
+
api_path("api", "gallery", name, "metadata"),
|
|
299
|
+
params={"envelope": "true"} if envelope else None,
|
|
300
|
+
workspace=workspace,
|
|
301
|
+
)
|
|
302
|
+
media = body.get("media")
|
|
303
|
+
job = body.get("job")
|
|
304
|
+
hints = []
|
|
305
|
+
if body.get("metadata") is None:
|
|
306
|
+
if job:
|
|
307
|
+
hints.append(
|
|
308
|
+
"metadata is null because only an image (PNG/JPEG/WebP) "
|
|
309
|
+
"carries it embedded - this file's job is known, and "
|
|
310
|
+
f'get_job_workflow(job_id="{job["id"]}") returns the exact '
|
|
311
|
+
"workflow, arguments and seed that produced it."
|
|
312
|
+
)
|
|
313
|
+
elif body.get("source") == "asset":
|
|
314
|
+
hints.append(
|
|
315
|
+
"metadata is null and this is a kept asset, which carries "
|
|
316
|
+
"no provenance - nothing on the server remembers which job, "
|
|
317
|
+
"if any, produced the file it was kept from."
|
|
318
|
+
)
|
|
319
|
+
if media and body.get("source") == "asset":
|
|
320
|
+
hints.append(
|
|
321
|
+
"These are the numbers a workflow's arguments have to match "
|
|
322
|
+
"before the run, not after: frame_count and fps decide a cut's "
|
|
323
|
+
"'total_frames', sample_rate decides what its audio is mixed "
|
|
324
|
+
"at, and duration_seconds says whether a score reaches the "
|
|
325
|
+
"length of the film it goes under - a score shorter than the "
|
|
326
|
+
"cut is padded with digital silence rather than refused, so "
|
|
327
|
+
"make a longer bed with the 'loop_audio' task instead."
|
|
328
|
+
)
|
|
329
|
+
elif media and media.get("kind") == "audio":
|
|
330
|
+
hints.append(
|
|
331
|
+
"Check duration_seconds against what was asked for: a Music 3 "
|
|
332
|
+
"track that lands within 0.2 s of its audio_duration ceiling was "
|
|
333
|
+
"cut off, one well short of it finished naturally. peak_dbfs is "
|
|
334
|
+
"the level normalize_audio would be given, and the range has two "
|
|
335
|
+
"ends: mean_dbfs below -40 on a track that should be full is a "
|
|
336
|
+
"near-silent render, and peak_dbfs at or above 0 is a deliverable "
|
|
337
|
+
"at or over full scale - a decoded lossy file overshoots by up to "
|
|
338
|
+
"a couple dB legitimately (0.59-1.56 dB measured on Music 3 "
|
|
339
|
+
"mp3s), but a figure of +1 or more is a mix with no headroom, and "
|
|
340
|
+
"'normalize_audio' (peak_dbfs: -3) before the saving step is what "
|
|
341
|
+
"fixes it."
|
|
342
|
+
)
|
|
343
|
+
elif media and media.get("kind") == "video":
|
|
344
|
+
hints.append(
|
|
345
|
+
"peak_dbfs is the level normalize_audio would be given, and the "
|
|
346
|
+
"range has two ends: mean_dbfs below -40 on a track that should "
|
|
347
|
+
"be full is a near-silent render, and peak_dbfs at or above 0 is "
|
|
348
|
+
"a deliverable at or over full scale - 'normalize_audio' "
|
|
349
|
+
"(peak_dbfs: -3) before the saving step is what fixes it."
|
|
350
|
+
)
|
|
351
|
+
if media and media.get("shots"):
|
|
352
|
+
hints.append(
|
|
353
|
+
f"This is a cut of {len(media['shots'])} shots, and whole-file "
|
|
354
|
+
f'numbers cannot see inside a join: assess_output(name="{name}") '
|
|
355
|
+
"measures each seam, the shots' levels and sync, and says where "
|
|
356
|
+
"to look."
|
|
357
|
+
)
|
|
358
|
+
if hints:
|
|
359
|
+
body["next"] = " ".join(hints)
|
|
360
|
+
return body
|