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

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>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 committed 2026-11-03 13:53:53 -05:00
1 parent e317bfee98
commit 31da21e061
12 files changed
+910 -33

No files matched your search

+348
View File
@@ -0,0 +1,348 @@
{
"1": {
"class_type": "ImageApiLoadImageFromPath",
"_meta": {
"title": "imageapi:source"
},
"inputs": {
"path": ""
}
},
"2": {
"class_type": "ImageApiTargetSize",
"_meta": {
"title": "Target Size (~0.92 MP, keeps aspect)"
},
"inputs": {
"image": [
"1",
0
],
"max_megapixels": 0.92,
"min_megapixels": 0.92,
"multiple_of": 16
}
},
"10": {
"class_type": "UNETLoader",
"_meta": {
"title": "Wan 2.2 i2v high noise"
},
"inputs": {
"unet_name": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
"weight_dtype": "default"
}
},
"11": {
"class_type": "UNETLoader",
"_meta": {
"title": "Wan 2.2 i2v low noise"
},
"inputs": {
"unet_name": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
"weight_dtype": "default"
}
},
"12": {
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "lightx2v 4-step (high)"
},
"inputs": {
"model": [
"10",
0
],
"lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors",
"strength_model": 1.0
}
},
"13": {
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "lightx2v 4-step (low)"
},
"inputs": {
"model": [
"11",
0
],
"lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors",
"strength_model": 1.0
}
},
"14": {
"class_type": "ModelSamplingSD3",
"_meta": {
"title": "Shift (high)"
},
"inputs": {
"model": [
"50",
0
],
"shift": 5.0
}
},
"15": {
"class_type": "ModelSamplingSD3",
"_meta": {
"title": "Shift (low)"
},
"inputs": {
"model": [
"51",
0
],
"shift": 5.0
}
},
"40": {
"class_type": "PathchSageAttentionKJ",
"_meta": {
"title": "Sage attention (high)"
},
"inputs": {
"model": [
"14",
0
],
"sage_attention": "auto"
}
},
"41": {
"class_type": "PathchSageAttentionKJ",
"_meta": {
"title": "Sage attention (low)"
},
"inputs": {
"model": [
"15",
0
],
"sage_attention": "auto"
}
},
"16": {
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
},
"inputs": {
"clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors",
"type": "wan",
"device": "default"
}
},
"17": {
"class_type": "CLIPTextEncode",
"_meta": {
"title": "imageapi:prompt"
},
"inputs": {
"clip": [
"16",
0
],
"text": "natural, lifelike motion with a slow, gentle camera movement, realistic, cinematic"
}
},
"18": {
"class_type": "CLIPTextEncode",
"_meta": {
"title": "Negative"
},
"inputs": {
"clip": [
"16",
0
],
"text": "static, frozen, blurry, low quality, distorted, warped faces, morphing, flicker, watermark, text"
}
},
"19": {
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
},
"inputs": {
"vae_name": "wan_2.1_vae.safetensors"
}
},
"21": {
"class_type": "WanImageToVideo",
"_meta": {
"title": "Wan Image To Video (81 frames)"
},
"inputs": {
"positive": [
"17",
0
],
"negative": [
"18",
0
],
"vae": [
"19",
0
],
"start_image": [
"1",
0
],
"width": [
"2",
0
],
"height": [
"2",
1
],
"length": 81,
"batch_size": 1
}
},
"22": {
"class_type": "KSamplerAdvanced",
"_meta": {
"title": "Sampler (high, steps 0-2)"
},
"inputs": {
"model": [
"40",
0
],
"positive": [
"21",
0
],
"negative": [
"21",
1
],
"latent_image": [
"21",
2
],
"add_noise": "enable",
"noise_seed": 1,
"steps": 4,
"cfg": 1.0,
"sampler_name": "euler",
"scheduler": "simple",
"start_at_step": 0,
"end_at_step": 2,
"return_with_leftover_noise": "enable"
}
},
"23": {
"class_type": "KSamplerAdvanced",
"_meta": {
"title": "Sampler (low, steps 2-4)"
},
"inputs": {
"model": [
"41",
0
],
"positive": [
"21",
0
],
"negative": [
"21",
1
],
"latent_image": [
"22",
0
],
"add_noise": "disable",
"noise_seed": 0,
"steps": 4,
"cfg": 1.0,
"sampler_name": "euler",
"scheduler": "simple",
"start_at_step": 2,
"end_at_step": 10000,
"return_with_leftover_noise": "disable"
}
},
"24": {
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
},
"inputs": {
"samples": [
"23",
0
],
"vae": [
"19",
0
]
}
},
"25": {
"class_type": "CreateVideo",
"_meta": {
"title": "Create Video (16 fps)"
},
"inputs": {
"images": [
"24",
0
],
"fps": 16.0
}
},
"36": {
"class_type": "ImageApiSaveVideoVersion",
"_meta": {
"title": "imageapi:save"
},
"inputs": {
"video": [
"25",
0
],
"source": [
"1",
1
],
"kind": "animated",
"subfolder": "aiedits",
"require_approval": true
}
},
"50": {
"inputs": {
"lora_name": "example_style_high.safetensors",
"strength_model": 1.0,
"model": [
"12",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "imageapi:lora:style"
}
},
"51": {
"inputs": {
"lora_name": "example_style_low.safetensors",
"strength_model": 1.0,
"model": [
"13",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "imageapi:lora:style"
}
}
}