feat: a Generate's settings are taken once and every run of it is queued with them

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-10-03 16:31:43 +03:00
co-authored by Claude Sonnet 5.5
parent 6e37dc36ef
commit 59c2153137
3 changed files with 39 additions and 2 deletions
+14
View File
@@ -504,7 +504,21 @@
return false;
}
// Every slot's own values as they are NOW, as per-run overrides: a Generate is several runs, and the
// settings are global to it -- an edit made while it runs reaches the next Generate, never half of
// this one. Inputs another node feeds (arrays) are left to that node.
function frozenInputs() {
const out = {};
(slots || []).forEach((s) => {
const own = {};
Object.entries(s.inputs || {}).forEach(([k, v]) => { if (!Array.isArray(v)) own[k] = JSON.parse(JSON.stringify(v)); });
out[s.id] = own;
});
return out;
}
window.PipelineState = {
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,
+13
View File
@@ -262,3 +262,16 @@ test("a project opened after another does not inherit the first one's pending sw
await PS.adopt([{ id: "model", node: "Loader", inputs: { file: "x" } }]);
assert.strictEqual(PS.isOff("sharpen"), false);
});
test("the settings of a Generate are taken once: frozenInputs is a copy, wired inputs are left to their node", async () => {
const PS = load([]);
await PS.ensureLoaded();
const frozen = PS.frozenInputs();
assert.deepStrictEqual(frozen.model, { file: "", dtype: "bf16" });
assert.deepStrictEqual(frozen.gen, { steps: 8 }); // `model: ["model", 0]` is fed by a node
await PS.save({ inputs: { gen: { steps: 4 } } });
assert.strictEqual(PS.slots().find((s) => s.id === "gen").inputs.steps, 4);
assert.strictEqual(frozen.gen.steps, 8); // the snapshot does not follow the edit
frozen.model.dtype = "fp16";
assert.strictEqual(PS.slots().find((s) => s.id === "model").inputs.dtype, "bf16"); // and is not the live object
});
+12 -2
View File
@@ -3756,6 +3756,11 @@
let unwiredNotice = null;
const GB = window.GenerateBridge;
if (!GB || !state.project) return { raw, unwiredNotice };
// The Generate's own settings are the base; what is specific to this run is laid over them below.
const live = new Set((window.PipelineState.slots() || []).map((s) => s.id));
for (const [slotId, own] of Object.entries(_frozenSettings || {})) {
if (live.has(slotId) && Object.keys(own).length) raw[slotId] = { ...own };
}
for (const slot of (window.PipelineState.slots() || [])) {
for (const role of (slot.roles || [])) {
@@ -4326,6 +4331,7 @@
// `await` (not `return fn()`) is what makes the finally wait for the whole montage.
// A Generate begins: captures still waiting for a rating from the last one are forgotten.
// Best effort -- a failure here must not stop a generation.
let _frozenSettings = null; // slot id -> inputs, for the Generate in flight (null outside one)
let _tasteGenPending = false; // set by an entry point that owns a Generate; spent by its first real run
async function _newTasteGeneration() {
try { await API.newTasteGeneration(); } catch (e) { /* nothing to forget */ }
@@ -4334,8 +4340,12 @@
// is refused later anyway and must not make the running Generate forget its unrated clips.
async function _ownsGenerate(fn) {
const owner = !_genRunActive;
if (owner) _tasteGenPending = true;
try { return await fn(); } finally { if (owner) _tasteGenPending = false; }
if (owner) {
_tasteGenPending = true;
// The settings of this Generate, taken once: every run of it is queued with these.
_frozenSettings = window.PipelineState && window.PipelineState.frozenInputs ? window.PipelineState.frozenInputs() : null;
}
try { return await fn(); } finally { if (owner) { _tasteGenPending = false; _frozenSettings = null; } }
}
async function _clockedGenerate(onlyScene) {
_genClockStart(onlyScene ? "scene" : "all");