From c4084d3db06e3f7c6a43d3c6aff797381bb3bf2d Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sun, 4 Oct 2026 14:43:05 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20Disable=20all=20enhancements=20(project?= =?UTF-8?q?=20switch=20for=20plain-pipeline=20A/B=20runs;=20Settings=20?= =?UTF-8?q?=E2=96=B8=20Modules,=20chip=20beside=20Generate)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- app/ui/features/generate/warn.js | 9 +++++++-- app/ui/features/settings/engine.js | 1 + app/ui/features/settings/modules.js | 6 ++++-- app/ui/shell/pipeline_state.js | 8 ++++++-- core/control.py | 23 +++++++++++++++++------ core/tests/test_control.py | 7 +++++++ modules/conditioning/shot_camera/nodes.py | 4 ++-- 7 files changed, 44 insertions(+), 14 deletions(-) diff --git a/app/ui/features/generate/warn.js b/app/ui/features/generate/warn.js index b65653a1..5ed265ed 100644 --- a/app/ui/features/generate/warn.js +++ b/app/ui/features/generate/warn.js @@ -9,9 +9,14 @@ export default { const chip = c.button.sm({ label: "⚠ Nothing encodes your prompt", tone: "ghost", disabled: true, title: "This pipeline has no prompt input: the scene text is not sent." }).node; chip.hidden = true; - host.append(chip); + const plain = c.button.sm({ label: "Enhancements off", tone: "ghost", title: "Disable all enhancements is on: runs use the plain pipeline. Switch it off in Settings ▸ Modules.", + onClick: () => app.openSettings?.("modules") }).node; + plain.hidden = true; + host.append(chip, plain); const draw = (slots) => { chip.hidden = !(slots && !slots.some((s) => (s.roles || []).some((r) => r.at === "generation.prompt"))); }; + const drawPlain = () => { plain.hidden = !app.pipeline.allOff(); }; + drawPlain(); draw(app.pipeline.slots()); - return app.pipeline.subscribe ? app.pipeline.subscribe(draw) : undefined; + return app.pipeline.subscribe ? app.pipeline.subscribe(() => { draw(app.pipeline.slots()); drawPlain(); }) : undefined; }, }; diff --git a/app/ui/features/settings/engine.js b/app/ui/features/settings/engine.js index 34d02148..a7f67fea 100644 --- a/app/ui/features/settings/engine.js +++ b/app/ui/features/settings/engine.js @@ -12,6 +12,7 @@ export const engine = (app) => function mount() { function draw() { if (ps.loading()) return page.set([c.hint.default({ text: "Loading…" })]); if (ps.loadError()) return page.set([c.banner.warn({ text: `Could not load Engine settings: ${ps.loadError()}` })]); + if (ps.allOff()) return page.set([c.banner.info({ text: "All enhancements are disabled for this project, so there is nothing to set. Turn them back on in Settings ▸ Modules." })]); const withSettings = ps.activeModules().filter((m) => Object.keys(m.settings || {}).length); const cats = CATEGORIES.filter(([k]) => withSettings.some((m) => (m.category || "") === k)); if (!cats.length) return page.set([c.emptyState.default({ icon: "⚡", title: "Nothing to set", hint: "No installed module exposes a setting for this pipeline." })]); diff --git a/app/ui/features/settings/modules.js b/app/ui/features/settings/modules.js index 1e7e70bc..5ae78269 100644 --- a/app/ui/features/settings/modules.js +++ b/app/ui/features/settings/modules.js @@ -23,11 +23,13 @@ export const modules = (app) => function mount() { await ps.refreshControl(); draw(); } }) }); } - return c.toggle.default({ label: name(m), hint: ps.isOff(m.id) ? "Off for this project" : undefined, checked: !ps.isOff(m.id), + return c.toggle.default({ label: name(m), hint: ps.allOff() ? "Off: all enhancements are disabled" : ps.isOff(m.id) ? "Off for this project" : undefined, checked: !ps.allOff() && !ps.isOff(m.id), disabled: ps.allOff(), onChange: (on) => ps.setOff(m.id, !on).then(draw) }); }; const notes = ps.saveNotes(); - page.set([notes.length ? c.banner.warn({ text: notes.join(" ") }) : null, c.hint.default({ text: "A module you turn off here is off for this project only, and disappears from Engine settings. One that failed while generating is off for every project until its code is repaired or you turn it back on." }), + const master = c.toggle.default({ label: "Disable all enhancements", hint: "Runs the plain pipeline: every module below sits out, and so do the rewrites of shot camera. Your choices are kept — switch it back off to restore them. Handy for an A/B test of whether a feature does anything.", + checked: ps.allOff(), onChange: (on) => ps.setAllOff(on).then(draw) }); + page.set([master, notes.length ? c.banner.warn({ text: notes.join(" ") }) : null, c.hint.default({ text: "A module you turn off here is off for this project only, and disappears from Engine settings. One that failed while generating is off for every project until its code is repaired or you turn it back on." }), ...CATEGORIES.flatMap(([key, label]) => { const list = mine.filter((m) => (m.category || "") === key); return list.length ? [c.label.section({ text: label }), ...list.map(row)] : []; })].filter(Boolean)); } draw(); diff --git a/app/ui/shell/pipeline_state.js b/app/ui/shell/pipeline_state.js index cfdad34b..3cfed7a6 100644 --- a/app/ui/shell/pipeline_state.js +++ b/app/ui/shell/pipeline_state.js @@ -275,13 +275,17 @@ export function createPipelineState(API) { if (off) cur.add(id); else cur.delete(id); return setModuleValue("_off", "modules", [...cur]); } + // "Disable all enhancements": one project switch that makes every run plain, for A/B tests. The per-module + // choices are kept underneath, so switching it back restores exactly what was on. + const allOff = () => Boolean((currentValues()._off || {}).all); + const setAllOff = (on) => setModuleValue("_off", "all", Boolean(on)); async function refreshControl() { try { controlState = (await API.modules()).control || {}; } catch (_) { /* keeps the last answer */ } return controlState; } function isQuarantined(id) { return !!(controlState[id] && controlState[id].quarantine); } function activeModules() { - return Object.values(modulesById).filter((m) => !isOff(m.id) && !isQuarantined(m.id)); + return allOff() ? [] : Object.values(modulesById).filter((m) => !isOff(m.id) && !isQuarantined(m.id)); } // Patch ONE field and save the WHOLE tree. place() (core/graph.py) writes @@ -520,7 +524,7 @@ export function createPipelineState(API) { frozenInputs, ensureLoaded, save, edit, restore, snapshot, setGroup, adopt, subscribe: (fn) => { listeners.add(fn); return () => listeners.delete(fn); }, valuesAlreadyPlaced, currentValues, setModuleValue, modulesById: () => modulesById, - activeModules, isOff, setOff, refreshControl, control: () => controlState, + activeModules, isOff, setOff, allOff, setAllOff, refreshControl, control: () => controlState, removedIds: () => [...removed], unwiredMap: () => JSON.parse(JSON.stringify(unwired)), slots: () => slots, diff --git a/core/control.py b/core/control.py index cc0bb972..27e5a14c 100644 --- a/core/control.py +++ b/core/control.py @@ -37,11 +37,22 @@ def controllable(spec) -> bool: return bool(spec.provides.get("modifier") or spec.provides.get("sampler_modifier")) +def all_off(settings) -> bool: + """The project's "disable all enhancements" switch: every module that modifies a run sits out, so a + run shows what the plain pipeline does (an A/B test of whether an enhancement does anything).""" + raw = settings.get("_off") if isinstance(settings, dict) else None + return isinstance(raw, dict) and raw.get("all") is True + + def off_by_project(settings) -> set: """Module ids the project turned off, read from the settings payload.""" raw = settings.get("_off") if isinstance(settings, dict) else None ids = raw.get("modules") if isinstance(raw, dict) else None - return {i for i in ids if isinstance(i, str)} if isinstance(ids, list) else set() + off = {i for i in ids if isinstance(i, str)} if isinstance(ids, list) else set() + if all_off(settings): + from . import registry + off |= {i for i, spec in registry.current().specs.items() if controllable(spec)} + return off def bad_off(raw) -> str | None: @@ -49,8 +60,8 @@ def bad_off(raw) -> str | None: if raw is None: return None ids = raw.get("modules") if isinstance(raw, dict) else None - if not isinstance(ids, list) or not all(isinstance(i, str) for i in ids): - return '"_off" must be {"modules": ["module_id", ...]}' + if not isinstance(ids, list) or not all(isinstance(i, str) for i in ids) or not isinstance(raw.get("all", False), bool): + return '"_off" must be {"modules": ["module_id", ...], "all": true|false}' return None @@ -192,13 +203,13 @@ def start_failed(spec, exc: BaseException) -> None: def partition(specs: Iterable, settings) -> Tuple[List, List[str]]: """(kept, notes): `specs` without the ones the project turned off or that are quarantined.""" specs = list(specs) - off = off_by_project(settings) + off, everything = off_by_project(settings), all_off(settings) held = quarantined(specs) kept, notes = [], [] for spec in specs: if controllable(spec): - if spec.id in off: - notes.append(f"{spec.id}: turned off for this project") + if spec.id in off or everything: + notes.append(f"{spec.id}: " + ("off -- all enhancements are disabled" if all_off(settings) else "turned off for this project")) continue if spec.id in held: notes.append(f"{spec.id}: OFF -- it failed on {held[spec.id].get('when', '?')} " diff --git a/core/tests/test_control.py b/core/tests/test_control.py index c00e3756..0111550e 100644 --- a/core/tests/test_control.py +++ b/core/tests/test_control.py @@ -118,3 +118,10 @@ def test_a_module_that_cannot_run_here_is_said_but_not_quarantined(): assert control.fingerprint() == "" control.start_failed(spec("a"), ValueError("a real bug")) assert control.fingerprint() == "a" + + +def test_disable_all_enhancements_sits_out_every_modifier_and_nothing_else(): + kept, notes = control.partition([spec("a"), spec("loader", ("routes",))], {"_off": {"all": True}}) + assert [s.id for s in kept] == ["loader"] and "all enhancements" in notes[0] + assert control.all_off({"_off": {"all": True}}) and not control.all_off({"_off": {"all": "yes"}}) and not control.all_off({}) + assert control.bad_off({"modules": [], "all": "yes"}) and control.bad_off({"modules": [], "all": True}) is None diff --git a/modules/conditioning/shot_camera/nodes.py b/modules/conditioning/shot_camera/nodes.py index 8e16393d..eabda28c 100644 --- a/modules/conditioning/shot_camera/nodes.py +++ b/modules/conditioning/shot_camera/nodes.py @@ -4,7 +4,7 @@ import hashlib from comfy_api.latest import io -from ..._core import log, registry as registry_mod, schema as schema_mod, shortcuts +from ..._core import control as control_mod, log, registry as registry_mod, schema as schema_mod, shortcuts from . import engine, memory ID = "conditioning_shot_camera" @@ -13,7 +13,7 @@ ID = "conditioning_shot_camera" def _values(settings): """This module's settings from the pipeline's settings object, checked against its own declaration.""" spec = registry_mod.current().specs.get(ID) - if spec is None: + if spec is None or control_mod.all_off(settings): # "disable all enhancements" is a switch for this too return {} clean, problems = schema_mod.check_values(spec, (settings or {}).get(ID)) for problem in problems or ():