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.
Files changed (260) hide show
  1. diffusers_workflow-0.4.0.dist-info/METADATA +318 -0
  2. diffusers_workflow-0.4.0.dist-info/RECORD +260 -0
  3. diffusers_workflow-0.4.0.dist-info/WHEEL +5 -0
  4. diffusers_workflow-0.4.0.dist-info/entry_points.txt +7 -0
  5. diffusers_workflow-0.4.0.dist-info/licenses/LICENSE +201 -0
  6. diffusers_workflow-0.4.0.dist-info/top_level.txt +2 -0
  7. dw/__init__.py +440 -0
  8. dw/adapter_compatibility.py +226 -0
  9. dw/arguments.py +1231 -0
  10. dw/assessment_rules.py +159 -0
  11. dw/assets.py +130 -0
  12. dw/cache_blocks.json +16 -0
  13. dw/cache_blocks.py +146 -0
  14. dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
  15. dw/content_types.py +150 -0
  16. dw/dissolve_frame_errors.py +121 -0
  17. dw/docs/ACCELERATION.md +352 -0
  18. dw/docs/AGENT_LOOP.md +95 -0
  19. dw/docs/DEPENDENCIES.md +91 -0
  20. dw/docs/IP_ADAPTER.md +109 -0
  21. dw/docs/LORAS.md +131 -0
  22. dw/docs/MCP.md +517 -0
  23. dw/docs/PROMPT_WEIGHTING.md +78 -0
  24. dw/docs/QUANTIZATION.md +230 -0
  25. dw/docs/RECIPES_24GB.md +201 -0
  26. dw/docs/RELEASING.md +195 -0
  27. dw/docs/REMOTE.md +140 -0
  28. dw/docs/REPL_COMMANDS.md +121 -0
  29. dw/docs/REPL_WORKER_GUIDE.md +51 -0
  30. dw/docs/SECURITY.md +272 -0
  31. dw/docs/SECURITY_QUICKREF.md +112 -0
  32. dw/docs/SERVER.md +679 -0
  33. dw/docs/TASKS.md +1741 -0
  34. dw/docs/TESTING.md +71 -0
  35. dw/docs/WORKFLOW_GUIDE.md +2038 -0
  36. dw/docs/WORKSPACES.md +316 -0
  37. dw/download_watch.py +335 -0
  38. dw/elision.py +306 -0
  39. dw/events.py +275 -0
  40. dw/for_each.py +409 -0
  41. dw/host_memory.py +258 -0
  42. dw/host_memory_projection.py +230 -0
  43. dw/hub_cache.py +432 -0
  44. dw/introspection.py +1228 -0
  45. dw/kernel_availability.py +208 -0
  46. dw/locations.py +599 -0
  47. dw/log_setup.py +45 -0
  48. dw/loudness.py +82 -0
  49. dw/media_audio.py +217 -0
  50. dw/media_frames.py +367 -0
  51. dw/media_info.py +297 -0
  52. dw/pipeline_processors/chain.py +821 -0
  53. dw/pipeline_processors/config_objects.py +237 -0
  54. dw/pipeline_processors/pipeline.py +2297 -0
  55. dw/pipeline_processors/remote.py +46 -0
  56. dw/plan.py +920 -0
  57. dw/previous_results.py +411 -0
  58. dw/probe_paths.py +59 -0
  59. dw/prompt_schema.json +48 -0
  60. dw/prompt_weighting.py +378 -0
  61. dw/prompts.py +159 -0
  62. dw/realize.py +250 -0
  63. dw/reference_limits.py +215 -0
  64. dw/reference_names.py +125 -0
  65. dw/repl.py +338 -0
  66. dw/repl_commands.py +836 -0
  67. dw/repl_worker.py +159 -0
  68. dw/result.py +1720 -0
  69. dw/result_fps.py +82 -0
  70. dw/run.py +162 -0
  71. dw/runs.py +768 -0
  72. dw/scalar_result_validation.py +97 -0
  73. dw/schema.py +283 -0
  74. dw/security.py +1038 -0
  75. dw/select_validation.py +115 -0
  76. dw/serve.py +277 -0
  77. dw/server/__init__.py +2 -0
  78. dw/server/app.py +4586 -0
  79. dw/server/assess.py +132 -0
  80. dw/server/catalog_shape.py +487 -0
  81. dw/server/enhancers.py +129 -0
  82. dw/server/exports.py +480 -0
  83. dw/server/guides.py +257 -0
  84. dw/server/jobs.py +1561 -0
  85. dw/server/mcp_mount.py +95 -0
  86. dw/server/netinfo.py +124 -0
  87. dw/server/observed_cost.py +379 -0
  88. dw/server/sysinfo.py +71 -0
  89. dw/server/ui/assets/abap-08VXUWAP.js +1 -0
  90. dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
  91. dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  92. dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
  93. dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
  94. dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
  95. dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
  96. dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
  97. dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
  98. dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
  99. dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
  100. dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
  101. dw/server/ui/assets/css-DIMkf-bt.js +3 -0
  102. dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
  103. dw/server/ui/assets/cssMode-CPznxfY8.js +1 -0
  104. dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
  105. dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
  106. dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  107. dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
  108. dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
  109. dw/server/ui/assets/editor.api-CpWcotrd.js +847 -0
  110. dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
  111. dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
  112. dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
  113. dw/server/ui/assets/freemarker2-CXtRM8N4.js +3 -0
  114. dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  115. dw/server/ui/assets/go-C-y9NEjX.js +1 -0
  116. dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
  117. dw/server/ui/assets/handlebars-N7x-6NMY.js +1 -0
  118. dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
  119. dw/server/ui/assets/html-PhsdjHSr.js +1 -0
  120. dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
  121. dw/server/ui/assets/htmlMode-Dgj0SEok.js +1 -0
  122. dw/server/ui/assets/index-3Vw6WAPW.css +1 -0
  123. dw/server/ui/assets/index-DgrYhQd9.js +43 -0
  124. dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
  125. dw/server/ui/assets/java-BEtHBSE6.js +1 -0
  126. dw/server/ui/assets/javascript-BJqN9Qhv.js +1 -0
  127. dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
  128. dw/server/ui/assets/jsonMode-DbM4SWSv.js +7 -0
  129. dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
  130. dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
  131. dw/server/ui/assets/less-B9JPFI3C.js +2 -0
  132. dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
  133. dw/server/ui/assets/liquid-BWr8lEc4.js +1 -0
  134. dw/server/ui/assets/lspLanguageFeatures-C1iGuDyZ.js +4 -0
  135. dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
  136. dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
  137. dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
  138. dw/server/ui/assets/mdx-DAdMi_0p.js +1 -0
  139. dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
  140. dw/server/ui/assets/monaco--ixms01u.css +1 -0
  141. dw/server/ui/assets/monaco-BGCeEqaw.js +56 -0
  142. dw/server/ui/assets/msdax-DauUninz.js +1 -0
  143. dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
  144. dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  145. dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
  146. dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  147. dw/server/ui/assets/perl-oz_6vUea.js +1 -0
  148. dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
  149. dw/server/ui/assets/php-nr791fC2.js +1 -0
  150. dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
  151. dw/server/ui/assets/postiats-43DmfD33.js +1 -0
  152. dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
  153. dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
  154. dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
  155. dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
  156. dw/server/ui/assets/python-Bcn70HdC.js +1 -0
  157. dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
  158. dw/server/ui/assets/r-BwWrilGY.js +1 -0
  159. dw/server/ui/assets/razor-D1HmNnby.js +1 -0
  160. dw/server/ui/assets/redis-ClamHrr6.js +1 -0
  161. dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
  162. dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  163. dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
  164. dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
  165. dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
  166. dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
  167. dw/server/ui/assets/scheme-BeGwcela.js +1 -0
  168. dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
  169. dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
  170. dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
  171. dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
  172. dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  173. dw/server/ui/assets/sql-NEE52Syq.js +1 -0
  174. dw/server/ui/assets/st-DbInun42.js +1 -0
  175. dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
  176. dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  177. dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
  178. dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
  179. dw/server/ui/assets/tsMode-D6u0XmOW.js +11 -0
  180. dw/server/ui/assets/twig-De2hgUGE.js +1 -0
  181. dw/server/ui/assets/typescript-BU6v-LMV.js +1 -0
  182. dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
  183. dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
  184. dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
  185. dw/server/ui/assets/workers-Cn7cTUKr.js +1 -0
  186. dw/server/ui/assets/xml--0LP2Lwk.js +1 -0
  187. dw/server/ui/assets/yaml-mpBg9jnt.js +1 -0
  188. dw/server/ui/index.html +17 -0
  189. dw/server/updater.py +192 -0
  190. dw/settings.py +98 -0
  191. dw/shot_span_preflight.py +116 -0
  192. dw/shots.py +359 -0
  193. dw/slice_preflight.py +148 -0
  194. dw/step.py +187 -0
  195. dw/step_cache.py +442 -0
  196. dw/subfolders.py +107 -0
  197. dw/task_domains.py +307 -0
  198. dw/tasks/assess.py +826 -0
  199. dw/tasks/audio_transcription.py +88 -0
  200. dw/tasks/audio_utils.py +1862 -0
  201. dw/tasks/background_remover.py +43 -0
  202. dw/tasks/borders.py +113 -0
  203. dw/tasks/compose_text.py +74 -0
  204. dw/tasks/concat_videos.py +300 -0
  205. dw/tasks/depth_estimator.py +54 -0
  206. dw/tasks/diffusion_upscale.py +109 -0
  207. dw/tasks/dissolve_videos.py +342 -0
  208. dw/tasks/format_messages.py +24 -0
  209. dw/tasks/gather.py +173 -0
  210. dw/tasks/grade.py +97 -0
  211. dw/tasks/image_to_text.py +43 -0
  212. dw/tasks/image_utils.py +764 -0
  213. dw/tasks/interpolate_frames.py +252 -0
  214. dw/tasks/judge.py +68 -0
  215. dw/tasks/model_cache.py +55 -0
  216. dw/tasks/pair_audio.py +268 -0
  217. dw/tasks/qr_code.py +19 -0
  218. dw/tasks/restore_faces.py +175 -0
  219. dw/tasks/rife_model.py +192 -0
  220. dw/tasks/segment.py +121 -0
  221. dw/tasks/select.py +111 -0
  222. dw/tasks/speech_generation.py +228 -0
  223. dw/tasks/stabilize.py +129 -0
  224. dw/tasks/task.py +920 -0
  225. dw/tasks/tensor_image.py +57 -0
  226. dw/tasks/text_generation.py +169 -0
  227. dw/tasks/text_sections.py +80 -0
  228. dw/tasks/upscale.py +203 -0
  229. dw/tasks/video_utils.py +624 -0
  230. dw/tasks/zoe_depth.py +71 -0
  231. dw/teacache.py +381 -0
  232. dw/teacache_models.json +99 -0
  233. dw/test.py +29 -0
  234. dw/type_helpers.py +231 -0
  235. dw/validate.py +68 -0
  236. dw/variable_constraints.py +444 -0
  237. dw/variables.py +443 -0
  238. dw/video_extensions.py +141 -0
  239. dw/vram_estimate.py +116 -0
  240. dw/worker.py +764 -0
  241. dw/workflow.py +2007 -0
  242. dw/workflow_schema.json +1346 -0
  243. dw/workflow_sources.py +383 -0
  244. dw/workflows/h3_context_ir.json +57 -0
  245. dw/workflows/test.json +31 -0
  246. dw/workspace.py +730 -0
  247. dw_mcp/__init__.py +6 -0
  248. dw_mcp/__main__.py +133 -0
  249. dw_mcp/assets.py +336 -0
  250. dw_mcp/authoring.py +114 -0
  251. dw_mcp/catalog.py +360 -0
  252. dw_mcp/client.py +486 -0
  253. dw_mcp/diagnose.py +371 -0
  254. dw_mcp/exports.py +84 -0
  255. dw_mcp/guides.py +35 -0
  256. dw_mcp/media.py +638 -0
  257. dw_mcp/models.py +97 -0
  258. dw_mcp/prompts.py +104 -0
  259. dw_mcp/server.py +1343 -0
  260. 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