fix: a wired positive_conditioning satisfies Studio's CLIP requirement
Studio encodes through clip, but a pre-encoded CONDITIONING on positive_conditioning replaces that path entirely. The Editor still treated Studio.clip as unconditionally required, so a project that brought its own conditioning was blocked — and with two text encoders installed it was blocked on 'N possible sources, wire it explicitly' for a port it did not need. The port is now waived when its alternative is wired, and deliberately left unwired in that case: auto-wiring a CLIP would take precedence over the user's own conditioning and silently win. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4e6c68bf51
commit
bd9cb3062a
@@ -86,6 +86,17 @@ OPEN_PORTS: list[tuple[str, str, str, bool]] = [ # (core_id, input, type, requ
|
||||
("audiodec", "audio_vae", "VAE", True),
|
||||
]
|
||||
|
||||
# A required port another input can stand in for: {(core_id, input): (alternative inputs…)}.
|
||||
# Studio encodes the prompt through `clip`, but a pre-encoded CONDITIONING wired into
|
||||
# positive_conditioning replaces that path entirely (_v2_conditioning_source takes whichever
|
||||
# is present, CLIP first). So a project that brings its own conditioning must not be blocked
|
||||
# for leaving CLIP unwired — or, with several text encoders installed, for its CLIP being
|
||||
# ambiguous. The port stays in OPEN_PORTS: it still auto-wires when there IS a single
|
||||
# obvious encoder, it just no longer holds up generation when the alternative is satisfied.
|
||||
PORT_ALTERNATIVES: dict[tuple[str, str], tuple[str, ...]] = {
|
||||
("studio", "clip"): ("positive_conditioning",),
|
||||
}
|
||||
|
||||
# core outputs offered as auto-wire producers for slot inputs (image-proc etc.).
|
||||
CORE_PRODUCERS: list[tuple[str, int, str]] = [ # (core_id, output_index, type)
|
||||
("frames", 0, "INT"),
|
||||
@@ -1111,6 +1122,15 @@ def _autowire(graph, slots, slot_node_id, slot_def, object_info, producers, repo
|
||||
continue
|
||||
if isinstance(node["inputs"].get(inp), list):
|
||||
continue # already wired (explicit/core)
|
||||
alt_wired = next((alt for alt in PORT_ALTERNATIVES.get((node_id, inp), ())
|
||||
if isinstance(node["inputs"].get(alt), list)), None)
|
||||
if alt_wired:
|
||||
# Left deliberately unwired: the alternative the user wired IS the source now, and
|
||||
# auto-wiring this port would take precedence over it and silently win.
|
||||
report["auto_wired"].append(
|
||||
f"{L(node_id)}.{inp} ({t}) left unwired — {L(node_id)}.{alt_wired} is wired and "
|
||||
f"takes over. Not required.")
|
||||
continue
|
||||
raw = [p for p in _matching_producers(producers, t) if p[0] != node_id]
|
||||
# Never auto-wire a source that already depends on this node — that closes a loop and
|
||||
# ComfyUI rejects the whole graph. Drop such candidates so auto-wire stays acyclic.
|
||||
|
||||
@@ -13,7 +13,7 @@ OI = {
|
||||
"studio_settings": ["STRING", {"default": "{}"}],
|
||||
"adjustments": ["STRING", {"default": "[]"}]},
|
||||
"optional": {"model": ["MODEL"], "clip": ["CLIP"], "source_image": ["IMAGE"],
|
||||
"latent": ["LATENT"],
|
||||
"latent": ["LATENT"], "positive_conditioning": ["CONDITIONING"],
|
||||
"positive_prompt": ["STRING", {"forceInput": True}],
|
||||
"negative_prompt": ["STRING", {"forceInput": True}],
|
||||
"refinement_key_input": ["STRING", {"forceInput": True}]}},
|
||||
@@ -52,6 +52,7 @@ OI = {
|
||||
"UnetLoader": {"input": {"required": {"unet_name": [["m.safetensors"]]}}, "output": ["MODEL"]},
|
||||
"ClipLoader": {"input": {"required": {"clip_name": [["c.safetensors"]]}}, "output": ["CLIP"]},
|
||||
"VaeLoader": {"input": {"required": {"vae_name": [["v.safetensors"]]}}, "output": ["VAE"]},
|
||||
"CondLoader": {"input": {"required": {"cond_name": [["c.json"]]}}, "output": ["CONDITIONING"]},
|
||||
"LoadImage": {"input": {"required": {"image": [["a.png"]]}}, "output": ["IMAGE", "MASK"]},
|
||||
"ImgProc": {"input": {"required": {"length": ["INT", {"default": 97}], "vae": ["VAE"], "image": ["IMAGE"]}},
|
||||
"output": ["LATENT", "LATENT", "IMAGE"], "output_name": ["latent", "Latent", "output_image"]},
|
||||
@@ -145,6 +146,44 @@ def test_explicit_wires_and_autowire():
|
||||
assert report["ambiguous"] == []
|
||||
|
||||
|
||||
def test_wired_positive_conditioning_replaces_the_clip_requirement():
|
||||
# Studio can run off a pre-encoded CONDITIONING. With two text encoders installed, CLIP
|
||||
# is ambiguous — that must not block generation once positive_conditioning is wired, and
|
||||
# the ambiguous CLIP must be left alone rather than auto-wired over the user's own
|
||||
# conditioning (Studio prefers CLIP whenever it's connected).
|
||||
models = {"full_control": True, "slots": [
|
||||
{"id": "u", "node_class": "UnetLoader", "inputs": {}, "wires": {"MODEL": "port:FunPackStudio.model"}},
|
||||
{"id": "c1", "node_class": "ClipLoader", "inputs": {}, "wires": {}},
|
||||
{"id": "c2", "node_class": "ClipLoader", "inputs": {}, "wires": {}},
|
||||
{"id": "v", "node_class": "VaeLoader", "inputs": {}, "wires": {}},
|
||||
{"id": "li", "node_class": "LoadImage", "inputs": {}, "wires": {"IMAGE": "node:ip:image"}},
|
||||
{"id": "ip", "node_class": "ImgProc", "inputs": {},
|
||||
"wires": {"Latent": "port:LTXVConcatAVLatent.audio_latent"}},
|
||||
{"id": "cd", "node_class": "CondLoader", "inputs": {},
|
||||
"wires": {"CONDITIONING": "port:FunPackStudio.positive_conditioning"}},
|
||||
]}
|
||||
graph, report = builder.build(OI, models, PARAMS)
|
||||
assert graph["studio"]["inputs"]["positive_conditioning"] == ["slot_cd", 0]
|
||||
assert "clip" not in graph["studio"]["inputs"]
|
||||
assert report["blocking"] == []
|
||||
assert not any(".clip" in m for m in report["ambiguous"])
|
||||
|
||||
|
||||
def test_clip_still_blocks_when_nothing_else_feeds_studio():
|
||||
# Without the conditioning wire, an ambiguous CLIP is still a hard stop.
|
||||
models = {"full_control": True, "slots": [
|
||||
{"id": "u", "node_class": "UnetLoader", "inputs": {}, "wires": {"MODEL": "port:FunPackStudio.model"}},
|
||||
{"id": "c1", "node_class": "ClipLoader", "inputs": {}, "wires": {}},
|
||||
{"id": "c2", "node_class": "ClipLoader", "inputs": {}, "wires": {}},
|
||||
{"id": "v", "node_class": "VaeLoader", "inputs": {}, "wires": {}},
|
||||
{"id": "li", "node_class": "LoadImage", "inputs": {}, "wires": {"IMAGE": "node:ip:image"}},
|
||||
{"id": "ip", "node_class": "ImgProc", "inputs": {},
|
||||
"wires": {"Latent": "port:LTXVConcatAVLatent.audio_latent"}},
|
||||
]}
|
||||
_, report = builder.build(OI, models, PARAMS)
|
||||
assert any(".clip" in m for m in report["blocking"])
|
||||
|
||||
|
||||
def test_stale_combo_value_coerced_to_live_choice():
|
||||
# "old.safetensors" was renamed/removed; the live spec only has "m.safetensors".
|
||||
models = {"full_control": True, "slots": [
|
||||
|
||||
Reference in New Issue
Block a user