feat: Disable all enhancements (project switch for plain-pipeline A/B runs; Settings ▸ Modules, chip beside Generate)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-10-04 14:43:05 +03:00
co-authored by Claude Sonnet 5.5
parent 7ffc06c424
commit c4084d3db0
7 changed files with 44 additions and 14 deletions
+7 -2
View File
@@ -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;
},
};
+1
View File
@@ -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." })]);
+4 -2
View File
@@ -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();
+6 -2
View File
@@ -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,
+17 -6
View File
@@ -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', '?')} "
+7
View File
@@ -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
+2 -2
View File
@@ -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 ():