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:
@@ -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"},
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user