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/server/assess.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Assess a finished output in the server process (#388).
|
|
2
|
+
|
|
3
|
+
`GET /api/gallery/{name}/assess` runs the assessment probes
|
|
4
|
+
(`dw/tasks/assess.py`) against one gallery file or asset without queueing
|
|
5
|
+
anything: the probes are CPU-only and read a file, so the route is a sync
|
|
6
|
+
`def` FastAPI runs in its threadpool, beside whatever job holds the GPU.
|
|
7
|
+
|
|
8
|
+
One request is one decode: the file is streamed once into a `Media` and
|
|
9
|
+
every applicable probe reads that. The shot boundaries are the ones the
|
|
10
|
+
caller's root recorded - the run manifest for an output, the sidecar
|
|
11
|
+
`keep_output` wrote for an asset (#393) - resolved here rather than by the
|
|
12
|
+
probe, which cannot know which workspace the file belongs to.
|
|
13
|
+
|
|
14
|
+
What comes back are places to look, not verdicts; nothing acts on a
|
|
15
|
+
finding (`dw/assessment_rules.py`).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
|
|
20
|
+
from ..tasks.assess import (
|
|
21
|
+
read_media,
|
|
22
|
+
seams_answer,
|
|
23
|
+
shots_answer,
|
|
24
|
+
sync_drift_answer,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
PROBES = {
|
|
28
|
+
"analyze_shots": shots_answer,
|
|
29
|
+
"analyze_seams": seams_answer,
|
|
30
|
+
"analyze_sync_drift": sync_drift_answer,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def unknown_probe(probe):
|
|
35
|
+
"""The 400 detail for a probe outside the whitelist, or None."""
|
|
36
|
+
if probe is None or probe in PROBES:
|
|
37
|
+
return None
|
|
38
|
+
return f"Unknown probe {probe!r} - one of {', '.join(PROBES)}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _dedupe_findings(findings):
|
|
42
|
+
"""Findings with exact duplicates dropped, order preserved.
|
|
43
|
+
|
|
44
|
+
`_shot_span_findings` (`dw/tasks/assess.py`) is shared by all three
|
|
45
|
+
probes and raises the identical finding from each one that ran, so a
|
|
46
|
+
file with a span overrun listed it three times in the compact merge
|
|
47
|
+
(#427) - one true finding, not three.
|
|
48
|
+
"""
|
|
49
|
+
seen = set()
|
|
50
|
+
deduped = []
|
|
51
|
+
for found in findings:
|
|
52
|
+
key = json.dumps(found, sort_keys=True, default=str)
|
|
53
|
+
if key in seen:
|
|
54
|
+
continue
|
|
55
|
+
seen.add(key)
|
|
56
|
+
deduped.append(found)
|
|
57
|
+
return deduped
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _not_applicable(kind, media, records):
|
|
61
|
+
"""Why each probe cannot say anything about this file: {probe: why}."""
|
|
62
|
+
if media is None:
|
|
63
|
+
why = (
|
|
64
|
+
"a still has no shots, seams or soundtrack to measure"
|
|
65
|
+
if kind == "image"
|
|
66
|
+
else "not a video or audio file"
|
|
67
|
+
)
|
|
68
|
+
return dict.fromkeys(PROBES, why)
|
|
69
|
+
reasons = {}
|
|
70
|
+
if media.audio is None:
|
|
71
|
+
reasons["analyze_shots"] = "no audio track"
|
|
72
|
+
reasons["analyze_sync_drift"] = "no audio track"
|
|
73
|
+
elif media.thumbs is None:
|
|
74
|
+
reasons["analyze_sync_drift"] = "no picture to sync against"
|
|
75
|
+
if not records or len(records) < 2:
|
|
76
|
+
reasons["analyze_seams"] = (
|
|
77
|
+
"no shot boundaries recorded for this file - analyze_seams "
|
|
78
|
+
"needs two or more shots"
|
|
79
|
+
)
|
|
80
|
+
return reasons
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def assess(path, kind, shots, probe=None, detail=False):
|
|
84
|
+
"""Run the applicable probes over one file.
|
|
85
|
+
|
|
86
|
+
path: the validated file; kind: its MEDIA_KINDS entry ("video", "audio",
|
|
87
|
+
"image", ...); shots: the shot records its root recorded, or None.
|
|
88
|
+
|
|
89
|
+
With `probe`, that probe's full answer. Otherwise every applicable
|
|
90
|
+
probe's findings merged, with `rules_applied`, `rules_skipped` and
|
|
91
|
+
`not_applicable`; `detail` adds each probe's full answer under `probes`.
|
|
92
|
+
"""
|
|
93
|
+
media = read_media(path) if kind in ("audio", "video") else None
|
|
94
|
+
records = [dict(shot) for shot in shots] if shots else None
|
|
95
|
+
source = "manifest" if records else "none"
|
|
96
|
+
reasons = _not_applicable(kind, media, records)
|
|
97
|
+
|
|
98
|
+
if probe is not None:
|
|
99
|
+
# Asked by name: a probe the file cannot feed at all says why; one
|
|
100
|
+
# it can (a shotless seam pass included) answers in full, its own
|
|
101
|
+
# rules_skipped saying what could not be measured
|
|
102
|
+
blocked = reasons.get(probe)
|
|
103
|
+
if media is None or (blocked and probe != "analyze_seams"):
|
|
104
|
+
return {"probe": probe, "not_applicable": {probe: blocked}}
|
|
105
|
+
return {"probe": probe, **PROBES[probe](media, records, source)}
|
|
106
|
+
|
|
107
|
+
answers = {
|
|
108
|
+
name: run(media, records, source)
|
|
109
|
+
for name, run in PROBES.items()
|
|
110
|
+
if name not in reasons
|
|
111
|
+
}
|
|
112
|
+
body = {
|
|
113
|
+
"shots_source": source,
|
|
114
|
+
"findings": _dedupe_findings(
|
|
115
|
+
found for answer in answers.values() for found in answer["findings"]
|
|
116
|
+
),
|
|
117
|
+
"rules_applied": [
|
|
118
|
+
rule for answer in answers.values() for rule in answer["rules_applied"]
|
|
119
|
+
],
|
|
120
|
+
"rules_skipped": [
|
|
121
|
+
{"probe": name, **skipped}
|
|
122
|
+
for name, answer in answers.items()
|
|
123
|
+
for skipped in answer["rules_skipped"]
|
|
124
|
+
],
|
|
125
|
+
"not_applicable": reasons,
|
|
126
|
+
}
|
|
127
|
+
if detail:
|
|
128
|
+
body["probes"] = answers
|
|
129
|
+
return body
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
__all__ = ["PROBES", "assess", "unknown_probe"]
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
"""What a workflow makes, read off its definition.
|
|
2
|
+
|
|
3
|
+
A catalog entry is chosen by shape before anything else - one still, a set,
|
|
4
|
+
one shot, a cut sequence - and the definition already says which, in the
|
|
5
|
+
steps it has and what they feed each other. Deriving it here means the
|
|
6
|
+
vocabulary cannot go stale against the file and needs no backfill; a
|
|
7
|
+
declared `shape`/`traits`/`summary` overrides for the odd workflow the
|
|
8
|
+
rules misread.
|
|
9
|
+
|
|
10
|
+
Reads the raw JSON only: no variable substitution, no type loading, so it
|
|
11
|
+
costs a dict walk and behaves the same on a repo template and a file an
|
|
12
|
+
agent saved a second ago. Nothing here names a model family - the rules
|
|
13
|
+
read structure (a concat step, a `references` argument), never checkpoints.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
from ..for_each import list_fields
|
|
19
|
+
from ..variable_constraints import declared_constraints, entry_constraint_fields
|
|
20
|
+
|
|
21
|
+
SHAPES = (
|
|
22
|
+
"image",
|
|
23
|
+
"image-set",
|
|
24
|
+
"image-edit",
|
|
25
|
+
"shot",
|
|
26
|
+
"sequence",
|
|
27
|
+
"audio",
|
|
28
|
+
"text",
|
|
29
|
+
"utility",
|
|
30
|
+
)
|
|
31
|
+
TRAITS = (
|
|
32
|
+
"has-audio",
|
|
33
|
+
"chained",
|
|
34
|
+
"image-conditioned",
|
|
35
|
+
"identity-referenced",
|
|
36
|
+
"needs-input-media",
|
|
37
|
+
"composes-workflows",
|
|
38
|
+
)
|
|
39
|
+
# Tasks that create content rather than process it. A workflow made only
|
|
40
|
+
# of processing tasks is a utility.
|
|
41
|
+
GENERATIVE_TASKS = frozenset(
|
|
42
|
+
{
|
|
43
|
+
"generate_speech",
|
|
44
|
+
"text_generation",
|
|
45
|
+
"image_to_text",
|
|
46
|
+
"diffusion_upscale",
|
|
47
|
+
"interpolate_frames",
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
SUMMARY_LIMIT = 120
|
|
51
|
+
|
|
52
|
+
_KIND_PRECEDENCE = ("video", "audio", "image", "text")
|
|
53
|
+
_EDIT_PIPELINE = re.compile(r"inpaint|img2img|edit|upscale|outpaint", re.I)
|
|
54
|
+
_CHAIN_ARGUMENTS = frozenset(
|
|
55
|
+
{"last_frame", "last_segment", "last_image", "match_audio"}
|
|
56
|
+
)
|
|
57
|
+
_MEDIA_ARGUMENTS = frozenset(
|
|
58
|
+
{"image", "video", "audio", "mask_image", "urls", "videos", "clip"}
|
|
59
|
+
)
|
|
60
|
+
_CUT_TASKS = frozenset({"concat_videos", "dissolve_videos"})
|
|
61
|
+
# Components that exist only to synthesise a waveform. A video pipeline
|
|
62
|
+
# carrying one emits an audio track whether or not it says so in `output`.
|
|
63
|
+
_AUDIO_COMPONENTS = frozenset({"vocoder", "audio_vae"})
|
|
64
|
+
_SENTENCE_END = re.compile(r"(?<=[.!?])\s|\n")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _steps(definition):
|
|
68
|
+
steps = definition.get("steps") if isinstance(definition, dict) else None
|
|
69
|
+
return (
|
|
70
|
+
[step for step in steps if isinstance(step, dict)]
|
|
71
|
+
if isinstance(steps, list)
|
|
72
|
+
else []
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _block(step):
|
|
77
|
+
"""The step's one body: pipeline, pipeline_reference, task or workflow."""
|
|
78
|
+
for key in ("pipeline", "pipeline_reference", "task", "workflow"):
|
|
79
|
+
body = step.get(key)
|
|
80
|
+
if isinstance(body, dict):
|
|
81
|
+
return key, body
|
|
82
|
+
return None, {}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _arguments(step):
|
|
86
|
+
_kind, body = _block(step)
|
|
87
|
+
arguments = body.get("arguments")
|
|
88
|
+
return arguments if isinstance(arguments, dict) else {}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _kind(step):
|
|
92
|
+
result = step.get("result")
|
|
93
|
+
if isinstance(result, dict) and isinstance(result.get("content_type"), str):
|
|
94
|
+
return result["content_type"].split("/")[0]
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _generates(step):
|
|
99
|
+
"""Whether the step creates content: a pipeline, a reference to one, a
|
|
100
|
+
sub-workflow, or a generative task."""
|
|
101
|
+
key, body = _block(step)
|
|
102
|
+
if key in ("pipeline", "pipeline_reference", "workflow"):
|
|
103
|
+
return True
|
|
104
|
+
return key == "task" and body.get("command") in GENERATIVE_TASKS
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _component_type(step):
|
|
108
|
+
key, body = _block(step)
|
|
109
|
+
if key != "pipeline":
|
|
110
|
+
return ""
|
|
111
|
+
configuration = body.get("configuration")
|
|
112
|
+
if not isinstance(configuration, dict):
|
|
113
|
+
return ""
|
|
114
|
+
return str(configuration.get("component_type", ""))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _fed_by(value):
|
|
118
|
+
"""Distinct step names a value's previous_result references name."""
|
|
119
|
+
found = set()
|
|
120
|
+
if isinstance(value, str) and value.startswith("previous_result:"):
|
|
121
|
+
found.add(value.split(":", 1)[1].split(".", 1)[0])
|
|
122
|
+
elif isinstance(value, list):
|
|
123
|
+
for item in value:
|
|
124
|
+
found |= _fed_by(item)
|
|
125
|
+
elif isinstance(value, dict):
|
|
126
|
+
for item in value.values():
|
|
127
|
+
found |= _fed_by(item)
|
|
128
|
+
return found
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _walk(value):
|
|
132
|
+
yield value
|
|
133
|
+
if isinstance(value, dict):
|
|
134
|
+
for item in value.values():
|
|
135
|
+
yield from _walk(item)
|
|
136
|
+
elif isinstance(value, list):
|
|
137
|
+
for item in value:
|
|
138
|
+
yield from _walk(item)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _needs_input_media(steps):
|
|
142
|
+
for step in steps:
|
|
143
|
+
arguments = _arguments(step)
|
|
144
|
+
for name, value in arguments.items():
|
|
145
|
+
if name not in _MEDIA_ARGUMENTS:
|
|
146
|
+
continue
|
|
147
|
+
# A list argument (gather_images' `urls`) carries the same fact
|
|
148
|
+
# one level in.
|
|
149
|
+
candidates = value if isinstance(value, list) else [value]
|
|
150
|
+
if any(
|
|
151
|
+
isinstance(item, str) and item.startswith("variable:")
|
|
152
|
+
for item in candidates
|
|
153
|
+
):
|
|
154
|
+
return True
|
|
155
|
+
for value in _walk(arguments):
|
|
156
|
+
if isinstance(value, str) and value.startswith("asset:"):
|
|
157
|
+
return True
|
|
158
|
+
if isinstance(value, dict) and "location" in value:
|
|
159
|
+
return True
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _cuts_together(steps):
|
|
164
|
+
"""A concat or dissolve fed by two or more distinct steps, or by a list
|
|
165
|
+
of shots handed in whole - one `variable:` reference is a supplied list
|
|
166
|
+
whose length only the caller knows, one `gather:` reference is every
|
|
167
|
+
member of a for_each group, and a cut over either is still an edit."""
|
|
168
|
+
for step in steps:
|
|
169
|
+
key, body = _block(step)
|
|
170
|
+
if key == "task" and body.get("command") in _CUT_TASKS:
|
|
171
|
+
videos = _arguments(step).get("videos")
|
|
172
|
+
if isinstance(videos, str) and videos.startswith(("variable:", "gather:")):
|
|
173
|
+
return True
|
|
174
|
+
sources = _fed_by(videos)
|
|
175
|
+
if isinstance(videos, list):
|
|
176
|
+
# Each supplied shot - a variable, an asset, a path - is its
|
|
177
|
+
# own source; two previous_result entries naming one step are not
|
|
178
|
+
sources |= {
|
|
179
|
+
item
|
|
180
|
+
for item in videos
|
|
181
|
+
if isinstance(item, str) and not item.startswith("previous_result:")
|
|
182
|
+
}
|
|
183
|
+
if len(sources) >= 2:
|
|
184
|
+
return True
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _audio_components(step):
|
|
189
|
+
"""Audio-only components the step's pipeline configures."""
|
|
190
|
+
key, body = _block(step)
|
|
191
|
+
if key != "pipeline":
|
|
192
|
+
return set()
|
|
193
|
+
configuration = body.get("configuration")
|
|
194
|
+
if not isinstance(configuration, dict) or not isinstance(
|
|
195
|
+
configuration.get("components"), dict
|
|
196
|
+
):
|
|
197
|
+
return set()
|
|
198
|
+
return set(configuration["components"]) & _AUDIO_COMPONENTS
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _derive_shape(steps, kind):
|
|
202
|
+
# Cutting shots together makes a sequence even when every shot was
|
|
203
|
+
# supplied rather than generated - an edit is what comes out, and the
|
|
204
|
+
# utility fallthrough would otherwise hide the assembly templates.
|
|
205
|
+
if kind == "video" and _cuts_together(steps):
|
|
206
|
+
return "sequence"
|
|
207
|
+
if kind is None or not any(_generates(step) for step in steps):
|
|
208
|
+
return "utility"
|
|
209
|
+
if kind == "text":
|
|
210
|
+
return "text"
|
|
211
|
+
if kind == "audio":
|
|
212
|
+
return "audio"
|
|
213
|
+
if kind == "video":
|
|
214
|
+
return "shot"
|
|
215
|
+
image_steps = [
|
|
216
|
+
step for step in steps if _generates(step) and _kind(step) == "image"
|
|
217
|
+
]
|
|
218
|
+
for step in image_steps:
|
|
219
|
+
if _EDIT_PIPELINE.search(_component_type(step)):
|
|
220
|
+
return "image-edit"
|
|
221
|
+
if {"image", "mask_image"} & set(_arguments(step)):
|
|
222
|
+
return "image-edit"
|
|
223
|
+
if len(image_steps) >= 2 or any(
|
|
224
|
+
_block(step)[0] == "workflow" for step in image_steps
|
|
225
|
+
):
|
|
226
|
+
return "image-set"
|
|
227
|
+
return "image"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _derive_traits(steps):
|
|
231
|
+
"""The independent facts about how the output is made or what it needs.
|
|
232
|
+
|
|
233
|
+
`has-audio` says the workflow emits a generated audio track - a step
|
|
234
|
+
whose own result is a waveform, a speech task, a video pipeline asked
|
|
235
|
+
for audio, or one carrying a component that exists only to synthesise
|
|
236
|
+
one. Not specifically dialogue, and not a track the workflow was handed.
|
|
237
|
+
"""
|
|
238
|
+
traits = set()
|
|
239
|
+
for step in steps:
|
|
240
|
+
key, body = _block(step)
|
|
241
|
+
arguments = _arguments(step)
|
|
242
|
+
if key == "task" and body.get("command") == "generate_speech":
|
|
243
|
+
traits.add("has-audio")
|
|
244
|
+
# A pipeline whose own result is a waveform generates one. The three
|
|
245
|
+
# checks below it all ask about a *video* step that also carries
|
|
246
|
+
# audio, which left Music 3's template - the one entry in the catalog
|
|
247
|
+
# whose whole output is a track - answering a `has-audio` filter with
|
|
248
|
+
# nothing.
|
|
249
|
+
if _generates(step) and _kind(step) == "audio":
|
|
250
|
+
traits.add("has-audio")
|
|
251
|
+
output = arguments.get("output")
|
|
252
|
+
if _kind(step) == "video" and isinstance(output, list) and "audio" in output:
|
|
253
|
+
traits.add("has-audio")
|
|
254
|
+
if _kind(step) == "video" and _audio_components(step):
|
|
255
|
+
traits.add("has-audio")
|
|
256
|
+
if key == "pipeline" and "chain" in body:
|
|
257
|
+
traits.add("chained")
|
|
258
|
+
if _CHAIN_ARGUMENTS & set(arguments):
|
|
259
|
+
traits.add("chained")
|
|
260
|
+
if _kind(step) == "video" and (
|
|
261
|
+
"image" in arguments or "ImageToVideo" in _component_type(step)
|
|
262
|
+
):
|
|
263
|
+
traits.add("image-conditioned")
|
|
264
|
+
if "references" in arguments:
|
|
265
|
+
traits.add("identity-referenced")
|
|
266
|
+
if key == "workflow":
|
|
267
|
+
traits.add("composes-workflows")
|
|
268
|
+
if _needs_input_media(steps):
|
|
269
|
+
traits.add("needs-input-media")
|
|
270
|
+
return sorted(traits)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _truncate(text):
|
|
274
|
+
"""One line, cut at a word boundary and elided if it runs past the limit.
|
|
275
|
+
|
|
276
|
+
A declared summary goes through this too: `workflow_details` reads a file
|
|
277
|
+
without validating it, so the schema's maxLength never runs on that path
|
|
278
|
+
and a long declaration would otherwise reach a listing unclipped.
|
|
279
|
+
"""
|
|
280
|
+
if len(text) <= SUMMARY_LIMIT:
|
|
281
|
+
return text, False
|
|
282
|
+
cut = text[: SUMMARY_LIMIT - 1]
|
|
283
|
+
cut = cut[: cut.rfind(" ")] if " " in cut else cut
|
|
284
|
+
return cut.rstrip() + "…", True
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _derive_summary(description):
|
|
288
|
+
text = str(description or "").strip()
|
|
289
|
+
if not text:
|
|
290
|
+
return "", False
|
|
291
|
+
return _truncate(_SENTENCE_END.split(text, maxsplit=1)[0].strip())
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def derive_catalog_metadata(definition):
|
|
295
|
+
"""shape, traits and summary for one definition, declarations honoured.
|
|
296
|
+
|
|
297
|
+
Returns {shape, traits, summary, summary_truncated, lists, declared};
|
|
298
|
+
`lists` names what an entry of each for_each-driven variable has to
|
|
299
|
+
carry (`list_fields`) plus its default's length, so an agent can write
|
|
300
|
+
the list argument without opening the definition; `declared` names
|
|
301
|
+
which of the three prose fields came from the file rather than the
|
|
302
|
+
rules, so a test can refuse a declaration that merely repeats the
|
|
303
|
+
derivation.
|
|
304
|
+
"""
|
|
305
|
+
if not isinstance(definition, dict):
|
|
306
|
+
definition = {}
|
|
307
|
+
steps = _steps(definition)
|
|
308
|
+
kinds = {_kind(step) for step in steps} - {None}
|
|
309
|
+
kind = next((k for k in _KIND_PRECEDENCE if k in kinds), None)
|
|
310
|
+
|
|
311
|
+
declared = set()
|
|
312
|
+
shape = _derive_shape(steps, kind)
|
|
313
|
+
if definition.get("shape") in SHAPES:
|
|
314
|
+
shape = definition["shape"]
|
|
315
|
+
declared.add("shape")
|
|
316
|
+
traits = _derive_traits(steps)
|
|
317
|
+
if isinstance(definition.get("traits"), list):
|
|
318
|
+
traits = sorted(t for t in definition["traits"] if t in TRAITS)
|
|
319
|
+
declared.add("traits")
|
|
320
|
+
summary, truncated = _derive_summary(definition.get("description"))
|
|
321
|
+
if isinstance(definition.get("summary"), str) and definition["summary"].strip():
|
|
322
|
+
summary, truncated = _truncate(definition["summary"].strip())
|
|
323
|
+
declared.add("summary")
|
|
324
|
+
variables = definition.get("variables")
|
|
325
|
+
variables = variables if isinstance(variables, dict) else {}
|
|
326
|
+
# A bound an entry field carries is reported beside that field, not
|
|
327
|
+
# only in the top-level `constraints` block: a caller reading what a
|
|
328
|
+
# `shots` entry takes reads the rule for `num_frames` there (#145)
|
|
329
|
+
rules = declared_constraints(definition)
|
|
330
|
+
entry_rules = entry_constraint_fields(definition)
|
|
331
|
+
lists = {
|
|
332
|
+
name: {
|
|
333
|
+
**fields,
|
|
334
|
+
"entries": (
|
|
335
|
+
len(variables[name]) if isinstance(variables.get(name), list) else None
|
|
336
|
+
),
|
|
337
|
+
**(
|
|
338
|
+
{
|
|
339
|
+
"constraints": {
|
|
340
|
+
field: terse_constraint(rules[field])
|
|
341
|
+
for field in entry_rules[name]
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if name in entry_rules
|
|
345
|
+
else {}
|
|
346
|
+
),
|
|
347
|
+
}
|
|
348
|
+
for name, fields in list_fields(definition).items()
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
"shape": shape,
|
|
352
|
+
"traits": traits,
|
|
353
|
+
"summary": summary,
|
|
354
|
+
"summary_truncated": truncated,
|
|
355
|
+
"lists": lists,
|
|
356
|
+
"declared": declared,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
COMPACT_FIELDS = (
|
|
361
|
+
"summary",
|
|
362
|
+
"shape",
|
|
363
|
+
"traits",
|
|
364
|
+
"cost",
|
|
365
|
+
"kinds",
|
|
366
|
+
"variable_names",
|
|
367
|
+
"lists",
|
|
368
|
+
"constraints",
|
|
369
|
+
"configures",
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Carried in the compact view only when set: a template has no
|
|
373
|
+
# `configures`, most workflows have no list-driven step, and most declare
|
|
374
|
+
# no bound on a variable
|
|
375
|
+
_COMPACT_WHEN_SET = frozenset({"configures", "lists", "constraints"})
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def terse_constraint(rule):
|
|
379
|
+
"""One variable's rule as a phrase, for the compact listing.
|
|
380
|
+
|
|
381
|
+
The block itself carries a `reason` in the author's words, which is what
|
|
382
|
+
a consumer reading one workflow wants and what the whole catalog cannot
|
|
383
|
+
afford - the compact listing has a token budget (#101), and the numbers
|
|
384
|
+
are the part that stops the next consumer picking 61 (#96).
|
|
385
|
+
"""
|
|
386
|
+
if not isinstance(rule, dict):
|
|
387
|
+
return rule
|
|
388
|
+
parts = []
|
|
389
|
+
if rule.get("modulus"):
|
|
390
|
+
parts.append(f"{rule['modulus']}*n+{rule.get('remainder', 0)}")
|
|
391
|
+
low, high = rule.get("min_frames"), rule.get("max_frames")
|
|
392
|
+
if low is not None and high is not None:
|
|
393
|
+
parts.append(f"{low}-{high}")
|
|
394
|
+
elif low is not None:
|
|
395
|
+
parts.append(f"{low}+")
|
|
396
|
+
elif high is not None:
|
|
397
|
+
parts.append(f"up to {high}")
|
|
398
|
+
if rule.get("snap") == "up":
|
|
399
|
+
parts.append("rounds up")
|
|
400
|
+
return ", ".join(parts)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def project_listing(
|
|
404
|
+
details,
|
|
405
|
+
*,
|
|
406
|
+
shape=None,
|
|
407
|
+
traits=None,
|
|
408
|
+
configures=None,
|
|
409
|
+
include_models=False,
|
|
410
|
+
view=None,
|
|
411
|
+
):
|
|
412
|
+
"""The listing an agent asked for: filtered by shape and traits, and in
|
|
413
|
+
the compact view stripped to what choosing a template needs.
|
|
414
|
+
|
|
415
|
+
Compact is templates-only unless `include_models` or `configures` says
|
|
416
|
+
otherwise - nine checkpoint variants of text-to-image are the noise the
|
|
417
|
+
two-tree split removed. A workflow with no `configures` is a template
|
|
418
|
+
for this purpose, whichever directory it sits in: a user wrote it to be
|
|
419
|
+
found. The full view never drops entries or fields.
|
|
420
|
+
"""
|
|
421
|
+
if shape is not None and shape not in SHAPES:
|
|
422
|
+
raise ValueError(
|
|
423
|
+
f"Unknown shape {shape!r}. The shapes are: {', '.join(SHAPES)}."
|
|
424
|
+
)
|
|
425
|
+
traits = list(traits or [])
|
|
426
|
+
unknown = [t for t in traits if t not in TRAITS]
|
|
427
|
+
if unknown:
|
|
428
|
+
raise ValueError(
|
|
429
|
+
f"Unknown trait(s) {', '.join(unknown)}. The traits are: {', '.join(TRAITS)}."
|
|
430
|
+
)
|
|
431
|
+
if view not in (None, "compact"):
|
|
432
|
+
raise ValueError("view must be 'compact' or omitted")
|
|
433
|
+
|
|
434
|
+
compact = view == "compact"
|
|
435
|
+
keep_models = include_models or configures is not None or not compact
|
|
436
|
+
projected = {}
|
|
437
|
+
for name, detail in details.items():
|
|
438
|
+
is_model = bool(detail.get("configures") or detail.get("configures_missing"))
|
|
439
|
+
if shape is not None and detail.get("shape") != shape:
|
|
440
|
+
continue
|
|
441
|
+
if traits and not set(traits) <= set(detail.get("traits", [])):
|
|
442
|
+
continue
|
|
443
|
+
if configures is not None and detail.get("configures") != configures:
|
|
444
|
+
continue
|
|
445
|
+
if is_model and not keep_models:
|
|
446
|
+
continue
|
|
447
|
+
if compact:
|
|
448
|
+
slim = {
|
|
449
|
+
key: detail.get(key)
|
|
450
|
+
for key in COMPACT_FIELDS
|
|
451
|
+
if key not in _COMPACT_WHEN_SET or detail.get(key)
|
|
452
|
+
}
|
|
453
|
+
if slim.get("constraints"):
|
|
454
|
+
# A rule that reaches only a list-entry field is reported
|
|
455
|
+
# beside that field under `lists`, so repeating it here
|
|
456
|
+
# would state it twice in one answer - and next to
|
|
457
|
+
# `variable_names`, which does not carry the name (#145).
|
|
458
|
+
# The full listing and `get_workflow` keep the block as the
|
|
459
|
+
# author wrote it
|
|
460
|
+
in_lists = {
|
|
461
|
+
field
|
|
462
|
+
for entry in (slim.get("lists") or {}).values()
|
|
463
|
+
for field in (entry.get("constraints") or {})
|
|
464
|
+
}
|
|
465
|
+
declared = set(detail.get("variable_names") or ())
|
|
466
|
+
slim["constraints"] = {
|
|
467
|
+
variable: terse_constraint(rule)
|
|
468
|
+
for variable, rule in slim["constraints"].items()
|
|
469
|
+
if variable in declared or variable not in in_lists
|
|
470
|
+
}
|
|
471
|
+
if not slim["constraints"]:
|
|
472
|
+
del slim["constraints"]
|
|
473
|
+
# This box's own history, at its two-key budget (#93/#101): the
|
|
474
|
+
# cold median, which is the one comparable to a curated `cost`,
|
|
475
|
+
# and how many runs stand behind it. The block with the
|
|
476
|
+
# cold/warm split, the drivers and `since` is in the full
|
|
477
|
+
# listing and in `get_workflow`
|
|
478
|
+
observed = detail.get("observed") or {}
|
|
479
|
+
if observed.get("cold_minutes") is not None:
|
|
480
|
+
slim["observed_minutes"] = observed["cold_minutes"]
|
|
481
|
+
slim["observed_runs"] = observed["cold_runs"]
|
|
482
|
+
if detail.get("configures_missing"):
|
|
483
|
+
slim["configures_missing"] = detail["configures_missing"]
|
|
484
|
+
projected[name] = slim
|
|
485
|
+
else:
|
|
486
|
+
projected[name] = detail
|
|
487
|
+
return projected
|