feat(enhance): LoRA switches per template (#218) #219

Merged
cameron merged 2 commits from feat/enhance-lora-switches into master 2026-11-03 19:08:04 +00:00
Owner

Part of #218 (ImageApi side; the app switches and sliders follow in FileViewer-React).

What changes

enhance.toml: [[template.lora]] { id, label, default, strength, min, max } per template.

  • Each id drives every workflow node titled imageapi:lora:<id>, so Wan's high/low pair shares one switch.
  • strength is the scale when on (default 1.0). min/max are the slider's range (default 0.0–1.5). Validation requires 0 <= min < max with strength between them.

The scale is a factor on each node's exported strength.

  • It multiplies strength_model, and strength_clip when present (LoraLoader).
  • So 1.0 means as exported and 0 means off.
  • A pair exported at different strengths keeps its ratio: 1.0/0.8 at scale 0.5 gives 0.5/0.4.
  • The graph is never rewired.

GET /enhance/templates: each template gains loras: [{id, label, default, strength, min, max}].

POST /enhance: takes loras: {id: true | false | scale}.

  • true means strength.
  • false, or a scale <= 0, means off.
  • Other scales are clamped to min..max.
  • A missing id uses its default.
  • An id the template doesn't offer is a 400.

Jobs: the resolved scale per id (0 = off) goes in a new enhance_jobs.loras column (JSON; additive migration). It's returned as loras: {id: scale} in the job view, so a retry or adjust repeats it exactly.

Startup checks:

  • A lora-titled node with no declaration keeps its exported strength.
  • A declaration with no matching node isn't offered.
  • Both log a startup warning.
  • A lora-titled node without a numeric strength_model fails that template.

Docs: enhance.example.toml gains an annotated Animate entry, and the CLAUDE.md Enhance section is updated.

Several LoRAs, and ordering

  • Several at once: a template can declare any number of switches, and one request can turn on and scale any combination, e.g. {style: 0.8, detail: 0.5}.
  • Ordering isn't exposed: LoRA patches add to the weights, so their order in the graph doesn't change the result.
  • Not covered: choosing an arbitrary LoRA file at request time. That needs nodes inserted dynamically. It would be a separate field later, so this shape stays valid.

Using it

  1. In the 4090's Animate workflow, add a LoraLoaderModelOnly after lightx2v on the high path, and another on the low path.
  2. Title both imageapi:lora:<id> and re-export the workflow in API format.
  3. Add a [[template.lora]] with that id to the prod enhance.toml.

Until then nothing changes: Animate offers no switches.

Testing

  • Unit tests:
    • config parsing, ranges and validation;
    • request resolution (on/off/scale, clamping, <= 0 as off);
    • one switch driving both nodes;
    • a scale multiplying a pair with different exported strengths;
    • default on with a configured strength;
    • an unknown id refused;
    • strength_clip scaled;
    • undeclared and missing switches reported as warnings;
    • a node with no strength rejected.
  • Integration test: switches listed with their ranges, then on, default-off and 0.5-scale requests sent to ComfyUI. The resolved scales are stored on the job and returned by GET /enhance/jobs/{id}, plus the 400s. It uses a new fixture: the Animate workflow plus a generic imageapi:lora:style pair.
  • cargo test: 1083 pass. Clippy is clean.

🤖 Generated with Claude Code

Part of #218 (ImageApi side; the app switches and sliders follow in FileViewer-React). ## What changes **`enhance.toml`:** `[[template.lora]]` `{ id, label, default, strength, min, max }` per template. - Each id drives every workflow node titled `imageapi:lora:<id>`, so Wan's high/low pair shares one switch. - `strength` is the scale when on (default 1.0). `min`/`max` are the slider's range (default 0.0–1.5). Validation requires `0 <= min < max` with `strength` between them. **The scale is a factor on each node's exported strength.** - It multiplies `strength_model`, and `strength_clip` when present (LoraLoader). - So 1.0 means as exported and 0 means off. - A pair exported at different strengths keeps its ratio: 1.0/0.8 at scale 0.5 gives 0.5/0.4. - The graph is never rewired. **`GET /enhance/templates`:** each template gains `loras: [{id, label, default, strength, min, max}]`. **`POST /enhance`:** takes `loras: {id: true | false | scale}`. - `true` means `strength`. - `false`, or a scale <= 0, means off. - Other scales are clamped to `min..max`. - A missing id uses its default. - An id the template doesn't offer is a 400. **Jobs:** the resolved scale per id (0 = off) goes in a new `enhance_jobs.loras` column (JSON; additive migration). It's returned as `loras: {id: scale}` in the job view, so a retry or adjust repeats it exactly. **Startup checks:** - A lora-titled node with no declaration keeps its exported strength. - A declaration with no matching node isn't offered. - Both log a startup warning. - A lora-titled node without a numeric `strength_model` fails that template. **Docs:** `enhance.example.toml` gains an annotated Animate entry, and the CLAUDE.md Enhance section is updated. ## Several LoRAs, and ordering - **Several at once:** a template can declare any number of switches, and one request can turn on and scale any combination, e.g. `{style: 0.8, detail: 0.5}`. - **Ordering** isn't exposed: LoRA patches add to the weights, so their order in the graph doesn't change the result. - **Not covered:** choosing an arbitrary LoRA file at request time. That needs nodes inserted dynamically. It would be a separate field later, so this shape stays valid. ## Using it 1. In the 4090's Animate workflow, add a `LoraLoaderModelOnly` after lightx2v on the high path, and another on the low path. 2. Title both `imageapi:lora:<id>` and re-export the workflow in API format. 3. Add a `[[template.lora]]` with that id to the prod `enhance.toml`. Until then nothing changes: Animate offers no switches. ## Testing - Unit tests: - config parsing, ranges and validation; - request resolution (on/off/scale, clamping, <= 0 as off); - one switch driving both nodes; - a scale multiplying a pair with different exported strengths; - default on with a configured strength; - an unknown id refused; - `strength_clip` scaled; - undeclared and missing switches reported as warnings; - a node with no strength rejected. - Integration test: switches listed with their ranges, then on, default-off and 0.5-scale requests sent to ComfyUI. The resolved scales are stored on the job and returned by `GET /enhance/jobs/{id}`, plus the 400s. It uses a new fixture: the Animate workflow plus a generic `imageapi:lora:style` pair. - `cargo test`: 1083 pass. Clippy is clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
cameron added 1 commit 2026-11-03 18:53:58 +00:00
A template can offer optional LoRAs the app turns on or off.

- enhance.toml: [[template.lora]] { id, label, default } per template.
  Each id drives every workflow node titled `imageapi:lora:<id>`, so
  Wan's high- and low-noise pair share one switch.
- POST /enhance takes `loras: {id: bool}`; a missing id uses its default,
  an id the template doesn't offer is a 400. Off sets strength_model
  (and strength_clip when present) to 0, so the graph is never rewired.
- The resolved switches are stored on the job (new enhance_jobs.loras
  JSON column) and returned in the job view, so a retry or adjust can
  repeat them; GET /enhance/templates lists each template's switches.
- A lora-titled node without a declaration keeps its exported strength,
  and a declaration without nodes isn't offered; both are startup
  warnings. A lora-titled node without a numeric strength_model fails
  the template.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cameron added 1 commit 2026-11-03 19:01:08 +00:00
So the app builds against the final shape: each [[template.lora]] gains
`strength` (the scale when on, default 1.0) and `min` / `max` (the
slider's range, default 0.0-1.5).

- POST /enhance `loras` takes true / false or a scale: true = strength,
  false or <= 0 = off, a scale is clamped to min..max.
- The scale multiplies each node's exported strength_model /
  strength_clip, so 1.0 = as exported and a high/low pair keeps its
  ratio.
- Jobs and the job view store the resolved scale per id (0 = off);
  GET /enhance/templates lists strength, min and max.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cameron merged commit 5e1519fa80 into master 2026-11-03 19:08:04 +00:00
cameron deleted branch feat/enhance-lora-switches 2026-11-03 19:08:04 +00:00
Sign in to join this conversation.