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:
digital-garbage
2026-08-08 22:04:37 +03:00
co-authored by Claude Opus 5
parent 4e6c68bf51
commit bd9cb3062a
2 changed files with 60 additions and 1 deletions
+20
View File
@@ -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.
+40 -1
View File
@@ -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": [