From 59c21531379692264888ddee44a8fb1bd799f66a Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 3 Oct 2026 16:31:43 +0300 Subject: [PATCH] feat: a Generate's settings are taken once and every run of it is queued with them Co-Authored-By: Claude Sonnet 5.5 --- app/legacy/pipeline_state.js | 14 ++++++++++++++ app/legacy/pipeline_state.test.js | 13 +++++++++++++ app/legacy/store.js | 14 ++++++++++++-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/app/legacy/pipeline_state.js b/app/legacy/pipeline_state.js index 95360998..45301631 100644 --- a/app/legacy/pipeline_state.js +++ b/app/legacy/pipeline_state.js @@ -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, diff --git a/app/legacy/pipeline_state.test.js b/app/legacy/pipeline_state.test.js index 47e096fa..11567957 100644 --- a/app/legacy/pipeline_state.test.js +++ b/app/legacy/pipeline_state.test.js @@ -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 +}); diff --git a/app/legacy/store.js b/app/legacy/store.js index dc679def..e574db69 100644 --- a/app/legacy/store.js +++ b/app/legacy/store.js @@ -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");