From 4f57799ebf7ab3fe51259d7f4d9a5400e4fc8c80 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sun, 28 Jun 2026 21:50:34 +0300 Subject: [PATCH] editor: expose Conditioning Adjustments (per-phrase universal steering) in Engine Settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- movie_editor/frontend/engine_settings.js | 63 +++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/movie_editor/frontend/engine_settings.js b/movie_editor/frontend/engine_settings.js index 5de8ce56..1de71e46 100644 --- a/movie_editor/frontend/engine_settings.js +++ b/movie_editor/frontend/engine_settings.js @@ -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 = [