fix: a new project keeps an inherited preset pipeline whole; an edit reaches its project before the model probe answers; readiness names non-CPU devices; a pipeline that cannot load says so
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fd35f4582d
commit
608b4dd566
+2
-1
@@ -15,6 +15,7 @@ import { createKeys } from "./shell/keys.js";
|
||||
import { createDock } from "./shell/dock.js";
|
||||
import { hostFor } from "./shell/mounts.js";
|
||||
import paths from "./modules.js";
|
||||
import { composer as c } from "./composer/composer.js";
|
||||
|
||||
// What the core offers a feature, by name. `on(fn)` hears every change; fn gets "open" when a
|
||||
// different project was opened, "change" for an edit.
|
||||
@@ -24,7 +25,7 @@ const on = (fn) => { heard.add(fn); return () => heard.delete(fn); };
|
||||
const project = createProject({ keepConsistent: syncSeparated, onChange: () => say("change"), onOpen: () => say("open"), onError: (err) => console.warn(err) });
|
||||
const pipeline = createPipelineState(api);
|
||||
const generate = createGenerate({ pipeline });
|
||||
linkPipeline({ project, pipeline, onOpen: (fn) => on((what) => { if (what === "open") fn(); }) });
|
||||
linkPipeline({ project, pipeline, onOpen: (fn) => on((what) => { if (what === "open") fn(); }), say: (text) => c.toast.warn({ text }) });
|
||||
const frame = build(document.getElementById("app"));
|
||||
const selection = createSelection({ project });
|
||||
selection.on(() => say("select"));
|
||||
|
||||
@@ -28,7 +28,7 @@ export function linkPipeline({ project, pipeline, onOpen, say = () => {} }) {
|
||||
owner = target;
|
||||
retries = 0;
|
||||
// An old project with nothing saved runs the default and stays as it is on disk; a new one keeps what it took.
|
||||
if (own || project.fresh) store(saved, pipeline.slots(), true);
|
||||
if (own || project.fresh) store(saved, pipeline.slots(), !project.fresh); // a new project's whole flag is a fact, not a guess
|
||||
} else if (retries++ < 3) {
|
||||
setTimeout(adopt, 5000);
|
||||
} else {
|
||||
|
||||
@@ -51,13 +51,14 @@ export function createPipelineState(API) {
|
||||
|
||||
async function refreshManifest() {
|
||||
const file = currentModelFile(slots);
|
||||
if (file === lastProbedFile) return;
|
||||
if (file === lastProbedFile) return false;
|
||||
const traits = await probeModelTraits(slots);
|
||||
const manifest = await API.modules(traits ? traits.join(",") : undefined);
|
||||
modulesById = {};
|
||||
(manifest.modules || []).forEach((m) => { modulesById[m.id] = m; });
|
||||
controlState = manifest.control || controlState;
|
||||
if (traits !== undefined) lastProbedFile = file; // only once it worked: a failed probe is tried again
|
||||
return true;
|
||||
}
|
||||
let incomplete = [];
|
||||
let refused = [];
|
||||
@@ -194,18 +195,20 @@ export function createPipelineState(API) {
|
||||
refused = (res && res.refused) || [];
|
||||
queueable = !!(res && res.queueable);
|
||||
saveNotes = (res && res.notes) || [];
|
||||
_changed(); // now: the project this edit was made in takes it, even if another opens during the probe
|
||||
// A no-op for the common case (same file as last time) -- see
|
||||
// refreshManifest()'s own guard. Only a Models & Pipeline edit that
|
||||
// actually changes the loader's file does a second round trip here.
|
||||
// Its own try/catch: a failure here is not "could not save" -- the
|
||||
// edit above already landed -- so it must not overwrite saveNotes
|
||||
// with a message about the wrong failure.
|
||||
let fresh = false;
|
||||
try {
|
||||
await refreshManifest();
|
||||
fresh = await refreshManifest();
|
||||
} catch (e) {
|
||||
console.warn(`[FunPack] could not refresh modules for the new model: ${e && e.message ? e.message : e}`);
|
||||
}
|
||||
_changed(); // after the modules: a listener sees the ones for this model
|
||||
if (fresh && mine === epoch) _changed(); // again once a new model's modules are known: a listener sees those
|
||||
} catch (e) {
|
||||
saveNotes = [`Could not save: ${e && e.message ? e.message : e}`];
|
||||
}
|
||||
|
||||
@@ -82,3 +82,13 @@ test("an Undo copy of the same project keeps receiving edits", async () => {
|
||||
heard([{ ...slots[0], inputs: { ckpt_name: "b" } }]);
|
||||
assert.equal(writes.length, 1);
|
||||
});
|
||||
|
||||
test("a new project keeps the preset pipeline it inherits as whole, so it reopens exactly", async () => {
|
||||
const writes = [];
|
||||
let opened;
|
||||
const project = { fresh: true, project: { id: "n", models: { slots: [] } }, setField: (k, v) => { writes.push(v); project.project[k] = v; } };
|
||||
const pipeline = { adopt: async () => true, slots: () => slots, removedIds: () => ["clip"], unwiredMap: () => ({}), whole: () => true, subscribe: () => {} };
|
||||
linkPipeline({ project, pipeline, onOpen: (fn) => { opened = fn; } });
|
||||
await opened();
|
||||
assert.equal(writes[0].whole, true);
|
||||
});
|
||||
|
||||
@@ -356,3 +356,17 @@ test("a model file no module recognises says so, and why nothing is hidden", asy
|
||||
await PS.ensureLoaded();
|
||||
assert.match(PS.saveNotes().join(" "), /only \.safetensors.*every module is offered/);
|
||||
});
|
||||
|
||||
test("a save reaches listeners before the model probe answers, so a project switched to meanwhile cannot take it", async () => {
|
||||
const PS = withCheckpoint(load([]));
|
||||
await PS.ensureLoaded();
|
||||
let answer;
|
||||
lastApi.probeFamily = () => new Promise((r) => { answer = r; });
|
||||
let heard = 0;
|
||||
PS.subscribe(() => { heard += 1; });
|
||||
const saving = PS.save({ inputs: { model: { ckpt_name: "picked" } } });
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
assert.ok(heard >= 1, "announced while the probe is still out");
|
||||
answer({ detected: true, traits: [] });
|
||||
await saving;
|
||||
});
|
||||
|
||||
+3
-1
@@ -46,8 +46,10 @@ def machine(disk_free_gb=None) -> List[dict]:
|
||||
kind = device()
|
||||
if kind == "mps":
|
||||
out.append(row("ok", "Apple GPU (MPS): generates, slowly; large video models may not fit in its memory."))
|
||||
elif kind != "cuda":
|
||||
elif kind == "cpu":
|
||||
out.append(row("warn", "ComfyUI runs on the CPU: generating is very slow, and only small models fit."))
|
||||
elif kind != "cuda":
|
||||
out.append(row("warn", f"ComfyUI generates on a '{kind}' device, which FunPack is not tested on: expect slow runs, and some features may fail."))
|
||||
else:
|
||||
props = torch.cuda.get_device_properties(0)
|
||||
out.append(row("ok", f"{props.name}, {props.total_memory / 1024 ** 3:.0f} GB, sm_{props.major}{props.minor}"))
|
||||
|
||||
@@ -39,3 +39,6 @@ def test_the_row_names_the_device_comfyui_generates_on(monkeypatch):
|
||||
assert not any("sageattention" in x["text"] for x in rows) # a CUDA-only library
|
||||
monkeypatch.setattr(r, "device", lambda: "cpu") # ComfyUI started with --cpu, GPU or not
|
||||
assert any(x["level"] == "warn" and "on the CPU" in x["text"] for x in r.machine())
|
||||
monkeypatch.setattr(r, "device", lambda: "xpu") # an Intel GPU is not the CPU
|
||||
rows = r.machine()
|
||||
assert any("'xpu'" in x["text"] for x in rows) and not any("on the CPU" in x["text"] for x in rows)
|
||||
|
||||
Reference in New Issue
Block a user