fix(models): a project's graph is no longer the template for new projects

Saving a pipeline mirrored it into the global default, so every later project
adopted an imported workflow. Only FunPack's own seeded loaders travel now; the
family is recorded on its own. Also: creating a linked input no longer self-aborts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-08-20 22:56:28 +03:00
co-authored by Claude Opus 5
parent 33aaa1b883
commit 7ae308f261
4 changed files with 74 additions and 15 deletions
+26
View File
@@ -680,6 +680,32 @@ def reseed_for_family(models: dict, object_info: Optional[dict] = None) -> bool:
return True
def global_template_update(current: Optional[dict], saved: dict) -> Optional[dict]:
"""What a project's save should leave in the global default that seeds NEW projects.
Only FunPack's own seeded loaders travel: those are the file picks anyone's next
project wants, and the reason the global exists. An imported workflow or a hand-wired
graph is that ONE project's business — mirroring it whole is how every later project
came to adopt it, loaders and all. The family is reusable either way, so it is recorded
on its own.
Returns the global to write, or None when the save must not touch it.
"""
current = current if isinstance(current, dict) else {}
saved = saved if isinstance(saved, dict) else {}
if is_seeded_pipeline(saved) and not is_workflow_import(saved):
out = dict(saved)
# A save that simply omits the family must not erase the one already recorded:
# a caller changing it sends it, one rearranging loaders does not.
if not out.get("model_family") and current.get("model_family"):
out["model_family"] = current["model_family"]
return out
family = saved.get("model_family")
if family and current.get("model_family") != family:
return {**current, "model_family": family}
return None
def new_project_models(glob: Optional[dict], object_info: Optional[dict] = None) -> dict:
"""The pipeline a NEW project starts with.
+8 -1
View File
@@ -1679,7 +1679,14 @@
}
ensureLinks().push(link);
applyLinkValue(link, val);
linkMode = false; linkSel = []; await persist(); setView("links");
linkMode = false; linkSel = [];
// Members are picked ON node pages, where saving is deferred until the node's Save
// button. Going through that buffer meant "Save link" only marked the page dirty, and
// the jump to the links view then offered to DISCARD the link it had just made — so a
// link could never be created at all. Pressing Save link is the explicit save.
if (deferSave) { await persistNow(); deferDirty = false; }
else await persist();
setView("links");
}
// Persistent bar while picking link members — stays visible as the user moves
+5 -14
View File
@@ -2614,22 +2614,13 @@ if web is not None and PromptServer is not None:
seeded_now = True
p.models = body
projects.save(p)
# Keep the global default in sync (it seeds new projects) — but never let a save that
# simply omits `model_family` erase the one already recorded there. A caller that
# means to change the family sends it; one that is only rearranging loaders does not,
# and silently clearing it is how every later project ends up with no family at all.
#
# A SEED is not a user edit, and must not reach the global at all: opening a new
# project would otherwise replace the loaders someone configured — the very thing
# the global exists to hand on — with four empty ones.
# A SEED is not a user edit and must not reach the global default at all: opening a
# new project would otherwise replace the loaders someone configured with empty ones.
if seeded_now:
return web.json_response(body)
glob = dict(body)
if not glob.get("model_family"):
inherited = nodes.load_models().get("model_family")
if inherited:
glob["model_family"] = inherited
nodes.save_models(glob)
glob = pipeline_wiring.global_template_update(nodes.load_models(), body)
if glob is not None:
nodes.save_models(glob)
return web.json_response(body)
# --- UI: legacy entry points ---
@@ -522,3 +522,38 @@ def test_the_samplers_own_positive_stays_core_owned():
offered = {p for ports in pipeline_wiring._type_fallback_ports(family).values()
for p in ports}
assert "FunPackLTXAVSceneChainSampler.positive" not in offered
# ── what a project's save leaves in the global default ────────────────────────
def test_an_imported_workflow_never_becomes_the_global_template():
"""Mirroring one project's graph is how every later project came to adopt it."""
current = {"model_family": "ltxav", "slots": []}
saved = dict(IMPORTED_GLOBAL)
assert pipeline_wiring.global_template_update(current, saved) is None
def test_a_hand_wired_graph_stays_that_projects_business():
saved = {"model_family": "ltxav",
"slots": [{"id": "mine", "role": "custom", "node_class": "VAELoader"}]}
assert pipeline_wiring.global_template_update({"model_family": "ltxav"}, saved) is None
def test_seeded_loaders_do_travel_because_that_is_what_the_global_is_for():
"""Picking the same model files in every project is the thing it prevents."""
saved = {"model_family": "ltxav",
"slots": pipeline_wiring.default_pipeline_slots("ltxav", OI_FILES)}
assert pipeline_wiring.global_template_update({}, saved) == saved
def test_the_family_is_recorded_even_when_the_graph_is_not():
saved = dict(IMPORTED_GLOBAL, model_family="minimax_h3")
out = pipeline_wiring.global_template_update({"model_family": "ltxav", "slots": []}, saved)
assert out["model_family"] == "minimax_h3"
assert out["slots"] == [] # the old global's own slots, not the import's
def test_a_save_that_omits_the_family_does_not_erase_it():
saved = {"slots": pipeline_wiring.default_pipeline_slots("ltxav", OI_FILES)}
out = pipeline_wiring.global_template_update({"model_family": "minimax_h3"}, saved)
assert out["model_family"] == "minimax_h3"