Picker wheel: Quick A/B (every enhancement off, last run's seed, generates the picked scene right away, then restores)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZzdHoWjZ83CzN8GQwBeUU
This commit is contained in:
@@ -210,3 +210,28 @@ test("with New seed every Generate off, the seed node's own number runs, and the
|
||||
await app.runner.units(["a"], 1);
|
||||
assert.notEqual(sent[1].s.seed, undefined, "the default draws a fresh one");
|
||||
});
|
||||
|
||||
test("Quick A/B runs the picked scene with every enhancement off and the last run's seed, then puts the enhancements back", async () => {
|
||||
const sent = [];
|
||||
let off = false;
|
||||
const { app, doc } = rig("idle", { generate: async ({ inputs }) => { sent.push({ inputs, off }); return true; } });
|
||||
app.pipeline.allOff = () => off;
|
||||
app.pipeline.setAllOff = async (on) => { off = on; };
|
||||
app.selection.ids = ["a"];
|
||||
await app.runner.quickAB();
|
||||
assert.equal(sent.length, 0, "no earlier run, nothing to compare with");
|
||||
assert.match(document.body.textContent, /generate once first/);
|
||||
app.lastRun.seed = 1234;
|
||||
doc.editor_settings = {}; // the default: a fresh seed every Generate, which Quick A/B must override
|
||||
await app.runner.quickAB();
|
||||
assert.equal(sent.length, 1);
|
||||
assert.equal(sent[0].off, true, "enhancements were off while the run was queued");
|
||||
assert.equal(sent[0].inputs.s.seed, 1234);
|
||||
assert.equal(off, false, "and are on again afterwards");
|
||||
assert.equal(app.lastRun.seed, 1234);
|
||||
await app.runner.units(["a"], 1);
|
||||
assert.notEqual(sent[1].inputs.s.seed, 1234, "the next ordinary Generate is fresh again");
|
||||
off = true; // already plain: stays plain
|
||||
await app.runner.quickAB();
|
||||
assert.equal(off, true);
|
||||
});
|
||||
|
||||
@@ -51,7 +51,8 @@ export default {
|
||||
}).catch(() => tell("The result could not be saved to its project.")); };
|
||||
|
||||
// `times` > 1: the same shots again, only the seed differing (the prompt's random picks are drawn once), so the takes can be compared.
|
||||
async function runUnits(units, times = 1) {
|
||||
// `once`: for a single run only - {seed} uses that seed on every seed input; onQueued runs once ComfyUI has taken (or refused) the run.
|
||||
async function runUnits(units, times = 1, once = {}) {
|
||||
if (busy) { tell("A run is already going: wait for it, or Stop it."); return; }
|
||||
busy = true; stopped = false; draw();
|
||||
const pid = p.project.id;
|
||||
@@ -97,7 +98,7 @@ export default {
|
||||
if (group.every((s) => s.excluded) || !isGenerative(root)) continue; // all left out, or not made by the model
|
||||
// The unit is made once at the length of its clips together (each cut adds one shared frame).
|
||||
const frames = group.reduce((t, s) => t + effFrames(s, snap), 0) - (group.length - 1);
|
||||
const { inputs, unwired, left, noPrompt, notes } = await buildInputs({ project: snap, scene: root, slots: frozen, expand: same, nodeInfo: app.api.wireTargets, frames, hooks: app.inputHooks, ...(fixedSeed ? { seed: null } : {}), prefix: (app.promptPrefix || []).flatMap((f) => { try { return f(root, snap); } catch { return []; } }) });
|
||||
const { inputs, unwired, left, noPrompt, notes } = await buildInputs({ project: snap, scene: root, slots: frozen, expand: same, nodeInfo: app.api.wireTargets, frames, hooks: app.inputHooks, ...(Number.isInteger(once.seed) ? { seed: () => once.seed } : fixedSeed ? { seed: null } : {}), prefix: (app.promptPrefix || []).flatMap((f) => { try { return f(root, snap); } catch { return []; } }) });
|
||||
app.lastRun.typed = (root.text || "").trim(); // what a Chat comment made now would be about
|
||||
const seeded = rolesAt(frozen, "generation.seed")[0];
|
||||
if (seeded) app.lastRun.seed = (inputs[seeded.slot.id] || {})[seeded.role.input] ?? (seeded.slot.inputs || {})[seeded.role.input]; // shown in ⏱ Sampler, to keep a liked one
|
||||
@@ -113,6 +114,7 @@ export default {
|
||||
current = snap.scenes.findIndex((s) => s.id === root.id) + 1;
|
||||
const queued = await g.generate({ sceneId: root.id, projectId: pid, inputs, slots: frozen });
|
||||
current = 0;
|
||||
if (once.onQueued) await once.onQueued();
|
||||
if (!queued) {
|
||||
done.cancel();
|
||||
const n = snap.scenes.findIndex((s) => s.id === root.id) + 1; // which clip: in a run of several, the toast alone does not say
|
||||
@@ -139,6 +141,21 @@ export default {
|
||||
} finally { busy = false; draw(); }
|
||||
}
|
||||
app.runner.units = (units, times) => runUnits(units, times);
|
||||
/** Quick A/B: the picked scene(s) once more with every enhancement off and the last run's seed, so the two clips differ only by the enhancements. Nothing stays changed afterwards. */
|
||||
app.runner.quickAB = async () => {
|
||||
const seed = app.lastRun && app.lastRun.seed;
|
||||
if (busy) { tell("A run is already going: wait for it, or Stop it."); return; }
|
||||
if (!p.project || !Number.isInteger(seed)) { tell("Quick A/B needs a run to compare with: generate once first."); return; }
|
||||
const units = unitsOf(pickedScenes());
|
||||
if (!units.length) return;
|
||||
const was = app.pipeline.allOff();
|
||||
let restored = was;
|
||||
const restore = async () => { if (!restored) { restored = true; await app.pipeline.setAllOff(false); } }; // as soon as ComfyUI has the run: later edits are the person's again
|
||||
try {
|
||||
if (!was) { restored = false; await app.pipeline.setAllOff(true); }
|
||||
await runUnits(units, 1, { seed, onQueued: restore });
|
||||
} finally { await restore().catch(() => tell("Enhancements could not be switched back on: use Enable enhancements on the wheel.")); }
|
||||
};
|
||||
/** One run for the Training window: the pipeline as it is, its LoRA loader pointed at `lora`, the picked scene (else the first one) and the project's own settings.
|
||||
* Nothing is put on the timeline; resolves to {image} or {error}. */
|
||||
app.runner.preview = async ({ lora, strength = 1 }) => {
|
||||
|
||||
@@ -81,3 +81,22 @@ test("the wheel offers Clear references and the guidance switch, both opt-in, an
|
||||
assert.ok(!find("clear-refs").when(), "nothing to clear without references");
|
||||
} finally { off(); }
|
||||
});
|
||||
|
||||
test("Quick A/B is an opt-in wheel action, offered only once there is a seed to reuse, and starts the run", async () => {
|
||||
server();
|
||||
localStorage.clear();
|
||||
const project = createProject({});
|
||||
await project.start();
|
||||
let ran = 0;
|
||||
const app = { dom: createDom(), project, pipeline: { allOff: () => false, setAllOff: async () => {} }, actions: [], editorSettings: [], lastRun: { n: 0 }, runner: { quickAB: async () => { ran += 1; } } };
|
||||
const off = hub.setup({ host: document.createElement("div"), app });
|
||||
try {
|
||||
const a = app.actions.find((x) => x.id === "quick-ab");
|
||||
assert.ok(a && a.optIn && a.label === "Quick A/B");
|
||||
assert.ok(!a.when(), "hidden until a run has made a seed");
|
||||
app.lastRun.seed = 7;
|
||||
assert.ok(a.when());
|
||||
await a.run();
|
||||
assert.equal(ran, 1);
|
||||
} finally { off(); }
|
||||
});
|
||||
|
||||
@@ -39,6 +39,9 @@ export default {
|
||||
offer(app, { id: "camera-off", optIn: true, icon: "◎", get label() { return pipe().cameraOff?.() ? "Enable camera move enhancements" : "Disable camera move enhancements"; },
|
||||
when: () => Boolean(pipe() && pipe().cameraOff && pipe().setCameraOff && pipe().modulesById?.().conditioning_shot_camera),
|
||||
run: async () => { const on = !pipe().cameraOff(); await pipe().setCameraOff(on); say(on ? "Camera move enhancements disabled" : "Camera move enhancements enabled again"); } }),
|
||||
offer(app, { id: "quick-ab", optIn: true, label: "Quick A/B", icon: "⇄",
|
||||
when: () => Boolean(app.runner && app.runner.quickAB && pipe() && pipe().setAllOff && Number.isInteger(app.lastRun && app.lastRun.seed)),
|
||||
run: () => app.runner.quickAB() }),
|
||||
offer(app, { id: "clear-refs", optIn: true, label: "Clear references", icon: "⌧",
|
||||
when: () => Boolean(p() && p().selected && (p().selected.references || []).length),
|
||||
run: async () => { await p().setScene(p().selected.id, "references", []); say("References cleared for this scene"); } }),
|
||||
|
||||
Reference in New Issue
Block a user