feat(enhance): LoRA strength scale alongside the on/off switch (#218)

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>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 committed 2026-11-03 14:01:04 -05:00
1 parent 31da21e061
commit cc96873f86
7 files changed
+298 -110

No files matched your search

+24 -6
View File
@@ -183,6 +183,9 @@ fn animate_with_style() -> Template {
id: "style".into(),
label: "Style".into(),
default: false,
strength: 0.8,
min: 0.0,
max: 1.5,
}];
Template::from_value(cfg, serde_json::from_str(ANIMATE_LORAS).unwrap()).unwrap()
}
@@ -1127,12 +1130,12 @@ async fn lora_switches_are_listed_sent_and_stored_on_the_job() {
let animate = templates.iter().find(|t| t["name"] == "animate").unwrap();
assert_eq!(
animate["loras"],
json!([{"id": "style", "label": "Style", "default": false}])
json!([{"id": "style", "label": "Style", "default": false, "strength": 0.8, "min": 0.0, "max": 1.5}])
);
let restore = templates.iter().find(|t| t["name"] == "restore").unwrap();
assert_eq!(restore["loras"], json!([]));
// On: the pair keeps its strength, and the job remembers the choice.
// On: the configured strength, and the job remembers it.
let (st, job, _) = call(
&fx,
"POST",
@@ -1141,7 +1144,7 @@ async fn lora_switches_are_listed_sent_and_stored_on_the_job() {
)
.await;
assert_eq!(st, 200, "{job}");
assert_eq!(job["loras"], json!({"style": true}));
assert_eq!(job["loras"], json!({"style": 0.8}));
let (_, again, _) = call(
&fx,
"GET",
@@ -1149,7 +1152,7 @@ async fn lora_switches_are_listed_sent_and_stored_on_the_job() {
None,
)
.await;
assert_eq!(again["loras"], json!({"style": true}));
assert_eq!(again["loras"], json!({"style": 0.8}));
// Omitted: the default (off) zeroes both nodes.
let (st, off, _) = call(
@@ -1160,12 +1163,27 @@ async fn lora_switches_are_listed_sent_and_stored_on_the_job() {
)
.await;
assert_eq!(st, 200, "{off}");
assert_eq!(off["loras"], json!({"style": false}));
assert_eq!(off["loras"], json!({"style": 0.0}));
// A slider value scales the exported strength.
let (st, half, _) = call(
&fx,
"POST",
"/enhance",
Some(json!({"path": PHOTO, "template": "animate", "loras": {"style": 0.5}})),
)
.await;
assert_eq!(st, 200, "{half}");
assert_eq!(half["loras"], json!({"style": 0.5}));
{
let sent = fx.mock.submitted.lock().unwrap();
assert_eq!(
sent[0]["prompt"]["50"]["inputs"]["strength_model"],
json!(1.0)
json!(0.8)
);
assert_eq!(
sent[2]["prompt"]["51"]["inputs"]["strength_model"],
json!(0.5)
);
assert_eq!(
sent[1]["prompt"]["50"]["inputs"]["strength_model"],