fix: a failed run says which node and why; one queued run per project across tabs; Sampler settings say when not saved
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9f8d8034e1
commit
ce60e4d9ad
@@ -141,3 +141,21 @@ test("an edit still on its way when Generate is clicked (the Story box splitting
|
||||
await app.runner.units(["a"]);
|
||||
assert.deepEqual(seen, ["typed last"]);
|
||||
});
|
||||
|
||||
test("a run that fails while running says which node and ComfyUI's own reason", async () => {
|
||||
const { app } = rig("idle", { generate: async () => true, waitForTerminal: () => Object.assign(Promise.resolve("failed"), { cancel() {} }) });
|
||||
app.generate.run.state.error = { node: "SaveVideo", message: "Saving image outside the output folder is not allowed." };
|
||||
const before = document.body.textContent;
|
||||
await app.runner.units(["a"]);
|
||||
assert.match(document.body.textContent.replace(before, ""), /failed in SaveVideo: Saving image outside the output folder/);
|
||||
});
|
||||
|
||||
test("a project that already has a run in ComfyUI's queue (another tab) is not queued again, and the click says why", async () => {
|
||||
let queued = 0;
|
||||
const { app } = rig("idle", { generate: async () => { queued += 1; return true; } });
|
||||
app.api.projectQueued = async (id) => id === "P";
|
||||
const before = document.body.textContent;
|
||||
await app.runner.units(["a"]);
|
||||
assert.equal(queued, 0);
|
||||
assert.match(document.body.textContent.replace(before, ""), /already has a run in ComfyUI's queue/);
|
||||
});
|
||||
|
||||
@@ -60,6 +60,8 @@ export default {
|
||||
// A project's pipeline still going in is the one this run uses, not the last one's.
|
||||
if (app.pipeline.settled && !(await app.pipeline.settled())) { tell("Not started: this project's pipeline is still loading. ComfyUI is slow to answer; try again in a moment."); return; }
|
||||
if (app.pipelineOwned && !app.pipelineOwned()) { tell(`Not started: ${(app.pipelineWhy && app.pipelineWhy()) || "this project's pipeline is not loaded yet."}`); return; }
|
||||
// The page's own guard does not see another tab: ComfyUI's queue does. One GPU job per project at a time.
|
||||
if (app.api.projectQueued && (await app.api.projectQueued(pid))) { tell("This project already has a run in ComfyUI's queue (another tab or window?). Wait for it, or Stop it there."); return; }
|
||||
// The pipeline as it is NOW, at the click: every shot of this run uses it, whatever is opened or edited meanwhile.
|
||||
const frozen = structuredClone(app.pipeline.slots() || []);
|
||||
if (times > 1 && !rolesAt(frozen, "generation.seed").length) { tell("This pipeline has no seed input the app controls, so the takes would all come out the same."); return; }
|
||||
@@ -96,7 +98,11 @@ export default {
|
||||
const end = await done;
|
||||
if (end === g.CANCELLED) break;
|
||||
const images = g.run.state.images;
|
||||
if (end !== g.DONE) { tell("Generation failed. The log has ComfyUI's message."); break; }
|
||||
if (end !== g.DONE) { // ComfyUI's own words: which node, and why
|
||||
const e = g.run.state.error;
|
||||
tell(e && e.message ? `Generation failed${e.node ? ` in ${e.node}` : ""}: ${e.message}` : "Generation failed. The log has ComfyUI's message.");
|
||||
break;
|
||||
}
|
||||
if (!images.length) { tell("ComfyUI finished without a result. Try again."); break; }
|
||||
await record(pid, unit, images[images.length - 1], frames, g.run.state.promptId); // the next shot may continue from this one: it must be on the clip first
|
||||
snap.scene_renders = structuredClone(p.project.scene_renders || {}); // ...and in what the next shot reads
|
||||
|
||||
@@ -149,3 +149,21 @@ test("typing a value back while the change away from it is still saving puts it
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
assert.equal(live[0].inputs.steps, 7);
|
||||
});
|
||||
|
||||
test("a sampling setting that could not be saved is said in the window, not only on the Engine page", async () => {
|
||||
const mod = { id: "sharpen", category: "sampling", settings: { enabled: { type: "bool", default: false, label: "Adjust detail" } } };
|
||||
const ps = { slots: () => [], activeModules: () => [mod], useful: () => true, usefulness: () => () => true, currentValues: () => ({}), loading: () => false, loadError: () => null,
|
||||
subscribe: () => () => {}, ensureLoaded: async () => {}, save: async () => {}, setModuleValue: async () => {}, settled: async () => true,
|
||||
saveNotes: () => ["Could not save: ComfyUI refused this project's saved pipeline"] };
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
hub.setup({ host, app: { pipeline: ps, api: { describeNodes: async () => ({ nodes: {} }) }, on: () => () => {}, actions: null } });
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
host.querySelector("button").click();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
const before = document.body.textContent;
|
||||
const box = [...document.querySelectorAll("input[type=checkbox], [role=switch]")].pop();
|
||||
box.click();
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
assert.match(document.body.textContent.replace(before, ""), /Could not save: ComfyUI refused/);
|
||||
});
|
||||
|
||||
@@ -49,7 +49,13 @@ export default {
|
||||
if (ps.loading()) return body.set([c.hint.default({ text: "Loading…" })]);
|
||||
if (ps.loadError()) return body.set([c.banner.warn({ text: `Could not load: ${ps.loadError()}` })]);
|
||||
const mods = sampling(), values = ps.currentValues();
|
||||
const change = async (id, name, value) => { await ps.setModuleValue(id, name, value); draw(); };
|
||||
const change = async (id, name, value) => {
|
||||
await ps.setModuleValue(id, name, value);
|
||||
if (ps.settled) await ps.settled();
|
||||
const why = (ps.saveNotes ? ps.saveNotes() : []).find((n) => /could not save/i.test(n));
|
||||
if (why) c.toast.warn({ text: why }); // refused, or not saved: said here, not only on the Engine page
|
||||
draw();
|
||||
};
|
||||
const rows = marked().map(inputRow);
|
||||
const unread = marked().length && !rows.some(Boolean) && (unreadable || marked().every(({ slot }) => slot.node in specs)) ? [c.hint.default({ text: "Steps, sampler and scheduler could not be read from the sampler node: change them in Settings ▸ Models & Pipeline." })] : [];
|
||||
body.set([...unread, ...rows, ...mods.flatMap((m) => [mods.length > 1 ? c.label.section({ text: m.title || m.id }) : null,
|
||||
|
||||
@@ -76,6 +76,13 @@ export const api = {
|
||||
}
|
||||
return body.prompt_id;
|
||||
},
|
||||
/** Whether ComfyUI's queue holds a run of this project (queued from any tab). Unreadable: false, the queue decides. */
|
||||
async projectQueued(projectId) {
|
||||
try {
|
||||
const q = await (await fetch("/queue")).json();
|
||||
return [...(q.queue_running || []), ...(q.queue_pending || [])].some((r) => (r[3] || {}).funpack_project_id === projectId);
|
||||
} catch { return false; }
|
||||
},
|
||||
/** null while it is queued or running, else {videos} | {error} | {gone} | {retry} (a dropped reply is not a failed job). */
|
||||
async upscaleResult(promptId) {
|
||||
let entry = null;
|
||||
|
||||
Reference in New Issue
Block a user