editor: expose Conditioning Adjustments (per-phrase universal steering) in Engine Settings

The Studio popup's per-phrase universal steering (the `adjustments` widget:
CLIP-encode each phrase and shift conditioning toward (+) / away (−) from it
on every generation, prompt-independent) was the one Studio control with no
Editor surface — engine_settings.js even said it "remains in the ComfyUI
Studio popup".

Add a "Conditioning adjustments" section to the Studio card: a phrase+strength
list with add/remove, stored in project.studio_inputs.adjustments (JSON
[{phrase, strength}]). Frontend-only — _run_studio_inputs already passes the
whole studio_inputs dict through and builder.py wires any studio_inputs key
onto the matching FunPackStudio widget, so this reaches FunPackStudio.adjust
with no backend change. Backend already ignores blank/zero rows, so the list
persists as-is while editing.

Also: count active adjustments in the Studio "changed" badge and drop the now-
stale "adjustments remain in the popup" hint.

Not ported: the popup's learned "session phrase bank" (needs a refinement key
+ the /funpack/phrase_memory route) — possible follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-28 21:50:34 +03:00
co-authored by Claude Opus 4.8
parent ff224cbffb
commit 4f57799ebf
+62 -1
View File
@@ -93,6 +93,7 @@
const cur = rf[f.name] != null ? rf[f.name] : f.default;
if (cur !== f.default) n++;
});
n += parseAdjustments(p).filter(adjIsActive).length;
return n;
}
@@ -104,6 +105,64 @@
else S.setStudioInput("studio_settings", next);
}
// Conditioning adjustments — universal per-phrase steering. Stored as the FunPackStudio
// `adjustments` widget (JSON [{phrase, strength}]), a top-level studio_input the builder
// wires straight onto the node. Each phrase is CLIP-encoded and shifts conditioning toward
// (+) / away (−) from it on EVERY generation, regardless of prompt. The backend ignores
// blank/zero rows, so we persist the list as-is (no need to prune while editing).
function parseAdjustments(p) {
const si = (p && p.studio_inputs) || {};
try {
const v = JSON.parse(si.adjustments || "[]");
return Array.isArray(v) ? v.map((i) => ({ phrase: String(i.phrase || ""), strength: +i.strength || 0 })) : [];
} catch (_) { return []; }
}
function adjIsActive(i) {
return !!String(i.phrase || "").trim() && Math.abs(+i.strength || 0) > 1e-6;
}
function persistAdjustments(items, now) {
const json = JSON.stringify(items);
if (now) S.setStudioInputNow("adjustments", json);
else S.setStudioInput("adjustments", json);
}
function renderStudioAdjustments(parent) {
const sec = collapsibleSection(parent, "studio_adjust", "Conditioning adjustments", false);
sec.append(el("div", "insp-hint",
"Universal per-phrase steering: each phrase is encoded by CLIP and shifts conditioning "
+ "toward (+) or away (−) from it on every generation, regardless of the prompt. "
+ "Typical range −0.3 to +0.3."));
const items = parseAdjustments(S.get().project);
if (!items.length) sec.append(el("div", "insp-hint", "No adjustments. Add a phrase below."));
items.forEach((item, idx) => {
const row = el("div", "fields-row studio-adjust-row");
const phrase = el("input"); phrase.type = "text"; phrase.placeholder = "phrase or word";
phrase.value = item.phrase; phrase.dataset.k = "adj-phrase-" + idx; phrase.style.flex = "1";
phrase.oninput = () => { items[idx].phrase = phrase.value; persistAdjustments(items, false); };
const strength = el("input"); strength.type = "number";
strength.step = "0.05"; strength.min = "-1"; strength.max = "1";
strength.value = item.strength; strength.dataset.k = "adj-str-" + idx; strength.style.width = "72px";
strength.title = "Positive steers toward the phrase, negative away.";
strength.oninput = () => { items[idx].strength = parseFloat(strength.value || "0"); persistAdjustments(items, false); };
const del = el("button", "btn ghost tiny", "✕"); del.type = "button"; del.title = "Remove";
del.onclick = () => { items.splice(idx, 1); persistAdjustments(items, true); };
row.append(phrase, strength, del);
sec.append(row);
});
const add = el("button", "btn ghost tiny", "+ Add phrase"); add.type = "button";
add.onclick = () => {
items.push({ phrase: "", strength: 0.1 });
persistAdjustments(items, true);
setTimeout(() => {
const inputs = document.querySelectorAll('[data-k^="adj-phrase-"]');
inputs[inputs.length - 1]?.focus();
}, 0);
};
sec.append(add);
}
function renderStudioRefinerBool(parent, rf, f) {
const cur = rf[f.name] != null ? rf[f.name] : f.default;
const ctrl = el("input"); ctrl.type = "checkbox"; ctrl.checked = !!cur; ctrl.style.width = "auto";
@@ -159,6 +218,8 @@
const refSec = collapsibleSection(card.body, "studio_ref", "Refinement", false);
STUDIO_REFINER_ADVANCED.forEach((f) => renderStudioRefinerField(refSec, rf, f));
renderStudioAdjustments(card.body);
const sampSec = collapsibleSection(card.body, "studio_samp", "Sampler algorithm", false);
function persistSamplers(updatedSamplers, quiet) {
const { cur } = parseStudioSettings(S.get().project);
@@ -177,7 +238,7 @@
}
card.body.append(el("div", "insp-hint",
"Scene text and transitions come from the timeline. Advisor, LoRA, batch training, and adjustments remain in the ComfyUI Studio popup on the graph."));
"Scene text and transitions come from the timeline. Advisor, LoRA, and batch training remain in the ComfyUI Studio popup on the graph."));
}
const SAMPLER_KNOBS = [