diff --git a/CLAUDE.md b/CLAUDE.md index 1808633b..4861cd5d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,7 +88,7 @@ Reserved names: `workflows`, `prompts`, `assets`, `outputs`, `exports`, workspace on the server (`ui/src/lib/Sidebar.svelte`) and the selected one named in the hash (`#/ws//...`). -The web UI has a page for it: `ui/src/lib/pages/AssetsPage.svelte` (#165) +The web UI has a page for it: `ui/src/lib/pages/AssetsPage.svelte` 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. @@ -164,8 +164,9 @@ references) are documented above in *Workflow sources* and *Type System*. - Values prefixed with `prompt:` load a stored prompt's `text` from the prompt library: `"prompt:name"` or `"prompt:folder/name"`. Resolved in `realize_args` (`dw/prompts.py`), rooted at the library rather than the workflow file. The library is `DW_PROMPT_DIR` / - `--prompt-dir`, else `./prompts` if it exists, else found by walking up from the - workflow file's directory + `--prompt-dir`, else the workspace's `prompts/` when a workspace was named, else + `./prompts` if it exists, else found by walking up from the workflow file's + directory, else the workspace's `prompts/` - A step's `result.subfolder` names a subfolder of the run directory for that step's files - by convention `final` for the deliverable and `intermediate` for the rest; any relative path (`shots/act-1`); `variable:`/`item:` allowed; no default. Mechanics under @@ -204,10 +205,10 @@ 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` +The catalog's IC-LoRA templates are `templates/ltx2/generative-upscale` and three +conditioning templates, all through `LTX2InContextPipeline` + +`LTX2ReferenceCondition`; the three run 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 @@ -221,13 +222,13 @@ 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. +under `model_name` directly rather than through `from_pretrained_arguments`, and +a walk that skipped it would report `[]` on a box missing only the IC-LoRA, +which then downloads 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. +Quantization configs are defined per-component in workflow JSON and instantiated in `dw/pipeline_processors/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. SDNQ pre-quantized models use a different pattern: `pre_load_modules` imports sdnq (registers with diffusers), then the entire pipeline loads from the pre-quantized repo. Optional `sdnq_optimize` applies quantized matmul post-load (CUDA/XPU only). @@ -248,7 +249,7 @@ The same translation reaches the settings that carry a device or a CUDA-only fea A `components` entry can additionally set `residency: "on_demand"`, which rests the component on the CPU and wraps its `forward`/`encode`/`decode` to move it to the device around each call (`apply_on_demand_placement` in `pipeline.py`). The wrappers use `functools.wraps` because callers introspect the signature — MiniMax H3's denoiser picks its arguments from `signature(transformer.forward)`. It is mutually exclusive with `group_offload` on the same component, and like `group_offload` it suppresses the wholesale `pipeline.to(device)` at load. -Settings in `~/.diffusers_helper/settings.json`: `device`, `enable_tf32`, `cudnn_benchmark`, `cudnn_deterministic`, `log_level`, `log_filename`. +Settings in `~/.diffusers_helper/settings.json` (`dw/settings.py`): `device`, `workspace`, `output_layout`, `public_url`, `enable_tf32`, `cudnn_benchmark`, `cudnn_deterministic`, `log_level`, `log_filename`. ## Security Rules @@ -279,9 +280,9 @@ same reason - default setup cannot load a pack. - **`previous_result:` references are checked statically too** — once the schema passes, `previous_result_reference_errors` (`dw/previous_results.py`) reports any literal `previous_result:` or `from_previous_result` naming no *earlier* step, with - the JSON path it sits at. References otherwise resolve lazily per step, so a step - renamed in one place and not another failed only when the run reached it, after - every step before it had generated. The definition is substituted before the check, + the JSON path it sits at. References otherwise resolve lazily per step, so without + the check a step renamed in one place and not another would fail only when the run + reached it, after every step before it had generated. The definition is substituted before the check, so a reference spelled by a *declared* variable is checked by its value; one spelled by an undeclared variable is itself a validation error (below) - **`for_each` expands before the reference check** — `validation_errors` substitutes @@ -294,32 +295,28 @@ same reason - default setup cannot load a pack. warning and not a complaint about the `for_each` list that did substitute: once a `variables` block exists, `replace_variables` refuses an undeclared reference, so it is a run that cannot start -- **The two MiniMax cut templates take one `shots` list** — since the stage-2 - rewrite (2026-09-11) `templates/minimax/dialogue-short` and `music-video` - have no `shot_N_*` variables; a scripted caller passes `shots` (entries +- **The two MiniMax cut templates take one `shots` list** — + `templates/minimax/dialogue-short` and `music-video` have no per-shot + variables; a scripted caller passes `shots` (entries `{name, prompt, references, num_frames}` and `{name, prompt, start_frame}`). - The members are `shot@` in the manifest and the gallery. This is the - breaking change the next release note should name. The CLI and REPL only + The members are `shot@` in the manifest and the gallery. The CLI and REPL only 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 `@` 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 + writes carry that `@` in their base name, so `OUTPUT_REFERENCE_PATTERN` + and `ASSET_REFERENCE_PATTERN` accept it: every file the server names can be + named back to it. `@` is safe in a path — not a separator, not `..`, and + containment is still `validate_path`'s — but a name may not *start* with + one. `reference_name_errors` (`dw/reference_names.py`) checks the shape of + every `asset:`/`prompt:`/`output:` reference in the definition in + `validation_errors`, so a bad name is refused before the queue, 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: 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"` @@ -334,9 +331,9 @@ same reason - default setup cannot load a pack. job that fails on its first step, and `POST /api/validate` takes the same `arguments` (plus an `asset:`/`prompt:`/`output:` existence check against the workspace) so the free pre-flight covers the part the caller wrote. - A workflow that declares no variables takes no arguments at all - those were - dropped in silence, since `Workflow.run` only substitutes when a `variables` - block exists. A valid `POST /api/validate` answer also carries `plan` + A workflow that declares no variables takes no arguments at all, since + `Workflow.run` only substitutes when a `variables` block exists - any passed + would be dropped in silence. A valid `POST /api/validate` answer also carries `plan` (`dw/plan.py`): the fingerprint of the work, step and list counts, `downloads_required` and a cost `estimate` with its `basis` - the number an agent quotes, with `basis` saying whether it was measured for this list @@ -362,8 +359,8 @@ same reason - default setup cannot load a pack. Identity is the workflow's path under a `workflows/` tree, else its file name, else its `id`; the run id is `-<8 hex of the spec>`, with a `-N` counter if taken. A sub-workflow inherits the parent's run directory and writes no manifest of its own. - `--output-layout flat` / `DW_OUTPUT_LAYOUT` / the `output_layout` setting restores the - old layout. The gallery groups a workflow's runs under one folder by stripping the run + `--output-layout flat` / `DW_OUTPUT_LAYOUT` / the `output_layout` setting writes + straight into the output directory with no run directories. The gallery groups a workflow's runs under one folder by stripping the run id (`strip_run_id`). The realized workflow is written into the same directory as `workflow.json` (`dw/realize.py`, `write_realized_workflow`), and the manifest's `workflow` block carries `realized`, `prompts` (the stored prompts inlined) and @@ -373,10 +370,9 @@ same reason - default setup cannot load a pack. `POST /api/jobs/{id}/export` gathers one finished job into `/exports//` and `GET /exports/.zip` streams it. - **A run has a number, and it is not derived from the listing** - a file's name - is per *step*, so four runs of one workflow write four files called - `AcornWarsCutAndScore-film.7-0.0.mp4` and the gallery drew four identical - captions: the run id told them apart but is not something anyone says out - loud, so an agent had no way to name one of them to a person. Every run now + is per *step*, so four runs of one workflow write four files with the same + name; the run id tells them apart but is not something anyone says out + loud, so the number is how an agent names one of them to a person. Every run takes an ordinal, `assign_run_version` (`dw/runs.py`) at the moment `Workflow.run` opens the run directory, recorded as `version` in `manifest.json` and read back by `run_versions`. Assigned once and never @@ -429,20 +425,16 @@ same reason - default setup cannot load a pack. whatever the step named) (`sectionBySubfolder`, `ui/src/lib/results.ts`), unchanged for a run that chose none. `file_base_name` may not contain a separator - it is a name, not a path - and it *replaces* the derived - `-.` base rather than prefixing it (#100), so + `-.` base rather than prefixing it, so two steps in one subfolder that set the same one collide onto `output_file_path`'s `-2` counter. Every `workflows/templates/**` file with two or more saving steps marks each one `final`/`intermediate` (`tests/test_template_subfolders.py` pins the rule; - `dw/workflows/` builtins stay unmarked - a role is the parent's to assign). That moved - the templates' outputs into `/final/` and `/intermediate/`: an - `output: