From 1072da0ce12a05cdeb09bbcc6e08e902afff22b9 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Thu, 11 Jun 2026 22:36:33 +0300 Subject: [PATCH] Clarify guided wiring: LATENT and IMAGE go to Studio, not Concat video. Concat AV video_latent is an internal Studio output; guided mode now routes user LATENT/IMAGE through Studio.latent and source_image with chain support. --- movie_editor/backend/nodes.py | 1 + movie_editor/backend/pipeline_wiring.py | 41 ++++++++++++++++++---- movie_editor/frontend/models.js | 18 ++++++++-- movie_editor/tests/test_pipeline_wiring.py | 29 +++++++++++++++ 4 files changed, 79 insertions(+), 10 deletions(-) diff --git a/movie_editor/backend/nodes.py b/movie_editor/backend/nodes.py index b3788db8..be0c1d19 100644 --- a/movie_editor/backend/nodes.py +++ b/movie_editor/backend/nodes.py @@ -85,6 +85,7 @@ ROLES: dict[str, dict] = { "clip_vision": {"label": "CLIP Vision", "category": "Loaders", "output": "CLIP_VISION"}, "image_processing": {"label": "Input Image Processing", "category": "Pipeline", "output": "IMAGE", "input": "IMAGE"}, "empty_latent": {"label": "Empty Latent Generator", "category": "Pipeline", "output": "LATENT"}, + "video_latent": {"label": "Video Latent Source", "category": "Pipeline", "output": "LATENT"}, "audio_encoder": {"label": "Audio Encoder", "category": "Pipeline", "output": "LATENT"}, } diff --git a/movie_editor/backend/pipeline_wiring.py b/movie_editor/backend/pipeline_wiring.py index 3150e287..e754ae8c 100644 --- a/movie_editor/backend/pipeline_wiring.py +++ b/movie_editor/backend/pipeline_wiring.py @@ -19,19 +19,28 @@ ROLE_WIRE_TARGETS: dict[str, list[tuple[str, Optional[str], str]]] = { "audio_vae": [("VAE", None, "LTXVAudioVAEDecode.audio_vae")], "audio_encoder": [("LATENT", None, "LTXVConcatAVLatent.audio_latent")], "empty_latent": [("LATENT", None, "FunPackStudio.latent")], + "video_latent": [("LATENT", None, "FunPackStudio.latent")], "image_processing": [ ("IMAGE", None, "FunPackStudio.source_image"), ("LATENT", None, "FunPackStudio.latent"), ], } -# MODEL / CLIP may pass through patcher nodes (LoRA, etc.) via node:* wires; these are the -# only built-in ports they may terminate on in guided mode. +# MODEL / CLIP / LATENT / IMAGE may pass through patcher or encode nodes via node:* wires; +# these are the built-in ports they may terminate on in guided mode. TYPE_CHAIN_TERMINALS: dict[str, list[str]] = { "MODEL": ["FunPackStudio.model"], "CLIP": ["FunPackStudio.clip"], + "LATENT": ["FunPackStudio.latent"], + "IMAGE": ["FunPackStudio.source_image"], } +# Core-internal ports: shown in the full port list but not user-wirable in guided mode +# (another core node feeds them automatically). +GUIDED_HIDDEN_PORTS: frozenset[str] = frozenset({ + "LTXVConcatAVLatent.video_latent", # Studio output 12 -> concat (CORE_LINKS) +}) + DEFAULT_WIRES_BY_ROLE: dict[str, dict[str, str]] = { "unet": {"MODEL": "port:FunPackStudio.model"}, "clip": {"CLIP": "port:FunPackStudio.clip"}, @@ -39,6 +48,7 @@ DEFAULT_WIRES_BY_ROLE: dict[str, dict[str, str]] = { "audio_vae": {"VAE": "port:LTXVAudioVAEDecode.audio_vae"}, "audio_encoder": {"LATENT": "port:LTXVConcatAVLatent.audio_latent"}, "empty_latent": {"LATENT": "port:FunPackStudio.latent"}, + "video_latent": {"LATENT": "port:FunPackStudio.latent"}, "image_processing": {"IMAGE": "port:FunPackStudio.source_image"}, } @@ -50,8 +60,8 @@ DEFAULT_INPUT_SOURCES_BY_ROLE: dict[str, dict[str, str]] = { PORT_LABELS: dict[str, str] = { "FunPackStudio.model": "Studio · model", "FunPackStudio.clip": "Studio · clip", + "FunPackStudio.latent": "Studio · latent (forwards to Concat AV · video_latent)", "FunPackStudio.source_image": "Studio · source_image (Img2Video anchor)", - "FunPackStudio.latent": "Studio · latent", "FunPackLTXAVSceneChainSampler.vae": "Chain Sampler · vae", "LTXVConcatAVLatent.audio_latent": "Concat AV Latent · audio_latent", "LTXVAudioVAEDecode.audio_vae": "Audio VAE Decode · audio_vae", @@ -74,15 +84,20 @@ def wiring_locked(models: Any) -> bool: def allowed_port_ids(role: str, out_type: str, out_name: Optional[str] = None) -> list[str]: out: list[str] = [] - for t, name, port in ROLE_WIRE_TARGETS.get(role or "", []): + role_rules = ROLE_WIRE_TARGETS.get(role or "", []) + for t, name, port in role_rules: if t != out_type: continue if name is not None and out_name is not None and name != out_name: continue out.append(port) - for port in TYPE_CHAIN_TERMINALS.get(out_type, []): - if port not in out: - out.append(port) + # Generic chain terminals only when the role has no explicit rule for this type + # (e.g. audio_encoder LATENT must stay on Concat · audio_latent, not Studio · latent). + has_explicit = any(t == out_type for t, _, _ in role_rules) + if not has_explicit: + for port in TYPE_CHAIN_TERMINALS.get(out_type, []): + if port not in out: + out.append(port) return out @@ -108,6 +123,17 @@ def validate_port_wire( if not target.startswith("port:"): return f"Unknown wire target '{target}'." port_id = target[5:] + if port_id in GUIDED_HIDDEN_PORTS: + if port_id == "LTXVConcatAVLatent.video_latent": + return ( + f"{out_name} ({out_type}) cannot wire directly to Concat AV Latent · video_latent " + f"in guided mode — wire to Studio · latent instead; the built-in pipeline " + f"forwards Studio output to Concat automatically." + ) + return ( + f"{out_name} ({out_type}) cannot wire to {port_label(port_id)} in guided mode " + f"(internal core link). Enable Full control to override." + ) allowed = allowed_port_ids(role, out_type, out_name) if not allowed and role in ("custom", "", None): # Legacy slots without a role: allow canonical built-in ports for this output type. @@ -193,6 +219,7 @@ def wiring_rules_payload() -> dict[str, Any]: return { "role_targets": role_targets, "type_chain_terminals": TYPE_CHAIN_TERMINALS, + "guided_hidden_ports": sorted(GUIDED_HIDDEN_PORTS), "default_wires": DEFAULT_WIRES_BY_ROLE, "default_input_sources": DEFAULT_INPUT_SOURCES_BY_ROLE, "port_labels": PORT_LABELS, diff --git a/movie_editor/frontend/models.js b/movie_editor/frontend/models.js index cd2ed7d6..8ffa888e 100644 --- a/movie_editor/frontend/models.js +++ b/movie_editor/frontend/models.js @@ -56,12 +56,18 @@ const role = slot.role || "custom"; const rules = wiringRules.role_targets?.[role] || []; const chainPorts = wiringRules.type_chain_terminals?.[out.type] || []; + const hidden = new Set(wiringRules.guided_hidden_ports || []); const allowedPorts = [...rules .filter((r) => r.type === out.type && (r.output_name == null || r.output_name === out.name)) .map((r) => "port:" + r.port), ...chainPorts.map((p) => "port:" + p), ].filter((p, i, a) => a.indexOf(p) === i); - return all.filter((d) => !d.value.startsWith("port:") || allowedPorts.includes(d.value)); + return all.filter((d) => { + if (!d.value.startsWith("port:")) return true; + const id = d.value.slice(5); + if (hidden.has(id)) return false; + return allowedPorts.includes(d.value); + }); } function allowedSources(slot, ci) { @@ -608,7 +614,10 @@ } if (pipelineLocked()) { sec.append(el("div", "req-hint guided-hint", - "Guided wiring is on — MODEL/CLIP may chain through LoRA loaders before Studio. Audio latent, image anchor, and VAE ports stay role-locked. Enable Full control to rewire everything manually.")); + "Guided wiring: LATENT → Studio · latent (core forwards to Concat · video_latent). " + + "IMAGE → Studio · source_image (Input Image Processing defaults to Timeline scene image). " + + "MODEL/CLIP may chain through LoRA. Audio latent → Concat · audio_latent only. " + + "Enable Full control for manual rewiring.")); } sec.append(el("div", "req-title", "Pipeline requirements")); @@ -863,7 +872,10 @@ } if (coreOpen) { const hint = pipelineLocked() - ? "Fixed FunPack path (Studio → Conditioning → Chain Sampler → decode). Wire Unet → LoRA → Studio via slot outputs; MODEL/CLIP may chain through patcher nodes. Audio latent, image anchor, and VAE ports stay fixed unless Full control is on." + ? "Fixed FunPack path (Studio → Conditioning → Chain Sampler → decode). " + + "Wire video LATENT to Studio · latent (not Concat · video_latent — that link is internal). " + + "Wire IMAGE via Input Image Processing → Studio · source_image (Timeline default). " + + "MODEL/CLIP may chain through patchers. Enable Full control to override core links." : "The fixed FunPack nodes and their wiring. Each input defaults to its built-in source — pick another to re-wire it."; sec.append(el("div", "links-hint", hint)); coreNodes.forEach((n) => sec.append(coreCard(n))); diff --git a/movie_editor/tests/test_pipeline_wiring.py b/movie_editor/tests/test_pipeline_wiring.py index bb5eebfd..348077f4 100644 --- a/movie_editor/tests/test_pipeline_wiring.py +++ b/movie_editor/tests/test_pipeline_wiring.py @@ -77,6 +77,35 @@ def test_custom_model_may_terminate_at_studio(): assert ok is None +def test_latent_chains_to_studio_not_concat_video(): + ok = pipeline_wiring.validate_port_wire( + role="empty_latent", out_type="LATENT", out_name="LATENT", + target="port:FunPackStudio.latent", models={"slots": []}, + ) + assert ok is None + + err = pipeline_wiring.validate_port_wire( + role="empty_latent", out_type="LATENT", out_name="LATENT", + target="port:LTXVConcatAVLatent.video_latent", models={"slots": []}, + ) + assert err is not None + assert "Studio · latent" in err + + ok = pipeline_wiring.validate_port_wire( + role="custom", out_type="LATENT", out_name="LATENT", + target="port:FunPackStudio.latent", models={"slots": []}, + ) + assert ok is None + + +def test_image_chains_to_studio_source_image(): + ok = pipeline_wiring.validate_port_wire( + role="custom", out_type="IMAGE", out_name="IMAGE", + target="port:FunPackStudio.source_image", models={"slots": []}, + ) + assert ok is None + + def test_duplicate_port_wire_blocked(): models = { "slots": [