feat(enhance): pick LoRAs from ComfyUI per job (#220)

Templates with `imageapi:lora-slot:<n>:high|low` nodes take LoRAs picked
per job from a catalog built live from ComfyUI.

- Catalog: ComfyUI's LoraLoaderModelOnly `lora_name` choices (cached
  60 s, re-read when a submit names an unknown id), paired by High/Low
  name tokens (StyleHigh/StyleLow, x-HIGH/x-LOW, x_high_noise/...). Files
  the workflow already loads outside the slots (lightx2v, switches) and
  `[template.catalog] exclude` substrings never show; ids derive from the
  names so they stay stable.
- POST /enhance `slots: [{lora, scale?}]` fills the slots in order: each
  pick sets the slot nodes' lora_name and scales their exported strength;
  unused slots run at 0, so the graph is never rewired. More picks than
  slots, duplicates or unknown ids are 400s.
- GET /enhance/templates gains `slots` and `catalog: {loras, strength,
  min, max}`; jobs store and return the picks (new enhance_jobs.lora_slots
  JSON column).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 committed 2026-11-03 16:28:23 -05:00
1 parent 6fffca9989
commit e2cf18678f
15 files changed
+1454 -33

No files matched your search

+15 -4
View File
@@ -460,7 +460,8 @@ auth and stays LAN-only, so ImageApi relays previews too.
`unreachable_grace_secs`, `default_template`, `warn_above_megapixels`,
`[[path_map]]` (`from`, `to`), `[[template]]` (`name`, `label`, `description`,
`file`, `prompt_mode`, `prompt_join`, `output_megapixels`, `[[template.lora]]`
(`id`, `label`, `default`, `strength`, `min`, `max`)).
(`id`, `label`, `default`, `strength`, `min`, `max`), `[template.catalog]` (`strength`,
`min`, `max`, `exclude`)).
- **Templates** are API-format workflow exports, read-only (never edit them from
here). Nodes are found by `_meta.title`: `imageapi:source` (required, gets the
photo path), `imageapi:save` (required, forced to `require_approval = true`; a
@@ -472,7 +473,12 @@ auth and stays LAN-only, so ImageApi relays previews too.
low pair; the resolved scale multiplies each node's exported `strength_model` /
`strength_clip` (1.0 = as exported, 0 = off, a pair keeps its ratio), never rewired; a
node with no declaration keeps its strength, a declaration with no node isn't offered;
both log a startup warning). `prompt_mode = "append"`: the user's text is joined
both log a startup warning), `imageapi:lora-slot:<n>:high|low` (pickable LoRAs, #220:
each slot needs a high and a low node, numbered 1.. without gaps; picks fill them in order,
setting `lora_name` and scaling the exported strength, unused slots run at 0; the catalog is
ComfyUI's `/object_info/LoraLoaderModelOnly` list (cached 60 s, re-read when a submit names
an unknown id) paired by High/Low name tokens (`src/enhance/lora_catalog.rs`), minus every
`lora_name` the workflow loads outside the slots and `catalog.exclude` substrings). `prompt_mode = "append"`: the user's text is joined
after the template's own prompt with `prompt_join` (default one space); empty text
leaves the prompt as is. `"replace"`: the text *is* the prompt and is required (empty
= 400); it needs a prompt node. With no `reference_path` the reference node **and
@@ -519,8 +525,12 @@ GET /enhance/templates
"prompt_mode": "append"|"replace", "prompt_required",
"accepts_reference","default","output_megapixels",
"warn_above_megapixels", "output": "image"|"video",
"loras": [{"id","label","default","strength","min","max"}]}]}
POST /enhance {"path","template","prompt"?,"reference_path"?,"loras"?: {id: bool|scale},"library"?} -> job
"loras": [{"id","label","default","strength","min","max"}],
"slots": n, "catalog"?: {"loras": [{"id","label"}],"strength","min","max"}}]}
POST /enhance {"path","template","prompt"?,"reference_path"?,"loras"?: {id: bool|scale},
"slots"?: [{"lora": catalog id, "scale"?}],"library"?} -> job
(slots: at most the template's slot count, no duplicates, unknown id = 400; no scale =
catalog.strength, <= 0 = slot unused)
(true = the switch's strength, false or scale <= 0 = off, a scale is clamped to min..max;
a missing switch uses its default; an id the template doesn't offer = 400)
GET /enhance/jobs/{id} -> job
@@ -540,6 +550,7 @@ POST /enhance/jobs/{id}/discard -> job (hidden from the list; ComfyUI untou
job = {"id","status","queue_position"?,"error"?,"path","library_id","template",
"prompt"?,"reference_path"?,"loras"?: {id: scale} (resolved, 0 = off; absent without switches),
"slots"?: [{"lora","label","scale"}] (picked LoRAs; absent when none),
"created_at","updated_at",
"run_ms": <ms ComfyUI ran it, or null until finished / when ComfyUI gave no timings>,
"pending": [{"n","preview": "/enhance/jobs/{id}/preview/{n}","approved"}],