fix: manifest refreshed on adopt, deferred adopt after a failed load, split clips count once for length, non-slot project.models keys kept, bounded waits
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
5673f7513b
commit
dfb0cc5a8d
@@ -65,6 +65,7 @@
|
||||
let queueable = false;
|
||||
let loading = false;
|
||||
let loadError = null;
|
||||
let deferred = null; // a project's saved pipeline waiting for the first successful load
|
||||
let epoch = 0; // bumped whenever a project's pipeline replaces the live one
|
||||
let saving = false;
|
||||
let pending = false;
|
||||
@@ -127,6 +128,7 @@
|
||||
}
|
||||
loading = false;
|
||||
loadPromise = null;
|
||||
if (slots !== null && deferred) { const d = deferred; deferred = null; await adopt(d); }
|
||||
})();
|
||||
return loadPromise;
|
||||
}
|
||||
@@ -293,7 +295,10 @@
|
||||
// Not announced as a change: it IS the project's own copy.
|
||||
async function adopt(saved) {
|
||||
await ensureLoaded();
|
||||
if (slots === null || !Array.isArray(saved) || !saved.length) return;
|
||||
// Not loaded (ComfyUI unreachable): remember what the project holds and put it in the moment a
|
||||
// load succeeds, so the default is never what the next edit is built on.
|
||||
if (slots === null) { if (Array.isArray(saved) && saved.length) deferred = saved; return; }
|
||||
if (!Array.isArray(saved) || !saved.length) return;
|
||||
// A saved slot is only what the server would accept: a project file outlives the code that
|
||||
// wrote it (v4 files name the node differently), and one bad slot refuses every later edit.
|
||||
const sound = (s) => s && typeof s.id === "string" && typeof s.node === "string"
|
||||
@@ -309,16 +314,20 @@
|
||||
return out;
|
||||
}).concat(extras ? saved.filter((s) => sound(s) && !have.has(s.id)) : []);
|
||||
// Replaces whatever pipeline was live: nothing queued for the old one may land on the new.
|
||||
epoch++;
|
||||
const mine = ++epoch;
|
||||
pendingBody = null; pending = false;
|
||||
while (saving) await new Promise((r) => setTimeout(r, 20));
|
||||
for (let waited = 0; saving && waited < 10000; waited += 20) await new Promise((r) => setTimeout(r, 20));
|
||||
for (const extras of [true, false]) {
|
||||
try {
|
||||
const res = await API.editPipeline({ slots: lay(extras) });
|
||||
if (mine !== epoch) return; // a later project was opened meanwhile
|
||||
if (res && res.slots && !(res.refused || []).length) {
|
||||
slots = res.slots;
|
||||
incomplete = res.incomplete || []; refused = []; queueable = !!res.queueable;
|
||||
saveNotes = res.notes || [];
|
||||
// The modules offered depend on the model this pipeline names, which may differ from the
|
||||
// previous project's: refresh, or the next settings edit drops this project's modules.
|
||||
try { await refreshManifest(); } catch (_) { /* the next save retries it */ }
|
||||
return;
|
||||
}
|
||||
} catch (_) { /* this layering was refused; try the next */ }
|
||||
|
||||
@@ -91,3 +91,15 @@ test("nothing loaded (ComfyUI unreachable) leaves slots null, never an empty pip
|
||||
await P2.adopt([{ id: "model", node: "Loader", inputs: {} }]);
|
||||
assert.strictEqual(P2.slots(), null);
|
||||
});
|
||||
|
||||
test("a project's pipeline waits out an unreachable server and goes in on the first successful load", async () => {
|
||||
const PS = load([]);
|
||||
const good = global.window.MovieEditorAPI.pipeline;
|
||||
let up = false;
|
||||
global.window.MovieEditorAPI.pipeline = async () => { if (!up) throw new Error("down"); return good(); };
|
||||
await PS.adopt([{ id: "model", node: "Loader", inputs: { file: "h3.safetensors" } }]);
|
||||
assert.strictEqual(PS.slots(), null);
|
||||
up = true;
|
||||
await PS.ensureLoaded();
|
||||
assert.strictEqual(PS.slots().find((s) => s.id === "model").inputs.file, "h3.safetensors");
|
||||
});
|
||||
|
||||
+8
-4
@@ -3741,7 +3741,8 @@
|
||||
if (input === "length") {
|
||||
// The PROJECT's length per scene, never the scene's own: a scene cropped on the timeline
|
||||
// regenerates at the project length (the crop was a timeline decision; a regenerate is new).
|
||||
const n = (targetSceneIds || []).filter((id) => scene(id)).length || 1;
|
||||
// Story scenes, not scene rows: a split clip is one story scene in two rows.
|
||||
const n = new Set((targetSceneIds || []).map((id) => scene(id)).filter(Boolean).map((sc) => genUnitId(sc))).size || 1;
|
||||
return n * p.num_frames_per_scene;
|
||||
}
|
||||
return undefined;
|
||||
@@ -4705,11 +4706,14 @@
|
||||
const saved = (state.project && state.project.models && state.project.models.slots) || [];
|
||||
const p = (async () => {
|
||||
try {
|
||||
await window.PipelineState.adopt(saved);
|
||||
// Bounded: a hung request must not hold every later project save behind it.
|
||||
await Promise.race([window.PipelineState.adopt(saved), new Promise((r) => setTimeout(r, 20000))]);
|
||||
const live = window.PipelineState.slots();
|
||||
// Not loaded (ComfyUI unreachable): the project's own copy stays exactly as it was -- an
|
||||
// empty live pipeline must never be mistaken for "no pipeline" and saved over it. Tried again.
|
||||
state.models = { slots: live ? JSON.parse(JSON.stringify(live)) : saved };
|
||||
// Everything else the project keeps beside the slots (model family...) stays as it was.
|
||||
state.models = { ...((state.project && state.project.models) || {}),
|
||||
slots: live ? JSON.parse(JSON.stringify(live)) : saved };
|
||||
if (!live && _modelsRetries++ < 3) setTimeout(loadModels, 5000);
|
||||
else if (live) _modelsRetries = 0;
|
||||
} catch (_) { state.models = { slots: saved }; }
|
||||
@@ -5014,7 +5018,7 @@
|
||||
// Every landed pipeline edit is the project's copy now; one writer (PipelineState), no snapshot of its own.
|
||||
window.PipelineState.subscribe((slots) => {
|
||||
if (!state.project) return;
|
||||
state.models = { slots: JSON.parse(JSON.stringify(slots || [])) };
|
||||
state.models = { ...(state.models || {}), slots: JSON.parse(JSON.stringify(slots || [])) };
|
||||
// Soon, and past a Settings window's hold: a model pick must survive the reload that follows it.
|
||||
_localDirty = true;
|
||||
clearTimeout(_pipelineSaveTimer);
|
||||
|
||||
Reference in New Issue
Block a user