fix: skipped slots offer no Remove/Swap, stand-in load clears skipped, null pass value refused; route-level and precedence tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
803ea68f21
commit
1565ac3982
+1
-1
@@ -38,7 +38,7 @@ A rewrite from scratch, not an update of 4.x. Same idea (a studio for video insi
|
||||
- **`.gguf` text encoders** need the ComfyUI-GGUF node pack and work only for architectures it understands; otherwise they are refused with a message. Gemma 4 `.gguf` does not work; a loader for it may come later.
|
||||
- **H3 start frame**: in the H3 preset the scene picture sets the video's shape only; a start frame is not part of it.
|
||||
- **H3 second pass** is held back: not offered until verified.
|
||||
- The ready-made pipelines need Save video, Load media, the loaders, the sampler and decode: if the module behind one of those fails to load, Generate names the missing node and refuses. The optional nodes (Enhance prompt, Shot camera, Prompt markup, Negative erase, Cut opening, Identity transfer) are skipped instead, with a note beside Generate, and their controls are hidden. The one exception: the Reference 1 picture of Identity transfer stays on screen and does nothing until the module is back.
|
||||
- The ready-made pipelines need Save video, Load media, the loaders, the sampler and decode: if the module behind one of those fails to load, Generate names the missing node and refuses. The optional nodes (Enhance prompt, Shot camera, Prompt markup, Negative erase, Cut opening, Identity transfer) are skipped instead, with a note beside Generate, and their controls are hidden. Which nodes are optional is declared by the model module (MiniMax H3, LTX), so it applies while that module loads. The one exception: the Reference 1 picture of Identity transfer stays on screen and does nothing until the module is back.
|
||||
- Turning some modules on switches the model compiler off for the run, which costs sampling time.
|
||||
|
||||
### Docs
|
||||
|
||||
@@ -126,7 +126,7 @@ export const models = (app) => function mount() {
|
||||
const spec = specs[slot.node];
|
||||
if (spec === undefined) return [c.hint.default({ text: "Loading…" })];
|
||||
const head = c.header.sm({ text: label(slot) });
|
||||
if (spec === null && (ps.skipped && ps.skipped() || {})[slot.id]) return [head, structure(slot), c.banner.warn({ text: `${slot.node} is skipped: its module did not load, so the run goes without it. Settings ▸ Modules says why; when it is back, this node runs again.` })];
|
||||
if (spec === null && (ps.skipped && ps.skipped() || {})[slot.id]) return [head, c.banner.warn({ text: `${slot.node} is skipped: its module did not load, so the run goes without it. Settings ▸ Modules says why; when it is back, this node runs again.` })];
|
||||
if (spec === null) return [head, structure(slot), c.banner.warn({ text: `${slot.node} is not installed — this slot can't be edited or run. Install the pack that provides it, swap it for another node, or remove it.` }), installRow(slot.node)];
|
||||
const sockets = (spec.sockets || []).filter((w) => showing(w, slot, spec)).map((s) => c.field.default({ label: s.name, hint: s.required ? `${s.type} · needed` : s.type, control: wiring(slot, s.name, s.type, slot.inputs && slot.inputs[s.name], "Not connected") }));
|
||||
const rows = (spec.widgets || []).filter((w) => showing(w, slot, spec)).flatMap((w) => {
|
||||
|
||||
@@ -690,6 +690,7 @@ export function createPipelineState(rawApi, { answerMs = ANSWER_MS } = {}) {
|
||||
// Nothing saved was accepted (a failed request, or a file the server refuses): the default
|
||||
// runs, said, and the caller must NOT save it over the project's own copy.
|
||||
slots = JSON.parse(JSON.stringify(offered));
|
||||
skipped = {}; // the stand-in is the default pipeline: whatever the last project skipped says nothing about it
|
||||
removed = new Set(); unwired = {}; whole = false; // the stand-in is the plain default: no flags of the project it could not load
|
||||
incomplete = []; refused = []; queueable = false; // nor the last project's readiness
|
||||
saveNotes = ["This project's saved pipeline could not be loaded, so the default is in use."];
|
||||
|
||||
@@ -295,6 +295,8 @@ def bypass_missing(slots: Sequence[dict], schemas: Optional[Schemas] = None,
|
||||
if name is None or name not in (slot.get("inputs") or {}):
|
||||
return None, value[0]
|
||||
value = slot["inputs"][name]
|
||||
if value is None: # nothing was typed or wired there
|
||||
return None, slot["id"]
|
||||
return None, value[0] # skipped slots reading each other in a loop
|
||||
|
||||
while gone: # a slot that cannot be passed through stays; that can strand another, so look again
|
||||
|
||||
@@ -796,3 +796,13 @@ def test_the_table_marks_nodes_by_class_for_pipelines_saved_without_any_marker()
|
||||
table = {"Rewrite": {"pass": {"0": "text"}}}
|
||||
prompt, problems = graph.build(slots, _without("Rewrite", "Markup"), table)
|
||||
assert any("Markup" in p for p in problems) and not any("Rewrite" in p for p in problems) # Markup is not in the table: still required
|
||||
|
||||
|
||||
def test_a_slots_own_marker_wins_over_the_table_and_a_null_pass_value_is_not_passed_on():
|
||||
slots = _optional_pipeline()
|
||||
slots[2]["optional"] = {"pass": {"0": "text"}}
|
||||
prompt, _ = graph.build(slots, _without("Rewrite", "Markup"), {"Rewrite": {"pass": {"0": "enabled"}}})
|
||||
assert prompt["encode"]["inputs"]["prompt"] == "a typed prompt" # the slot said "text"; the table said "enabled"
|
||||
slots[2]["inputs"]["text"] = None
|
||||
_prompt, problems = graph.build(slots, _without("Rewrite", "Markup"))
|
||||
assert any("no node called" in p for p in problems) # nothing typed: refused, not sent as null
|
||||
|
||||
@@ -568,3 +568,18 @@ def test_empty_values_do_not_wipe_the_settings_the_slots_carry(server):
|
||||
assert status == 200, body
|
||||
settings = next(s for s in body["slots"] if s["node"] == "FunPackModifierSettings")
|
||||
assert json.loads(settings["inputs"]["settings"]) == {"sampling_alg": {"enabled": True}}
|
||||
|
||||
|
||||
def test_an_optional_node_that_is_not_installed_is_skipped_over_http_and_said(server, monkeypatch):
|
||||
"""The call sites pass the table: the check route Generate uses, and the pipeline GET."""
|
||||
monkeypatch.setattr(routes, "optional_nodes", lambda: {"NoSuchModuleNode": {"pass": {"0": "text"}}})
|
||||
slots = [{"id": "gone", "node": "NoSuchModuleNode", "inputs": {"text": "typed prompt"}},
|
||||
{"id": "use", "node": "PrimitiveString", "inputs": {"value": ["gone", 0]}}]
|
||||
status, body = _request(server, "POST", "/funpack/api/pipeline", {"action": "check", "slots": slots})
|
||||
assert status == 200, body
|
||||
assert [s["slot"] for s in body["skipped"]] == ["gone"]
|
||||
assert body["notes"][0].startswith("Skipped, its module did not load") and "NoSuchModuleNode" in body["notes"][0]
|
||||
assert body["incomplete"] == [] and body["prompt"]["use"]["inputs"]["value"] == "typed prompt"
|
||||
monkeypatch.setattr(routes, "optional_nodes", lambda: {})
|
||||
status, body = _request(server, "POST", "/funpack/api/pipeline", {"action": "check", "slots": slots})
|
||||
assert body["skipped"] == [] and any("no node called 'NoSuchModuleNode'" in p for p in body["incomplete"])
|
||||
|
||||
@@ -44,7 +44,7 @@ def _cases():
|
||||
return [(preset["id"], slot["id"]) for preset in _presets() for slot in preset["slots"] if slot["node"] in table]
|
||||
|
||||
|
||||
def test_the_six_optional_nodes_are_declared():
|
||||
def test_the_seven_optional_node_classes_are_declared():
|
||||
nodes = {s["node"] for p in _presets() for s in p["slots"] if s["node"] in _table()}
|
||||
assert nodes == {"FunPackEnhancePrompt", "FunPackShotCamera", "FunPackPromptMarkup", "FunPackApplyPromptMarkup",
|
||||
"FunPackNegativeErase", "FunPackCutOpening", "FunPackIdentityTransfer"}
|
||||
|
||||
Reference in New Issue
Block a user