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.
This commit is contained in:
digital-garbage
2026-06-11 22:36:33 +03:00
parent 7a4f0cb670
commit 1072da0ce1
4 changed files with 79 additions and 10 deletions
+1
View File
@@ -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"},
}
+34 -7
View File
@@ -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,
+15 -3
View File
@@ -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)));
@@ -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": [