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:
co-authored by
Claude Opus 4.8
parent
ff224cbffb
commit
4f57799ebf
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user