diffusers-workflow 0.8.0__py3-none-any.whl → 0.9.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.8.0.dist-info → diffusers_workflow-0.9.0.dist-info}/METADATA +1 -1
- {diffusers_workflow-0.8.0.dist-info → diffusers_workflow-0.9.0.dist-info}/RECORD +14 -14
- dw/docs/MCP.md +1 -1
- dw/docs/RELEASING.md +60 -0
- dw/docs/REMOTE.md +5 -0
- dw/plan.py +67 -13
- dw/server/routes/jobs.py +7 -5
- dw/tasks/joins.py +10 -0
- dw_mcp/exports.py +15 -12
- dw_mcp/tools_jobs.py +10 -11
- {diffusers_workflow-0.8.0.dist-info → diffusers_workflow-0.9.0.dist-info}/WHEEL +0 -0
- {diffusers_workflow-0.8.0.dist-info → diffusers_workflow-0.9.0.dist-info}/entry_points.txt +0 -0
- {diffusers_workflow-0.8.0.dist-info → diffusers_workflow-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {diffusers_workflow-0.8.0.dist-info → diffusers_workflow-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
diffusers_workflow-0.
|
|
1
|
+
diffusers_workflow-0.9.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
2
2
|
dw/__init__.py,sha256=fWaCvN4c5PtD97ZufrMz0okq33fxTWHmJp9UCuluouw,19183
|
|
3
3
|
dw/adapter_compatibility.py,sha256=OAJdCDsiU29DX-vW5DIGN3kUcapbq3Bz6vU6GCgV_6w,11316
|
|
4
4
|
dw/argument_media.py,sha256=yeXBDAoFnLhMKb-YFxtqmqgXhwqmj3Eh-sJblSxsa_4,12816
|
|
@@ -33,7 +33,7 @@ dw/media_types.py,sha256=to2bGLpfF__EJnXxis-t0SHlzgx_WzlK6t0roqLf24g,8045
|
|
|
33
33
|
dw/output_extraction.py,sha256=Q4OB2Lnz8yF9sK6H2svBDNuf4G8shEuQRExEaVieUXU,9610
|
|
34
34
|
dw/path_redaction.py,sha256=Zv8sGmZQ0meQYinMhqo10IJKt5egqkjekbiTSPoBVJg,2949
|
|
35
35
|
dw/pipeline_ownership.py,sha256=sY90vQVLBXzAeyR6jVygGm4i_FqfSeGp7up_0SvNSYE,12944
|
|
36
|
-
dw/plan.py,sha256=
|
|
36
|
+
dw/plan.py,sha256=7yif6_PKWYOHROLWSPtswoSnnN_-mwgoduZ4hB9_gXg,41470
|
|
37
37
|
dw/previous_results.py,sha256=Ano0nl0714eEgJd76e4Xu-1EC3vvaE4MsdvHVTIBMoU,16313
|
|
38
38
|
dw/probe_paths.py,sha256=iJgy4G256zELyzi4Aol4_qifejjZsUtdeEEDWryAB0o,2683
|
|
39
39
|
dw/prompt_schema.json,sha256=5e7NWgce28cf-YWEWwyDjQQ90ki0BcI4tczTyr_d8lQ,1934
|
|
@@ -86,12 +86,12 @@ dw/docs/ARCHITECTURE.md,sha256=RFIW1eSQ6l25uA5QKJWDns1i8iJgGoqXxwq-PrHZzuw,35102
|
|
|
86
86
|
dw/docs/DEPENDENCIES.md,sha256=avxinFm52qDToYi2_C0M39OTXNo1ERqxZj6t_0HIUjE,4142
|
|
87
87
|
dw/docs/IP_ADAPTER.md,sha256=AAHaUIfbnOojHfT0noIeypbhdYPluR7sbZeVn5FpbjU,3730
|
|
88
88
|
dw/docs/LORAS.md,sha256=lwug-7yQp7O1AHJfzv5jzvH8Vta4KMAt5G8m6khyMH8,6895
|
|
89
|
-
dw/docs/MCP.md,sha256=
|
|
89
|
+
dw/docs/MCP.md,sha256=L7vXp9j8VmzupKJlMkmUi7ZMlXR1QRHij9xJxil-URE,56114
|
|
90
90
|
dw/docs/PROMPT_WEIGHTING.md,sha256=Pvr25d3yvS8SlnYwX9qc61VvTa8yFMxK_1zMLvrEigI,3031
|
|
91
91
|
dw/docs/QUANTIZATION.md,sha256=NZCL-09bDM-JhzV1WqK2JGkL1la8VoH9fie1X39arsE,7678
|
|
92
92
|
dw/docs/RECIPES_24GB.md,sha256=j7CCtMCNg830o6xD7C1DPz7dXRHvBQkbJaViRnH8AR4,16035
|
|
93
|
-
dw/docs/RELEASING.md,sha256=
|
|
94
|
-
dw/docs/REMOTE.md,sha256=
|
|
93
|
+
dw/docs/RELEASING.md,sha256=LUISTzZYjxcy1ND0MXE-1rYnGaEJ6Jei0DaSZf4-5DY,41460
|
|
94
|
+
dw/docs/REMOTE.md,sha256=quum2Y54g2bp2rgXg9WkSPY3zhVGHEpMkEAiL6oAQDU,6650
|
|
95
95
|
dw/docs/SECURITY.md,sha256=HYkBnAGlX83moyCryHylYdyWFodyZzzF7J38elebYGA,17900
|
|
96
96
|
dw/docs/SECURITY_QUICKREF.md,sha256=-xZNIV6j1oWcoy5B-oHKqh3ko2zJgB2fuNMiDV4f1zY,3537
|
|
97
97
|
dw/docs/SERVER.md,sha256=XAMoVdlCAjTR0AMtupWv5nuDUGClEz31YBfaOObSBlc,52418
|
|
@@ -134,7 +134,7 @@ dw/server/routes/__init__.py,sha256=Z6hnzGs1jdXuY_1C74t09_e1QDYWOxWbCnyjo6_qKB0,
|
|
|
134
134
|
dw/server/routes/assets.py,sha256=D3VSCSo_xKJeBZ-5ZMWWRtMlr1db7iiDPDFIUAPg8_I,17517
|
|
135
135
|
dw/server/routes/files.py,sha256=6c7ySIPe7o8HSc8Z7M2b1fKWonQdkC8S_TPAVppCxOo,7125
|
|
136
136
|
dw/server/routes/gallery.py,sha256=pwE1qsizXpqL-kMieM6UbXAgAHOpdQ1V3MR53510PsU,15107
|
|
137
|
-
dw/server/routes/jobs.py,sha256=
|
|
137
|
+
dw/server/routes/jobs.py,sha256=lrtGkk7_CzgVsmO0MhXWQAleOm0--LwKqahG5WBHNo4,34451
|
|
138
138
|
dw/server/routes/library.py,sha256=GiUFqYuin8nHx1pU28gJsr8m6YvgBNAWoUZjUbT9PYM,29738
|
|
139
139
|
dw/server/routes/loras.py,sha256=OVxlXutJs1ThSsTC3R31pg44sUaV9iH8PhY0Xy9UyVQ,9289
|
|
140
140
|
dw/server/routes/media.py,sha256=vRhH52iZ8BwmPeNtN0wS_zM6A3qj92p2V8E2DwIifN8,26005
|
|
@@ -259,7 +259,7 @@ dw/tasks/image_to_text.py,sha256=iNza67G5h24y6GIs22mCyLOFgqsAOfz9hf88jBn16iY,161
|
|
|
259
259
|
dw/tasks/image_utils.py,sha256=4mAHIob7Yh8JEdekI5X5yI_dJSKia1PTrRDGjljAOfc,25397
|
|
260
260
|
dw/tasks/interpolate_frames.py,sha256=Jxp0GcETheu1SBwAP6QrCCRKRs9XfoF5SHzwfUkIwCk,9272
|
|
261
261
|
dw/tasks/join_into_song.py,sha256=54nr2-Q5MOUV4rTtBeWyJV-oK1misJ86LV2eRRV_BTo,17825
|
|
262
|
-
dw/tasks/joins.py,sha256=
|
|
262
|
+
dw/tasks/joins.py,sha256=BbzoTOfc6WJDXHWxdm0dVjA-awa634U3SpQT83RoNDc,25861
|
|
263
263
|
dw/tasks/judge.py,sha256=fdT6a20x66tCCP37d2NnVFrZAPiuhduuBvg_bsAdtSg,2312
|
|
264
264
|
dw/tasks/loop_bed.py,sha256=JQ-Zx8qpNiobBtdmRPU4nwYiLJyrje6hAsT8QauXkz0,35563
|
|
265
265
|
dw/tasks/model_cache.py,sha256=Bh97x5mFFFzT8M8_LiHo0a1izsaIqvsQyIURPkjKxTk,1879
|
|
@@ -289,7 +289,7 @@ dw_mcp/catalog.py,sha256=RMPqJ_Vtgoc3wFlRp0xd4GedbQVICaI0rkXwiqbvzus,15219
|
|
|
289
289
|
dw_mcp/client.py,sha256=YhIJY6Nlf2B1MbBfdcMZySsQ5PFvdEVF41dFL8MRhm4,22166
|
|
290
290
|
dw_mcp/confine.py,sha256=bAitZXmyoNMFkmzNa7WwzYNgKhVLT5nUsMOAGe5t-bI,2766
|
|
291
291
|
dw_mcp/diagnose.py,sha256=V9-8gNf1eMVjzRDilj9FiD8dcnHu5AZ9nRVm9AcXIYk,17392
|
|
292
|
-
dw_mcp/exports.py,sha256=
|
|
292
|
+
dw_mcp/exports.py,sha256=U37jOe40K1HVM4ic_CpYFvkZiupDVWPCzWiOJYqvc8g,4388
|
|
293
293
|
dw_mcp/guides.py,sha256=QDvofzCO5FOXTNO5DW82UKqDn2IdvwNEr7rB91fHmxE,1762
|
|
294
294
|
dw_mcp/loras.py,sha256=dp0fUZ9YfECYnDemJNvq62ZbvXjJJaEAEPdkRKIDVGM,875
|
|
295
295
|
dw_mcp/media.py,sha256=1MzdoyO9p2B1PKAm8Neu9gCSHYmECEQ_X4boMBHuTvo,20937
|
|
@@ -298,11 +298,11 @@ dw_mcp/prompts.py,sha256=ZgbcATJbWUxHlebt8ssH2PlUc3nBD6rrVLdFLJSo8TM,3944
|
|
|
298
298
|
dw_mcp/server.py,sha256=dEVWxyyN5yh-lIkc4Yub3S9TwcFOV5TSYNikqyiVXXg,7738
|
|
299
299
|
dw_mcp/tools_authoring.py,sha256=FBelAhWf1OWRQX290rF2Eoy2fY1fpMoG5lZXytBFgpY,12313
|
|
300
300
|
dw_mcp/tools_catalog.py,sha256=cZf32oDGgc-VOeKT4Z8guFDSFzQwPR5HG1oTCW0TR7M,22086
|
|
301
|
-
dw_mcp/tools_jobs.py,sha256=
|
|
301
|
+
dw_mcp/tools_jobs.py,sha256=e4DY1f6xqlHtK_M4K2gg7OyjjVu5_iw4mn-2laNuB78,11988
|
|
302
302
|
dw_mcp/tools_media.py,sha256=HsMQ5ihUmkUcZ1NELtJMNbxfpSjdfFLQA6-Q9jOhe0w,16848
|
|
303
303
|
dw_mcp/workspaces.py,sha256=OF6oVMQx_pkTuRMLvbOYkpVWwcA6GQwaEf5k0oxqBnI,8449
|
|
304
|
-
diffusers_workflow-0.
|
|
305
|
-
diffusers_workflow-0.
|
|
306
|
-
diffusers_workflow-0.
|
|
307
|
-
diffusers_workflow-0.
|
|
308
|
-
diffusers_workflow-0.
|
|
304
|
+
diffusers_workflow-0.9.0.dist-info/METADATA,sha256=lZa1AWCNfxpdjcpmfEXQ-GqVnIoMyU4bGLukSVnct80,14938
|
|
305
|
+
diffusers_workflow-0.9.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
306
|
+
diffusers_workflow-0.9.0.dist-info/entry_points.txt,sha256=Dem_AOzV-Kcpr9AASlSvJ-S5sn3f6TQu6ABb6BbApho,148
|
|
307
|
+
diffusers_workflow-0.9.0.dist-info/top_level.txt,sha256=o-uFAN_oUOrhXfMKO39HsMs2r19L9LykxoVKj2S9q7U,10
|
|
308
|
+
diffusers_workflow-0.9.0.dist-info/RECORD,,
|
dw/docs/MCP.md
CHANGED
|
@@ -308,7 +308,7 @@ references written in the same session.
|
|
|
308
308
|
| `run_workflow(workflow_path=None, inline_workflow=None, arguments=None, acknowledged_cost=False, workspace=None, wait_seconds=0)` | exactly one of `workflow_path` (a catalog name from `list_workflows`, with or without `.json`, or a path to a workflow file on the server) or `inline_workflow`, optional `arguments`, `acknowledged_cost`, `workspace`, `wait_seconds` | Queue a workflow for generation. Returns as soon as the job is queued - unless `wait_seconds` is above 0, in which case the call then waits on the queued job exactly as `wait_for_job(job_id, timeout_seconds=wait_seconds)` would (same per-call cap, clamped not honoured) and the result carries the queued-job fields plus that wait's (`status`, `still_running`, `waited_seconds`, `timeout_requested_seconds`, `timeout_applied_seconds`, `timeout_capped`, the slim `job`); when the cap covers the job's runtime one call is the run and the wait, and a `still_running: true` result is followed with `wait_for_job` as before. `workspace` names the workspace for this one call without switching the session to it - use it to pin a job whose `output:` or `asset:` references live in a workspace other than the session's - `acknowledged_cost` is `true` or the bound `{fingerprint, minutes, downloads}` from the validate plan; a 409 means the plan changed and the message carries the new estimate, and nothing is waited on |
|
|
309
309
|
| `get_job(job_id)` | `job_id` | Get a job's status, warnings, output manifest, error and traceback; each manifest entry's `subfolder` is the in-run subfolder the step declared - by convention `final` for the deliverable, `intermediate` for scratch, `''` for none. A running job also carries `progress` (below) |
|
|
310
310
|
| `get_job_workflow(job_id)` | `job_id` | The REST equivalent is `GET /api/jobs/{id}/workflow` (see [SERVER.md](SERVER.md#jobs-api)). The workflow the job actually ran. `realized: true` means every mutable input is pinned (arguments, seed, prompts, `output:latest`); `false` means the job predates run tracking and this is the definition as submitted. Pass it to `save_workflow` to keep it under a name |
|
|
311
|
-
| `export_job(job_id, overwrite=False)` | `job_id`, `overwrite` | Gather one finished job into `<workspace>/exports/<job id>/` on the server: the realized workflow, the run's manifest, the job row, a README, and copies of the assets, earlier-run inputs and outputs. Returns the directory, a zip URL, the file list with sizes and the total. The three JSON files are in the zip, not repeated here - get_job_workflow and get_job serve them individually. **The directory is on the machine running the server**, like `download_output`'s destination.
|
|
311
|
+
| `export_job(job_id, overwrite=False)` | `job_id`, `overwrite` | Gather one finished job into `<workspace>/exports/<job id>/` on the server: the realized workflow, the run's manifest, the job row, a README, and copies of the assets, earlier-run inputs and outputs. Returns the directory, a zip URL, the file list with sizes and the total. The three JSON files are in the zip, not repeated here - get_job_workflow and get_job serve them individually. **The directory is on the machine running the server**, like `download_output`'s destination. The zip needs no token (`/exports/*.zip` is ungated like `/outputs`, #592), so `auth_required` is false and the agent fetches `open_url` itself (prefixing a relative one with the server address) and unpacks it into `exports/` under the session's working directory (a deliverable, not a temp file) - the archive already unpacks into one folder named after the job id, so do not create that folder first; only an agent without HTTP gives the user `open_url`. `true` is kept as a forward guard for a gated zip, when `open_url` is handed to the person instead |
|
|
312
312
|
| `get_job_events(job_id, after=-1, limit=200)` | `job_id`, `after`, `limit` | Get a page of a job's progress events |
|
|
313
313
|
| `wait_for_job(job_id, timeout_seconds=20)` | `job_id`, `timeout_seconds` | Block until a job reaches a terminal status, or `timeout_seconds` elapses. **One call blocks for at most the server's cap** — 55 seconds unless the deployment sets `DW_MCP_MAX_WAIT_SECONDS` higher, and the tool's description states the live value; a larger `timeout_seconds` is clamped, not honoured. Ask for the job's `plan.estimate` plus a margin: under the cap that is one call for the whole job, over it one call per cap. A deployment raises the cap only as far as its clients (and anything between them and the server) hold one silent HTTP request open - Claude Code's limit is `MCP_TOOL_TIMEOUT`. Every reply carries `waited_seconds`, `timeout_requested_seconds`, `timeout_applied_seconds` and `timeout_capped`, so a capped return is distinguishable from an elapsed one. Use instead of hand-polling `get_job`/`get_job_events` in a loop; if it returns `still_running: true`, call it again. Returns a slim job - status, warnings, error, `run_id` and `run_version` (the run's `v5`, as the gallery labels it), and the manifest once finished - without the arguments; `get_job` has those. A running job also carries `progress` (below) |
|
|
314
314
|
| `cancel_job(job_id)` | `job_id` | Ask a queued or running job to stop |
|
dw/docs/RELEASING.md
CHANGED
|
@@ -7,6 +7,63 @@ notes from commits at tag time (see below). This section is a scratch pad
|
|
|
7
7
|
for items a branch's author wants the next release note to name; clear it
|
|
8
8
|
when a release ships.
|
|
9
9
|
|
|
10
|
+
### 0.9.0
|
|
11
|
+
|
|
12
|
+
<!-- Drafted from v0.8.0..dd74e17b (develop). Paste into the GitHub release body once the tag has published: gh release edit v0.9.0 --notes-file ... -->
|
|
13
|
+
|
|
14
|
+
A smaller range than 0.8.0: the LoRA catalog grows to cover three more
|
|
15
|
+
bases, the cost planner is corrected for list-driven and `for_each` steps,
|
|
16
|
+
and `export_job` reports its zip's real auth gating. No upgrade-order
|
|
17
|
+
changes.
|
|
18
|
+
|
|
19
|
+
**LoRA catalog** (docs/LORAS.md)
|
|
20
|
+
|
|
21
|
+
- 11 LTX-2.5 entries added: IC-LoRAs for alpha generation, clean plate,
|
|
22
|
+
colorization, day-to-night, layout-to-render, detail refinement,
|
|
23
|
+
restoration, SDR-to-HDR and water simulation, plus cinemagraph and
|
|
24
|
+
slow-motion control.
|
|
25
|
+
- MiniMax-H3 goes to 13 trial and 3 rejected entries beyond the 0.8.0 set:
|
|
26
|
+
styles, speech, orbits, action and motion, and a diffusers-native 4-step
|
|
27
|
+
turbo. FastVideo FastH3 (`.diff` keys), RAVEN (unrecognised prefix, loads
|
|
28
|
+
nothing silently) and TaoMate (lowercase `lora_a`/`lora_b`) are rejected.
|
|
29
|
+
- New bases: Qwen-Image-2.1 (10 trial, including three few-step distills
|
|
30
|
+
that need their scheduler and sigma overrides, plus edit LoRAs; Fun-Acc
|
|
31
|
+
rejected) and Z-Image-Turbo (13 trial). The Z-Image entries match
|
|
32
|
+
`Tongyi-MAI/Z-Image-Turbo` only; whether they apply to the SDNQ
|
|
33
|
+
checkpoint is untested.
|
|
34
|
+
- Every new entry pins the repo's current sha, and its header was read
|
|
35
|
+
against the installed diffusers converter.
|
|
36
|
+
|
|
37
|
+
**Cost planning**
|
|
38
|
+
|
|
39
|
+
- `other_device` figures are re-priced for `for_each` counts and shifted
|
|
40
|
+
list drivers (#589, #590).
|
|
41
|
+
- A shifted per-entry field in a list driver resets to unknown, and a
|
|
42
|
+
summed child figure takes the children's basis (#593). A numeric string
|
|
43
|
+
in a list-driver entry compares as its number (#593).
|
|
44
|
+
- Measured MPS cost entries for `templates/ltx2/text-to-video` and Music 3
|
|
45
|
+
(#590).
|
|
46
|
+
|
|
47
|
+
**Fixes**
|
|
48
|
+
|
|
49
|
+
- `assemble-and-score` threads `sample_rate` into its edit join, and mixed-rate
|
|
50
|
+
shots resampled to a pinned `sample_rate` no longer draw a warning advising
|
|
51
|
+
you to pass it (#594).
|
|
52
|
+
- `export_job` reports the zip's real gating: `/exports/*.zip` is ungated
|
|
53
|
+
like `/outputs`, so `auth_required` is false whether or not the server has
|
|
54
|
+
a token. The MCP `next` text and the skills now say to fetch `open_url` and
|
|
55
|
+
unpack into `exports/` to bring a project home (#595, #592).
|
|
56
|
+
|
|
57
|
+
**Plugin**
|
|
58
|
+
|
|
59
|
+
- The `ltx-2.5`, `minimax-h3` and `minimax-music3` skills moved
|
|
60
|
+
request-specific detail (the LTX caption spec, H3 checkpoint and LoRA
|
|
61
|
+
combinations and cuts recipes, Music 3 loudness) into `references/`
|
|
62
|
+
beside each `SKILL.md`, which are read when the skill points there. The
|
|
63
|
+
12 KiB cap applies to `SKILL.md` alone; a new test fails on an unlinked
|
|
64
|
+
reference or a dead link.
|
|
65
|
+
- The H3 and LTX skills point at `list_loras`.
|
|
66
|
+
|
|
10
67
|
### 0.8.0
|
|
11
68
|
|
|
12
69
|
<!-- Drafted from v0.7.0..a9e1e72b (develop). Paste into the GitHub release body once the tag has published: gh release edit v0.8.0 --notes-file ... -->
|
|
@@ -38,6 +95,9 @@ upscaler and two security fixes.
|
|
|
38
95
|
- GHSA-crqf-hw9p-r739: `create_workspace`'s MCP result is `name`,
|
|
39
96
|
`default`, `current` and `next` only; `list_workspaces(detail=true)` is
|
|
40
97
|
the opt-in for folder paths. `POST /api/workspaces` is unchanged.
|
|
98
|
+
- `GET /api/loras/recommend` and `recommend_loras`: `hub_error` names the
|
|
99
|
+
exception type, or the HTTP status, never the exception's text, which
|
|
100
|
+
could name the server's HF cache directory. The log keeps the full error.
|
|
41
101
|
- UI lockfile bumps for open Dependabot alerts (devalue, dompurify,
|
|
42
102
|
brace-expansion, undici).
|
|
43
103
|
|
dw/docs/REMOTE.md
CHANGED
|
@@ -48,6 +48,11 @@ responses add an `absolute_url` / `absolute_zip_url` built from it; nothing
|
|
|
48
48
|
guesses this from request headers, so an unconfigured server omits the
|
|
49
49
|
field rather than composing a wrong origin.
|
|
50
50
|
|
|
51
|
+
The export zip, like `/outputs` and `/inputs` files, needs no token (#592), so
|
|
52
|
+
an agent with HTTP fetches `export_job`'s `open_url` itself, prefixing a
|
|
53
|
+
relative one with the address it reaches the server at; `DW_PUBLIC_URL` is for
|
|
54
|
+
the URLs handed to a person.
|
|
55
|
+
|
|
51
56
|
## Browser
|
|
52
57
|
|
|
53
58
|
Open `http://<box>:8765`. Click the key icon next to the theme toggle,
|
dw/plan.py
CHANGED
|
@@ -17,6 +17,7 @@ import copy
|
|
|
17
17
|
import hashlib
|
|
18
18
|
import json
|
|
19
19
|
import logging
|
|
20
|
+
import math
|
|
20
21
|
import os
|
|
21
22
|
|
|
22
23
|
from huggingface_hub import model_info
|
|
@@ -340,6 +341,36 @@ def _driver_comparable(value):
|
|
|
340
341
|
return json.dumps(value, sort_keys=True, default=str)
|
|
341
342
|
|
|
342
343
|
|
|
344
|
+
def _numeric_fields(entries):
|
|
345
|
+
"""{field: {values}} over the numeric fields of a list's dict entries. A
|
|
346
|
+
numeric string ("243") counts as its number, as `_driver_comparable` does
|
|
347
|
+
for a scalar driver."""
|
|
348
|
+
fields = {}
|
|
349
|
+
if not isinstance(entries, list):
|
|
350
|
+
return fields
|
|
351
|
+
for entry in entries:
|
|
352
|
+
if not isinstance(entry, dict):
|
|
353
|
+
continue
|
|
354
|
+
for key, value in entry.items():
|
|
355
|
+
number = _driver_comparable(value)
|
|
356
|
+
if isinstance(number, float) and math.isfinite(number):
|
|
357
|
+
fields.setdefault(key, set()).add(number)
|
|
358
|
+
return fields
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _list_entry_field_shifted(default_entries, effective_entries):
|
|
362
|
+
"""Whether a list driver's entries carry a numeric field (a per-shot
|
|
363
|
+
`num_frames`, say) with a value none of the default entries had. The
|
|
364
|
+
curated figure was measured over the default entries' values, so a value
|
|
365
|
+
outside them has no matching bucket - the #267 rule applied inside a
|
|
366
|
+
list driver (#593)."""
|
|
367
|
+
measured = _numeric_fields(default_entries)
|
|
368
|
+
for field, values in _numeric_fields(effective_entries).items():
|
|
369
|
+
if field in measured and not values <= measured[field]:
|
|
370
|
+
return True
|
|
371
|
+
return False
|
|
372
|
+
|
|
373
|
+
|
|
343
374
|
def _scalar_driver_shifted(definition, expanded, list_entries):
|
|
344
375
|
"""Whether a declared, non-list `cost_driver` was overridden away from
|
|
345
376
|
the default value the curated `cost` was measured against (#267).
|
|
@@ -352,10 +383,12 @@ def _scalar_driver_shifted(definition, expanded, list_entries):
|
|
|
352
383
|
defaults = definition.get("variables") or {}
|
|
353
384
|
effective = expanded.get("variables") or {}
|
|
354
385
|
for name in _declared_drivers(definition):
|
|
355
|
-
if name in list_entries:
|
|
356
|
-
continue
|
|
357
386
|
default_value = defaults.get(name)
|
|
358
387
|
if isinstance(default_value, list):
|
|
388
|
+
if _list_entry_field_shifted(default_value, effective.get(name)):
|
|
389
|
+
return True
|
|
390
|
+
continue
|
|
391
|
+
if name in list_entries:
|
|
359
392
|
continue
|
|
360
393
|
if _driver_comparable(effective.get(name)) != _driver_comparable(default_value):
|
|
361
394
|
return True
|
|
@@ -365,7 +398,7 @@ def _scalar_driver_shifted(definition, expanded, list_entries):
|
|
|
365
398
|
def _own_price(definition, expanded, list_entries, device, measured_entries):
|
|
366
399
|
"""The workflow's own price, reset to unknown when a scalar driver moved."""
|
|
367
400
|
own = _price(definition.get("cost"), device, list_entries, measured_entries or {})
|
|
368
|
-
if own["basis"]
|
|
401
|
+
if own["basis"] in (CATALOG, OTHER_DEVICE) and _scalar_driver_shifted(
|
|
369
402
|
definition, expanded, list_entries
|
|
370
403
|
):
|
|
371
404
|
# A scalar cost_driver (H3's num_frames, say) moved away from the
|
|
@@ -434,7 +467,7 @@ def _child_catalog_price(child_definition, child_cost, step_arguments, device):
|
|
|
434
467
|
child_list_entries = _list_entries(child_definition, child_expanded)
|
|
435
468
|
child = _price(child_cost, device, child_list_entries, child_measured_entries)
|
|
436
469
|
if (
|
|
437
|
-
child["basis"]
|
|
470
|
+
child["basis"] in (CATALOG, OTHER_DEVICE)
|
|
438
471
|
and child_definition is not None
|
|
439
472
|
and _scalar_driver_shifted(child_definition, child_expanded, child_list_entries)
|
|
440
473
|
):
|
|
@@ -458,6 +491,7 @@ class _ChildTotals:
|
|
|
458
491
|
self.all_observed = True
|
|
459
492
|
self.runs = []
|
|
460
493
|
self.measured_on = set()
|
|
494
|
+
self.bases = []
|
|
461
495
|
|
|
462
496
|
def add(self, path, child, observed):
|
|
463
497
|
self.had_child = True
|
|
@@ -469,7 +503,9 @@ class _ChildTotals:
|
|
|
469
503
|
if child["minutes"] is None:
|
|
470
504
|
self.partial = True
|
|
471
505
|
self.unpriced.append(path)
|
|
472
|
-
|
|
506
|
+
return
|
|
507
|
+
self.bases.append((child["basis"], child.get("measured_on")))
|
|
508
|
+
if self.minutes is not None:
|
|
473
509
|
self.minutes += child["minutes"]
|
|
474
510
|
else:
|
|
475
511
|
self.minutes = child["minutes"]
|
|
@@ -509,6 +545,20 @@ def _rolled_up_estimate(own, totals, device, cached_steps, total_steps):
|
|
|
509
545
|
top_measured_on = (
|
|
510
546
|
next(iter(totals.measured_on)) if len(totals.measured_on) == 1 else None
|
|
511
547
|
)
|
|
548
|
+
if top_basis == UNKNOWN and minutes is not None and totals.bases:
|
|
549
|
+
# A number must not carry the basis a caller reads as "no number":
|
|
550
|
+
# the parent has no cost of its own, but its priced children do, so
|
|
551
|
+
# the sum takes their basis (#593)
|
|
552
|
+
kinds = {basis for basis, _ in totals.bases}
|
|
553
|
+
if OTHER_DEVICE in kinds:
|
|
554
|
+
top_basis = OTHER_DEVICE
|
|
555
|
+
elif kinds == {CATALOG}:
|
|
556
|
+
top_basis = CATALOG
|
|
557
|
+
else:
|
|
558
|
+
top_basis = DERIVED
|
|
559
|
+
devices = {on for _, on in totals.bases}
|
|
560
|
+
if len(devices) == 1:
|
|
561
|
+
top_measured_on = next(iter(devices))
|
|
512
562
|
rounded = round(minutes, 1) if minutes is not None else None
|
|
513
563
|
result = {
|
|
514
564
|
"minutes": rounded,
|
|
@@ -696,18 +746,22 @@ def _price(cost, device, list_entries, measured_entries):
|
|
|
696
746
|
basis = OTHER_DEVICE
|
|
697
747
|
minutes = float(chosen.get("minutes", 0))
|
|
698
748
|
per = chosen.get("per_entry")
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
):
|
|
749
|
+
# Another device's figure is re-priced for the list length too (#589): it
|
|
750
|
+
# stays `other_device` (still not a measurement here), but a 32-entry
|
|
751
|
+
# for_each must not be quoted at the one-clip figure
|
|
752
|
+
if isinstance(per, dict) and per.get("variable") in list_entries:
|
|
704
753
|
count = list_entries[per["variable"]]
|
|
705
754
|
each = float(per.get("minutes", 0))
|
|
706
755
|
measured_with = int(per.get("entries", 0))
|
|
707
756
|
minutes = max(0.0, (minutes - each * measured_with) + each * count)
|
|
708
|
-
basis
|
|
709
|
-
|
|
710
|
-
|
|
757
|
+
if basis == CATALOG:
|
|
758
|
+
basis = PER_ENTRY
|
|
759
|
+
else:
|
|
760
|
+
minutes, repriced_basis = _repriced(minutes, list_entries, measured_entries)
|
|
761
|
+
if repriced_basis == UNKNOWN:
|
|
762
|
+
return {"minutes": None, "basis": UNKNOWN, "measured_on": None}
|
|
763
|
+
if basis == CATALOG:
|
|
764
|
+
basis = repriced_basis
|
|
711
765
|
return {"minutes": minutes, "basis": basis, "measured_on": chosen.get("name")}
|
|
712
766
|
|
|
713
767
|
|
dw/server/routes/jobs.py
CHANGED
|
@@ -455,11 +455,13 @@ def export_job_route(
|
|
|
455
455
|
absolute_zip_url = absolute_served_url(zip_path, ws)
|
|
456
456
|
if absolute_zip_url is not None:
|
|
457
457
|
body["absolute_zip_url"] = absolute_zip_url
|
|
458
|
-
#
|
|
459
|
-
#
|
|
460
|
-
#
|
|
461
|
-
#
|
|
462
|
-
|
|
458
|
+
# Whether the zip URL above needs a bearer token (#353, #592) - a
|
|
459
|
+
# property of the route, not of whether this server has a token. The
|
|
460
|
+
# auth middleware covers /api/ and /mcp only, and /exports/*.zip sits
|
|
461
|
+
# with /outputs and /inputs on dw/server/routes/files.py's ungated
|
|
462
|
+
# list, so it is false. export_job's `next` keeps its hand-it-over
|
|
463
|
+
# branch for the day /exports is gated.
|
|
464
|
+
body["auth_required"] = False
|
|
463
465
|
for key, name in (
|
|
464
466
|
("workflow", "workflow.json"),
|
|
465
467
|
("manifest", "manifest.json"),
|
dw/tasks/joins.py
CHANGED
|
@@ -94,6 +94,7 @@ def reconcile_sample_rates(command, videos, names, waveforms, sample_rate=None):
|
|
|
94
94
|
for video, waveform in zip(videos, waveforms)
|
|
95
95
|
if waveform is not None
|
|
96
96
|
]
|
|
97
|
+
pinned = bool(sample_rate)
|
|
97
98
|
sample_rate = sample_rate or (max(set(rates)) if rates else None)
|
|
98
99
|
if rates and len(set(rates)) == 1 and rates[0] != sample_rate:
|
|
99
100
|
# The inputs agree and the caller pinned another rate: converting
|
|
@@ -104,6 +105,15 @@ def reconcile_sample_rates(command, videos, names, waveforms, sample_rate=None):
|
|
|
104
105
|
command=command,
|
|
105
106
|
sample_rate=sample_rate,
|
|
106
107
|
)
|
|
108
|
+
elif rates and pinned and any(rate != sample_rate for rate in rates):
|
|
109
|
+
# Mixed inputs converted to the rate the caller (or the template)
|
|
110
|
+
# pinned: expected, and 'pass sample_rate' advice would be wrong (#594)
|
|
111
|
+
emit_log(
|
|
112
|
+
f"{command}: resampling tracks at {sorted(set(rates))} Hz to the "
|
|
113
|
+
f"requested {sample_rate} Hz",
|
|
114
|
+
command=command,
|
|
115
|
+
sample_rate=sample_rate,
|
|
116
|
+
)
|
|
107
117
|
elif rates and any(rate != sample_rate for rate in rates):
|
|
108
118
|
# emit_warning rather than logger.warning: resampling every track is
|
|
109
119
|
# an audio decision made on the caller's behalf, and a caller reading
|
dw_mcp/exports.py
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
The one thing this module has to keep saying: the directory it makes is on
|
|
4
4
|
the machine running dw.serve, which over a `dw.serve --mcp` endpoint is the
|
|
5
5
|
GPU box and not where the agent is. The zip URL is the way to it from
|
|
6
|
-
anywhere else
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
anywhere else, and it needs no token: /exports/*.zip is ungated like
|
|
7
|
+
/outputs (#592), so `auth_required` is false and the agent fetches it itself.
|
|
8
|
+
The hand-it-to-the-person branch (#353) stays for the day it is gated.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from dw_mcp.client import api_path
|
|
@@ -22,10 +22,10 @@ def export_job(client, job_id, overwrite=False):
|
|
|
22
22
|
here. The directory is on the server machine, not this one.
|
|
23
23
|
|
|
24
24
|
`auth_required` says whether the zip needs this server's bearer token
|
|
25
|
-
to open
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
to open. The server reports false - the zip route is ungated, token or
|
|
26
|
+
not (#592) - and `next` says to fetch `open_url` and unpack it. True
|
|
27
|
+
is a forward guard: a gated zip is for the *person* to open, since
|
|
28
|
+
this agent cannot attach the token to their browser. `open_url` is
|
|
29
29
|
`absolute_zip_url` when the server has one configured (`DW_PUBLIC_URL`
|
|
30
30
|
/ the `public_url` setting), else the relative `zip_url`."""
|
|
31
31
|
body = client.post_json(
|
|
@@ -58,11 +58,14 @@ def export_job(client, job_id, overwrite=False):
|
|
|
58
58
|
open_url = absolute_zip_url or zip_url
|
|
59
59
|
next_text = (
|
|
60
60
|
"The directory is on the server. To give the user the files, "
|
|
61
|
-
"fetch open_url
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
61
|
+
"fetch open_url with whatever HTTP you have - it needs no token; "
|
|
62
|
+
"prefix a relative one with the address you reach this server at "
|
|
63
|
+
"- and unpack it into exports/ under the session's working "
|
|
64
|
+
"directory - it is the user's deliverable, not a temporary file, "
|
|
65
|
+
"so not a scratch or temp directory. The archive already unpacks "
|
|
66
|
+
"into one folder named after the job id; do not create that "
|
|
67
|
+
"folder first or the id is doubled in the path. Only if you "
|
|
68
|
+
"cannot make HTTP requests, give the user open_url to open. "
|
|
66
69
|
"workflow.json, manifest.json and job.json are inside it - they "
|
|
67
70
|
"are not repeated here; get_job_workflow and get_job serve them "
|
|
68
71
|
"individually."
|
dw_mcp/tools_jobs.py
CHANGED
|
@@ -216,16 +216,15 @@ class JobTools:
|
|
|
216
216
|
repeated here - get_job_workflow and get_job serve them individually.
|
|
217
217
|
THE DIRECTORY IS ON THE MACHINE RUNNING THE SERVER, not on yours.
|
|
218
218
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
the
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
way. Refuses a job that is still running; refuses an existing
|
|
219
|
+
open_url is the zip and needs no token: fetch it with any HTTP
|
|
220
|
+
you have (prefix a relative one with the server's address) and
|
|
221
|
+
unpack it into exports/ under the session's working directory -
|
|
222
|
+
the user's deliverable, not a temp file; it already unpacks into
|
|
223
|
+
one folder named after the job id, so do not create that folder first.
|
|
224
|
+
Only without HTTP, give the user open_url to open. If `auth_required`
|
|
225
|
+
is ever true, the zip is gated: hand it over (see `next`).
|
|
226
|
+
Individual results stay reachable inline via
|
|
227
|
+
get_output_image/get_output_audio/get_output_frames
|
|
228
|
+
without the zip. Refuses a job that is still running; refuses an existing
|
|
230
229
|
export unless overwrite=true."""
|
|
231
230
|
return exports.export_job(self.client, job_id, overwrite=overwrite)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|