From bd9cb3062a69389e5d27c665d065d5ebbeff8b53 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:04:37 +0300 Subject: [PATCH] fix: a wired positive_conditioning satisfies Studio's CLIP requirement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- movie_editor/backend/builder.py | 20 +++++++++++++++ movie_editor/tests/test_builder.py | 41 +++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/movie_editor/backend/builder.py b/movie_editor/backend/builder.py index 543e68aa..ecde3219 100644 --- a/movie_editor/backend/builder.py +++ b/movie_editor/backend/builder.py @@ -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. diff --git a/movie_editor/tests/test_builder.py b/movie_editor/tests/test_builder.py index 1e04f20d..9b4de70d 100644 --- a/movie_editor/tests/test_builder.py +++ b/movie_editor/tests/test_builder.py @@ -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": [