Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
9b00eae
fix: #141 #142 - a task step's signature, and a soundtrack that follo…
dkackman Sep 14, 2026
df80db4
feat: #122 - a step nothing references does not run
dkackman Sep 14, 2026
e537520
feat: #96 - a variable's bound is declared, checked before anything l…
dkackman Sep 14, 2026
33f0b21
feat: #93 - what this box's own history says a workflow costs
dkackman Sep 14, 2026
95e9e6b
Merge fixes for #122 #141 #142 #96 #93 - elision, task signatures, so…
dkackman Sep 14, 2026
ac55683
fix: #93 - the observed figure says which accelerator it is about
dkackman Sep 14, 2026
1d9320f
docs: #145 - proposal for a declared bound that reaches a list entry
dkackman Sep 14, 2026
0492571
fix: #150 - a rerun's superseded pipeline is released before the reload
dkackman Sep 14, 2026
74f96da
feat: #146 - the music video's singer can be a portrait that already …
dkackman Sep 14, 2026
cd71fc5
feat: #147 #148 #149 - an H3 checkpoint's schedule and alpha are the …
dkackman Sep 14, 2026
ec36be1
feat: #153 - templates/ltx2/diffusion-decode, so the other decoder ca…
dkackman Sep 14, 2026
86207aa
Merge fixes for #146 #147 #148 #149 #150 #153 - H3 checkpoint knobs, …
dkackman Sep 14, 2026
09e5d79
fix: #154 #155 #157 #158 - the plan quotes this box's history, an H3 …
dkackman Sep 14, 2026
c0435d2
Merge fixes for #154 #155 #157 #158 - observed cost in the plan, the …
dkackman Sep 14, 2026
20e59d3
fix: #153 #159 - the diffusion decoder names the path it can actually…
dkackman Sep 14, 2026
0335888
Merge fixes for #153 #159 - the diffusion decoder's real attention pa…
dkackman Sep 14, 2026
348dcbe
fix: #153 - the decoder's memory figures are the ones an empty card m…
dkackman Sep 14, 2026
8445686
fix: #145 #162 - a constraint reaches a list entry, and a for_each me…
dkackman Sep 14, 2026
f741aaa
fix: #161 - the deliverable is measured as written, and a muxed one g…
dkackman Sep 14, 2026
a526fde
feat: #151 #152 - the LTX-2.5 family gets an identity route and two r…
dkackman Sep 14, 2026
cf2f5fe
feat: #165 - an assets page in the web UI
dkackman Sep 14, 2026
03c53b5
Merge fixes for #145 #151 #152 #161 #162 #165 - list-entry constraint…
dkackman Sep 14, 2026
5ddb630
fix: #145 - get_workflow(variables_only) names the entry bound beside…
dkackman Sep 14, 2026
b599cf3
fix: a loras entry counts toward plan.downloads_required
dkackman Sep 14, 2026
1e0951e
feat: add bulk asset download as zip and enhance asset selection UI
dkackman Sep 15, 2026
c4a8fb1
fix: the assets page says why a shared library follows the workspace
dkackman Sep 15, 2026
3336ab0
refactor: one selection for the two contact sheets, one zip for the t…
dkackman Sep 15, 2026
ad0c2cf
fix: archive routes - one search probe, one compression policy, one tail
dkackman Sep 15, 2026
3bf4ac9
fix: a bulk bar counts what its actions will touch
dkackman Sep 15, 2026
c98279c
feat: GET /api/assets names its libraries and what they shadow
dkackman Sep 15, 2026
fdf630a
refactor: optimize asset resolution and enhance error handling
dkackman Sep 15, 2026
10454f9
fix: a symlink out of the asset library is a miss, not a 500
dkackman Sep 15, 2026
20e02ba
feat: the assets page sections by library
dkackman Sep 15, 2026
8ea5282
fix: select-all takes only what a bulk action can reach
dkackman Sep 15, 2026
26514fc
fix: cleanups the review named - one format, one flex, one withicon
dkackman Sep 15, 2026
8fdf222
Merge branch 'assets-page' of https://github.com/dkackman/diffusers-w…
dkackman Sep 15, 2026
f23d5ad
fix(server): #166 - validate_workflow checks stored variable defaults…
dkackman Sep 15, 2026
a199d37
Merge fix for #166 - validate_workflow checks stored variable defaults
dkackman Sep 15, 2026
6beafde
fix(validation): #168 - refuse a non-MIME result.content_type at vali…
dkackman Sep 15, 2026
3db782e
Merge fix for #168 - refuse a non-MIME result.content_type at validat…
dkackman Sep 15, 2026
84dae64
fix(deps): #169 - exclude transformers 5.17.0, whose TextToAudioPipel…
dkackman Sep 16, 2026
f8d7452
Merge fix for #169 - exclude transformers 5.17.0, breaks generate_speech
dkackman Sep 16, 2026
a495260
docs(proposals): #170 - orphaned run directory visibility
dkackman Sep 16, 2026
5cc0760
Merge proposal for #170 - orphaned run directory visibility
dkackman Sep 16, 2026
516fd2f
docs(proposals): #171 - step-callback lead-in instrumentation design
dkackman Sep 16, 2026
31ee710
Merge proposal for #171 - step-callback lead-in instrumentation
dkackman Sep 16, 2026
f645232
docs(proposals): #174 - H3 video mux headroom warning decision
dkackman Sep 16, 2026
72dfb2b
Merge proposal for #174 - H3 video mux headroom warning decision
dkackman Sep 16, 2026
ce82f06
fix(mcp,result): #170 - only_orphans gallery listing, #174 - split he…
dkackman Sep 16, 2026
aab4ef5
Merge fix/170-174-orphan-listing-headroom-warning
dkackman Sep 16, 2026
ed6cae6
docs(proposals): #167 - workspace folders design
dkackman Sep 16, 2026
e98f560
Merge proposal for #167 - workspace folders design
dkackman Sep 16, 2026
c93eef8
docs(proposals): #174 - amend headroom-warning decision, pre-encode c…
dkackman Sep 16, 2026
1b61fdb
Merge proposal amendment for #174 - H3 video mux headroom warning
dkackman Sep 16, 2026
09ce1ef
fix(events): #176 - generic phase-stall watchdog
dkackman Sep 16, 2026
09e6901
Merge fix/176-phase-stall-watchdog
dkackman Sep 16, 2026
191eb05
fix(server): #177 - invalidate workspace-usage cache on delete
dkackman Sep 16, 2026
dfe6ee9
Merge fix/177-workspace-usage-cache-invalidation
dkackman Sep 16, 2026
2172cff
fix(dw): #178 - surface unsatisfiable kernel build as validate_workfl…
dkackman Sep 16, 2026
fff8a03
Merge fix/178-natten-kernel-availability
dkackman Sep 16, 2026
264dbd6
fix(mcp): #179 - accept validate_workflow/run_workflow parameter aliases
dkackman Sep 16, 2026
95bbbc3
Merge fix/179-mcp-validate-run-param-aliases
dkackman Sep 16, 2026
24a3971
fix(dw): #180 - resample the score at its own rate instead of relabel…
dkackman Sep 16, 2026
cf3d50a
Merge fix/180-slice-audio-rate-relabel
dkackman Sep 16, 2026
da568c6
Merge pull request #182 from dkackman/assets-page
dkackman Sep 16, 2026
c5637c1
Refactor code structure for improved readability and maintainability
dkackman Sep 16, 2026
f398897
Merge branch 'develop' of https://github.com/dkackman/diffusers-workf…
dkackman Sep 16, 2026
26e5f67
refactor(tests): clean up import statements and simplify skipif decor…
dkackman Sep 16, 2026
490a42a
fix(events): #176 - a phase-stall report stays out of the job's warnings
dkackman Sep 17, 2026
6d788d8
fix(server): an asset library's origin follows its directory, not its…
dkackman Sep 17, 2026
02732b2
fix(server): no asset library is a 404, not a 500
dkackman Sep 17, 2026
5918620
fix(elision): #122 - a sub-workflow step is kept whether or not it ha…
dkackman Sep 17, 2026
55a8ccd
fix(server): #170 - an orphan run holds nothing but bookkeeping
dkackman Sep 17, 2026
88ef55d
fix(server): #170 - the REST and catalog docstrings say what an orpha…
dkackman Sep 17, 2026
1959a57
fix(workflow): #150 - a key another step still maps to is not release…
dkackman Sep 17, 2026
31d3658
fix(workflow): #145 - constraints are applied after list-entry refere…
dkackman Sep 17, 2026
8e6813c
fix(workflow): #150 - a superseded key counts as shared only among th…
dkackman Sep 17, 2026
1559f3c
fix(validation): #178 - the kernel probe runs once per process per ty…
dkackman Sep 17, 2026
9e80a6f
fix(validation): #178 - a kernel probe fault is not memoized
dkackman Sep 17, 2026
ae55618
fix(server): #93 - a catalog listing reads the job watermark once
dkackman Sep 17, 2026
1303929
fix(server): a validate miss on a server with no asset library says so
dkackman Sep 17, 2026
da568f3
fix(workflow): #150 - a superseded key is shared only when a running …
dkackman Sep 17, 2026
d0e915e
fix(server): #170 - a dotfile does not make a run an output
dkackman Sep 17, 2026
c0ee0f3
refactor(server): RUN_BOOKKEEPING_FILES reuses the run-file constants
dkackman Sep 17, 2026
c84199a
Merge fix/pr183-merge-readiness - stall warnings, asset origin/404, e…
dkackman Sep 17, 2026
603a0a7
Merge master back into develop (release 0.4.0-beta.5, idea issue temp…
dkackman Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ common` by `GET /api/assets`, and is written to only when a call says so
Reserved names: `workflows`, `prompts`, `assets`, `outputs`, `exports`,
`common`.

The web UI has a page for it: `ui/src/lib/pages/AssetsPage.svelte` (#165)
reads `GET /api/assets` and shows the library the way the gallery shows
outputs, tagged by `origin` so a shadowed or read-only entry is visible
before a 403 explains it.

### Workflow sources

`dw/workflow_sources.py` is the server's workflow search path: the writable
Expand Down Expand Up @@ -190,6 +195,29 @@ The same conventions, written for an agent composing a workflow over MCP, are
the `Authoring a workflow from an agent` section of docs/WORKFLOW_GUIDE.md;
change both when one changes.

### LTX-2.5 IC-LoRAs

`templates/ltx2/generative-upscale` was the only IC-LoRA use in the catalog;
three more conditioning templates join it (#151, #152), all through
`LTX2InContextPipeline` + `LTX2ReferenceCondition`, all at
`reference_downscale_factor: 1` (the upscaler's is 2). `reference-sheet`
drives Ingredients — the family's only identity route, and the first two
templates here whose reference is a file the workflow did not make; the sheet
is a still, so a `loop_frames` step (`dw/tasks/video_utils.py`, the video
analogue of `loop_audio`) laps it into the static video the LoRA reads
through its 121-frame bucket. `restore-deblur` and `restore-decompression`
each invert one defect and no other. Every number in the three is the vendor
card's and is pinned by `tests/test_ltx2_ic_loras.py`; the trained caption
form is a *different* genre from a T2V shot caption, so those stored prompts
are tagged `ic-lora` and `tests/test_ltx_prompt_library.py` checks them
against their own convention rather than the 150-220-word paragraph rule.
The weights are `gated: auto` on Hugging Face — per repo, so a box that pulls
one can still 403 on another. A `loras` entry counts toward
`plan.downloads_required` (`_collect_sources`, `dw/plan.py`): it names its repo
under `model_name` directly rather than through `from_pretrained_arguments`, so
the walk used to miss it and a box holding every base weight but not the
IC-LoRA answered `[]` and then pulled it mid-run.

### Quantization Support

Quantization configs are defined per-component in workflow JSON and instantiated in `config_objects.py`. Supported frameworks: BitsAndBytes, TorchAO, GGUF, SDNQ, optimum-quanto. The `config_type` field is a free-form string — new quantization backends work automatically via dynamic import.
Expand Down Expand Up @@ -266,6 +294,23 @@ same reason - default setup cannot load a pack.
take `name=value` strings, and a string handed to a list variable is
comma-split - so `shots` can only be supplied over the API/MCP (a JSON
body); `python -m dw.run` runs the templates' default list
- **A reference name is checked for its shape before the queue, and `@` is
part of it** — a `for_each` member is `<group>@<entry>` and the files it
writes carry that `@` in their base name, which `OUTPUT_REFERENCE_PATTERN`
and `ASSET_REFERENCE_PATTERN` refused: a whole class of files the server
itself named could not be named back to it, so `output:` on a shot a
list-driven template produced forced a re-render or an `upload_asset` round
trip (#162). `@` is safe in a path — not a separator, not `..`, and
containment is still `validate_path`'s — and a name still may not *start*
with one. The other half is that the refusal arrived at run time, after the
queue, from a message that described a *valid* name and never said which
character it objected to: `reference_name_errors` (`dw/reference_names.py`)
now checks the shape of every `asset:`/`prompt:`/`output:` reference in the
definition in `validation_errors`, and `_name_fault` (`dw/security.py`)
names the offending character and position. Shape only — *existence*
depends on the workspace and on what pruning has taken, so it stays where it
was: the validate route resolves the caller's `arguments` against the
workspace, and the definition's own references resolve at run time
- **Cartesian product explosion** — multiple `previous_result` references multiply: 4 images × 3 masks = 12 iterations
- **Component sharing requires exact key matching** between `shared_components` and `reused_components`
- **Built-in workflows** need explicit argument mapping: `"prompt": "variable:prompt"`
Expand Down Expand Up @@ -368,6 +413,118 @@ same reason - default setup cannot load a pack.
one entry in the table; `tests/test_task_domains.py` pins every entry to a
real parameter of a real command so a rename cannot leave one checking
nothing
- **`cost` is curated, `observed` is derived, and they are different fields** —
`dw/workflow_schema.json` defines `cost` as *"Never derived"*, so nothing
writes one; `dw/server/observed_cost.py` reports a sibling built from this
box's own `jobs.sqlite` rows (#93). Four rules, each a way the naive median
would lie: runs are bucketed by the workflow's declared `cost_drivers` (a
list driver on its *length*, so two four-shot runs are comparable however
different their prompts) and the bucket reported is the one the *defaults*
give, keeping it comparable to a curated figure; `cold_minutes` and
`warm_minutes` are separate, each with its own run count, and only the cold
one is comparable to `cost` (wall clock including model load); a run whose
every manifest entry is `reused` wrote nothing and is excluded; and a run
whose persisted events hit `MAX_PERSISTED_EVENTS` without a `loading` phase
is `unclassified_runs` rather than assumed warm. Everything comes off the
job row in one query, so a figure survives a pruned run directory, and the
aggregate caches against `JobHistory.watermark()` rather than a file mtime —
a job landing changes every figure and changes no file. The compact listing
carries only `observed_minutes`/`observed_runs` (#101 budget); the full
block is in the full listing and `GET /api/workflows/{name}/variables`. The
raw `GET /api/workflows/{name}` is left verbatim, since the editor saves
what it reads back. A `cost_drivers` entry naming no declared variable is
dropped, and `tests/test_observed_cost.py` sweeps the catalog for one.
`plan.estimate` quotes the observed figure ahead of the curated one
(`basis: "observed"`, with `runs`) — `basis: "unknown"` has to mean nobody
has a number, not nobody curated one (#154). Only the *cold* median, only
when the history is this backend's, and only for the bucket the caller's
own arguments fall in (`ObservedCosts.observed(name, definition,
arguments)`); a resized list finds no bucket and falls back to the curated
figure. Nothing is added for a composed child, since an observed run
already ran it. An inline definition has no catalog name, so no history
- **An H3 adapter is checked against the partition its step denoises on** —
`ref2va` loads `transformer_ref` alone, so diffusers puts whatever
`lora_weight_name` names straight onto it: an FL2VA turbo LoRA on a
reference step runs, succeeds, and only retains identity worse (#149,
#155). `dw/adapter_compatibility.py` refuses the mispairing in
`validation_errors` (so `POST /api/validate` and the pre-queue check both
catch it, at `arguments.<name>` when the caller supplied it) and *warns*
on a file name carrying neither `ref2v` nor `fl2v` — the name of a future
reference-trained checkpoint cannot be predicted, so the escape hatch
stays open while the one documented mistake is closed. The workflow names
and the partition each denoises against are diffusers'
(`MiniMaxH3Blocks._workflow_map`, pinned by `tests/test_h3_adapters.py`);
the file-name convention is MiniMax's and is swept against the catalog's
own defaults
- **An elided step says whether anyone decided it** — `warn_elided` used to
tell every caller their reference was probably misspelled, including the
one who deliberately passed `singer_reference` and so bought the elision
`music-video` advertises (#146, #157). `overriding_variables`
(`dw/elision.py`) compares the definition as *written* against the
substituted steps: a step reached only through a variable whose value no
longer names it was replaced on purpose, and its record carries
`overridden_by` and drops the diagnosis. A variable no step reads is not
how the step was reached, so that case keeps the old wording
- **A deliverable with no audio headroom warns** — a track at or above
−0.5 dBFS is written anyway and said out loud (`warn_without_headroom`,
`dw/result.py`, kind `audio_no_headroom`), for both a saved audio file and
a muxed video: a clipped file succeeds, and a consumer that cannot listen
had `peak_dbfs` with no rule to read it against — `get_gallery_metadata`'s
hint taught the near-silent end of the range only (#158). A warning, not a
gain change: what level a deliverable sits at is the workflow's to decide,
and `normalize_audio` is the step that decides it. The two Music 3
templates decide it now (#159) - `music` and `music-video` peak-normalize
to -1 dBFS, the level `assemble-and-score` has always used, because the
warning was firing on their own defaults every run. `music-video`
normalizes only the track going into the mux, not the slices that condition
the shots, so the picture is unchanged; `music`'s deliverable moves to the
new `balanced` step, which renames the file an `output:` reference names
- **A deliverable is measured as written, not as handed to the writer** —
`warn_without_headroom` reads the waveform, and the encoder sits downstream
of it: a song normalized to exactly -1.0 dBFS came back out of
`music-video`'s AAC mux at **+0.94**, so a clean default run shipped a
clipped file and nothing warned (#161). `warn_if_written_above_full_scale`
(`dw/result.py`, kind `audio_clipped`) probes the file it just wrote and
warns when it decodes at or above 0 dBFS — whatever the encoder did, that
is the number a consumer's decoder sees. Only for a file that can carry a
soundtrack, and silent when `warn_without_headroom` already spoke for that
file, since two warnings would be two answers to one mistake. The encode's
overshoot is material-dependent — about 0.1 dB on an mp3 and about 1.9 dB
on the AAC mux of the same song — so no target chosen up front can be
*known* to be enough, which is why reading the file back is the half that
stops the next instance. The half that fixes this one: every template
whose deliverable ends in a `pair_audio` mux (`music-video`,
`assemble-and-score`, `dissolve-between-shots`) normalizes to **-3 dBFS**;
`music`, an mp3, keeps -1
- **A variable's bound is declared by the author, checked three times** — a
model's own rule about a value (H3's `num_frames` is `17 * n + 5` from 124
to 345) is a property of the model, so it lives in the workflow rather than
in engine code, as a `variable_constraints` entry (`dw/variable_constraints.py`).
One shape, not two: it takes a chain step's `frame_snap` field names, and a
chain writes `"frame_snap": "constraint:num_frames"` rather than repeating
the numbers. `snap: "up"` rounds an off-grid value to the next legal one
and warns (at validation *and* through `emit_warning`, so it reaches the
job's `warnings`); without `snap` an off-grid value is refused. The bounds
hold for the value the run will use, matching diffusers' own
`align_num_frames`, which snaps before it range-checks — so 108 is accepted
(it becomes 124) and 346 refused (it would become 362). LTX-2.5's templates
declare the `8 * n + 1` grid with *no* `snap`, because those pipelines floor
an off-grid count rather than raising: rounding up here would be a second
silent change to the length. Checked in `validation_errors` (so
`POST /api/validate`, `validate_workflow` and the pre-queue check all
refuse it at `arguments.<name>` / `variables.<name>`), at run time in
`apply_constraints` before anything loads, and reported beside the default
by `list_workflows` (terse) / `get_workflow(variables_only=true)` — that
last part is what stops the next consumer picking 61 (#96).
`tests/test_variable_constraints.py` sweeps the whole catalog and pins every
declared number to the diffusers symbol it derives from. A constraint key is
a plain variable name and is matched wherever a value by that name sits -
top-level variable *or* a field of a `for_each` entry (#145), the latter only
where a step consumes that field as `item:<name>` (`entry_constraint_fields`),
so the bound follows the value into the pipeline argument rather than the
name into the JSON. An entry violation is reported at
`arguments.shots[0].num_frames`, and the rule is reported beside the field in
the catalog's `lists` block as well as in `constraints`
- **Step cache**: a process-wide singleton (`dw/step_cache.py`) consulted by every `Workflow.run`, including server jobs; entries are keyed by `(workflow id, step name)` and validated against the output
*root*, never the per-run directory - a run directory is new every execution and would
defeat the cache; disabled entirely when the workflow sets no `seed`; a hit reports the earlier run's files with `reused: true` and writes nothing new; `memory clear` drops it. This is why "Run again" on a seeded workflow finishes instantly and generates nothing - the job page says so when every step was reused, and `POST /api/jobs/{id}/rerun` with `{"new_seed": true}` (MCP `rerun_job(new_seed=True)`) draws a fresh seed into the workflow's seed variable, which is the way to get a different image
Expand Down
Loading