Files
Azornes-Comfyui-Model-Resolver/core/workflow/subgraphs.py
T
Dariusz L 413e7cc0ab Promote subgraph widget contexts to top-level model references
When a subgraph exposes a promoted widget and the instance value or connected
inner widget contains a usable model path, create a top-level model reference
on the subgraph node instead of only pointing to the inner definition node.
This lets resolution, location, and queue updates target the instance widget
directly.

Also handle serialized subgraph instances that omit proxyWidgets by
reconstructing the same promotion from subgraph input/link metadata, and
extend widget values for promoted inputs in the workflow updater.
2026-08-03 04:17:49 +02:00

497 lines
17 KiB
Python

"""Subgraph traversal and promoted-widget context handling."""
import os
from typing import Any, Dict, List, Optional
from . import references
from .widgets import (
_has_widget_input,
_proxy_widget_name,
_proxy_widget_node_id,
_widget_item_name_candidates,
get_widget_name_candidates,
get_widget_name_hint,
normalize_widget_name,
)
def _get_node_by_id(
nodes: List[Dict[str, Any]], node_id: Any
) -> Optional[Dict[str, Any]]:
node_id_text = str(node_id)
for node in nodes:
if str(node.get("id")) == node_id_text:
return node
return None
def _get_widget_index_for_input_index(
node: Dict[str, Any], input_index: int
) -> Optional[int]:
widget_index = -1
for idx, item in enumerate(node.get("inputs", [])):
if _has_widget_input(item):
widget_index += 1
if idx == input_index:
return widget_index if _has_widget_input(item) else None
return None
def _get_widget_index_by_name(
node: Dict[str, Any], widget_name: str
) -> Optional[int]:
target_name = normalize_widget_name(widget_name)
if not target_name:
return None
widgets_values = node.get("widgets_values", [])
for widget_index in range(len(widgets_values)):
candidates = get_widget_name_candidates(node, widget_index)
if any(
normalize_widget_name(candidate) == target_name for candidate in candidates
):
return widget_index
for input_index, item in enumerate(node.get("inputs", [])):
if not _has_widget_input(item):
continue
candidates = _widget_item_name_candidates(item)
if any(
normalize_widget_name(candidate) == target_name for candidate in candidates
):
return _get_widget_index_for_input_index(node, input_index)
return None
def _get_subgraph_input_link_ids(subgraph: Dict[str, Any], input_name: str) -> set:
target_name = normalize_widget_name(input_name)
if not target_name:
return set()
link_ids = set()
for subgraph_input in subgraph.get("inputs", []):
candidates = _widget_item_name_candidates(subgraph_input)
if not any(
normalize_widget_name(candidate) == target_name for candidate in candidates
):
continue
for link_id in subgraph_input.get("linkIds", []) or []:
link_ids.add(str(link_id))
return link_ids
def _get_node_input_by_name(
node: Dict[str, Any], input_name: str
) -> Optional[Dict[str, Any]]:
target_name = normalize_widget_name(input_name)
if not target_name:
return None
for item in node.get("inputs", []):
candidates = _widget_item_name_candidates(item)
if any(
normalize_widget_name(candidate) == target_name for candidate in candidates
):
return item
return None
def _get_node_widget_value(node: Dict[str, Any], widget_index: Any) -> Any:
try:
widget_index = int(widget_index)
except (TypeError, ValueError):
return None
widgets_values = node.get("widgets_values", [])
if not isinstance(widgets_values, list) or widget_index < 0:
return None
if widget_index >= len(widgets_values):
return None
return widgets_values[widget_index]
def _has_promoted_value(value: Any) -> bool:
if value is None:
return False
if isinstance(value, str):
return bool(value.strip())
return True
def _promoted_widget_target_info(
node: Dict[str, Any], widget_index: int, proxy_widget_name: str = ""
) -> Dict[str, Any]:
widget_name = get_widget_name_hint(node, widget_index) or proxy_widget_name
return {
"node_id": node.get("id"),
"node_type": node.get("type", ""),
"node_title": str(node.get("title", "") or "").strip(),
"widget_index": widget_index,
"widget_name": widget_name,
"category": references.get_effective_model_category_hint(node, widget_index),
"value": _get_node_widget_value(node, widget_index),
}
def _find_promoted_widget_targets(
subgraph: Dict[str, Any], proxy_node_id: str, proxy_widget_name: str
) -> List[Dict[str, Any]]:
nodes = subgraph.get("nodes", [])
targets: List[Dict[str, Any]] = []
seen = set()
def add_target(node: Optional[Dict[str, Any]], widget_index: Optional[int]):
if node is None or widget_index is None:
return
key = (str(node.get("id")), int(widget_index))
if key in seen:
return
seen.add(key)
targets.append(
_promoted_widget_target_info(node, widget_index, proxy_widget_name)
)
if proxy_node_id and proxy_node_id != "-1":
target_node = _get_node_by_id(nodes, proxy_node_id)
add_target(
target_node,
_get_widget_index_by_name(target_node or {}, proxy_widget_name),
)
return targets
link_ids = _get_subgraph_input_link_ids(subgraph, proxy_widget_name)
if link_ids:
for node in nodes:
for input_index, item in enumerate(node.get("inputs", [])):
if not _has_widget_input(item):
continue
if str(item.get("link")) not in link_ids:
continue
add_target(node, _get_widget_index_for_input_index(node, input_index))
if targets:
return targets
for node in nodes:
add_target(node, _get_widget_index_by_name(node, proxy_widget_name))
return targets
def _build_promoted_widget_contexts(
workflow_json: Dict[str, Any], subgraphs: List[Dict[str, Any]]
) -> Dict[str, Dict[Any, Any]]:
subgraphs_by_id = {str(sg.get("id")): sg for sg in subgraphs if sg.get("id")}
subgraph_names = {
str(sg.get("id")): sg.get("name", sg.get("id"))
for sg in subgraphs
if sg.get("id")
}
contexts = {
"instance_widgets": {},
"inner_widgets": {},
"inner_widget_names": {},
}
for instance_node in workflow_json.get("nodes", []):
subgraph_id = str(instance_node.get("type", ""))
subgraph = subgraphs_by_id.get(subgraph_id)
if not subgraph:
continue
proxy_widgets = instance_node.get("properties", {}).get("proxyWidgets", [])
if not isinstance(proxy_widgets, list):
proxy_widgets = []
seen_proxy_indexes = set()
seen_proxy_names = set()
def add_instance_context(
proxy_index: Optional[int],
proxy_widget_name: str,
targets: List[Dict[str, Any]],
) -> None:
if proxy_index is None or not proxy_widget_name or not targets:
return
if proxy_index in seen_proxy_indexes:
return
instance_input = _get_node_input_by_name(
instance_node, proxy_widget_name
)
input_connected = bool(
instance_input and instance_input.get("link") is not None
)
instance_value = _get_node_widget_value(instance_node, proxy_index)
target_value = targets[0].get("value")
promoted_value = (
instance_value
if _has_promoted_value(instance_value)
else target_value
)
instance_context = {
**targets[0],
"subgraph_id": subgraph_id,
"subgraph_name": subgraph_names.get(subgraph_id, subgraph_id),
"proxy_widget_index": proxy_index,
"proxy_widget_name": proxy_widget_name,
"promoted_value": promoted_value,
"input_connected": input_connected,
}
contexts["instance_widgets"][
(str(instance_node.get("id")), proxy_index)
] = instance_context
locator = {
"node_id": instance_node.get("id"),
"node_type": instance_node.get("type", ""),
"node_title": str(instance_node.get("title", "") or "").strip(),
"subgraph_id": subgraph_id,
"subgraph_name": subgraph_names.get(subgraph_id, subgraph_id),
"is_top_level": True,
"proxy_widget_index": proxy_index,
"proxy_widget_name": proxy_widget_name,
"promoted_value": promoted_value,
"input_connected": input_connected,
}
seen_proxy_indexes.add(proxy_index)
seen_proxy_names.add(normalize_widget_name(proxy_widget_name))
for target in targets:
exact_key = (
subgraph_id,
str(target.get("node_id")),
target.get("widget_index"),
)
contexts["inner_widgets"].setdefault(exact_key, []).append(locator)
widget_name = normalize_widget_name(target.get("widget_name"))
if widget_name:
name_key = (
subgraph_id,
str(target.get("node_id")),
widget_name,
)
contexts["inner_widget_names"].setdefault(name_key, []).append(
locator
)
for proxy_index, proxy_entry in enumerate(proxy_widgets):
proxy_widget_name = _proxy_widget_name(proxy_entry)
if not proxy_widget_name:
continue
targets = _find_promoted_widget_targets(
subgraph,
_proxy_widget_node_id(proxy_entry),
proxy_widget_name,
)
add_instance_context(proxy_index, proxy_widget_name, targets)
# Some serialized workflows omit proxyWidgets even though a subgraph
# input is connected to a widget inside the definition. Reconstruct the
# same promotion from the subgraph input/link metadata in that case.
instance_inputs = instance_node.get("inputs", [])
if not isinstance(instance_inputs, list):
instance_inputs = []
for subgraph_input in subgraph.get("inputs", []):
input_names = _widget_item_name_candidates(subgraph_input)
proxy_widget_name = next(
(name for name in input_names if str(name or "").strip()),
"",
)
if not proxy_widget_name:
continue
if normalize_widget_name(proxy_widget_name) in seen_proxy_names:
continue
instance_input_index = next(
(
index
for index, item in enumerate(instance_inputs)
if _get_node_input_by_name(
{"inputs": [item]}, proxy_widget_name
)
),
None,
)
if instance_input_index is None:
continue
proxy_index = _get_widget_index_for_input_index(
instance_node, instance_input_index
)
targets = _find_promoted_widget_targets(
subgraph, "-1", proxy_widget_name
)
add_instance_context(proxy_index, proxy_widget_name, targets)
return contexts
def _apply_instance_promoted_widget_context(
ref: Dict[str, Any],
context: Optional[Dict[str, Any]],
available_models: Optional[List[Dict[str, Any]]],
) -> None:
if not context:
return
ref["promoted_widget_name"] = context.get("proxy_widget_name", "")
ref["promoted_inner_node_id"] = context.get("node_id")
ref["promoted_inner_node_type"] = context.get("node_type", "")
ref["promoted_inner_node_title"] = context.get("node_title", "")
ref["promoted_inner_widget_index"] = context.get("widget_index")
ref["promoted_inner_widget_name"] = context.get("widget_name", "")
category = context.get("category")
original_path = ref.get("original_path", "")
if not category or not original_path:
return
resolved = references.try_resolve_model_path(
original_path,
[category],
available_models=available_models,
)
ref["category"] = category
if resolved:
_, full_path = resolved
ref["full_path"] = full_path
ref["exists"] = os.path.exists(full_path)
else:
ref["full_path"] = None
ref["exists"] = False
def _select_promoted_locator(
locators: List[Dict[str, Any]], original_path: str
) -> Optional[Dict[str, Any]]:
if not locators:
return None
for locator in locators:
if locator.get("promoted_value") == original_path:
return locator
return locators[0]
def _get_promoted_widget_locators(
ref: Dict[str, Any], contexts: Dict[str, Dict[Any, Any]]
) -> List[Dict[str, Any]]:
subgraph_id = str(ref.get("subgraph_id") or "")
if not subgraph_id:
return []
exact_key = (subgraph_id, str(ref.get("node_id")), ref.get("widget_index"))
locators = contexts.get("inner_widgets", {}).get(exact_key, [])
if not locators and ref.get("widget_name"):
name_key = (
subgraph_id,
str(ref.get("node_id")),
normalize_widget_name(ref.get("widget_name")),
)
locators = contexts.get("inner_widget_names", {}).get(name_key, [])
return locators
def _promote_model_reference_to_instances(
ref: Dict[str, Any],
contexts: Dict[str, Dict[Any, Any]],
available_models: Optional[List[Dict[str, Any]]] = None,
existing_instance_slots: Optional[set] = None,
) -> Optional[List[Dict[str, Any]]]:
locators = _get_promoted_widget_locators(ref, contexts)
if not locators:
return None
if existing_instance_slots is None:
existing_instance_slots = set()
promoted_refs = []
for locator in locators:
instance_slot = (
str(locator.get("node_id")),
locator.get("proxy_widget_index"),
)
if instance_slot in existing_instance_slots:
continue
if locator.get("input_connected"):
continue
value = locator.get("promoted_value")
nested_key = ref.get("nested_key")
if nested_key and isinstance(value, dict):
value = value.get(nested_key)
if not _has_promoted_value(value):
continue
value_str = str(value).strip()
promoted = dict(ref)
promoted.update(
{
"node_id": locator.get("node_id"),
"node_type": locator.get("node_type", ""),
"node_title": locator.get("node_title", ""),
"widget_index": locator.get("proxy_widget_index"),
"widget_name": locator.get("proxy_widget_name", ""),
"original_path": value_str,
"subgraph_id": locator.get("subgraph_id", ""),
"subgraph_name": locator.get("subgraph_name", ""),
"subgraph_path": None,
"is_top_level": True,
"promoted_widget_name": locator.get("proxy_widget_name", ""),
"promoted_inner_node_id": ref.get("node_id"),
"promoted_inner_node_type": ref.get("node_type", ""),
"promoted_inner_node_title": ref.get("node_title", ""),
"promoted_inner_widget_index": ref.get("widget_index"),
"promoted_inner_widget_name": ref.get("widget_name", ""),
"locate_node_id": locator.get("node_id"),
"locate_node_type": locator.get("node_type", ""),
"locate_node_title": locator.get("node_title", ""),
"locate_subgraph_id": "",
"locate_subgraph_name": locator.get("subgraph_name", ""),
"locate_is_top_level": True,
"locate_via_promoted_widget": True,
"is_urn": bool(references.URN_REGEX.match(value_str)),
}
)
category_hints = promoted.get("category_hints") or []
if not category_hints and promoted.get("category"):
category_hints = [promoted["category"]]
resolved = references.try_resolve_model_path(
value_str,
category_hints or None,
available_models=available_models,
)
if resolved:
category, full_path = resolved
promoted["category"] = category
promoted["full_path"] = full_path
promoted["exists"] = os.path.exists(full_path)
else:
promoted["full_path"] = None
promoted["exists"] = False
promoted_refs.append(promoted)
existing_instance_slots.add(instance_slot)
return promoted_refs
def _apply_promoted_widget_locator(
ref: Dict[str, Any], contexts: Dict[str, Dict[Any, Any]]
) -> None:
locators = _get_promoted_widget_locators(ref, contexts)
locator = _select_promoted_locator(locators, ref.get("original_path", ""))
if not locator:
return
ref["locate_node_id"] = locator.get("node_id")
ref["locate_node_type"] = locator.get("node_type", "")
ref["locate_node_title"] = locator.get("node_title", "")
ref["locate_subgraph_id"] = ""
ref["locate_subgraph_name"] = locator.get("subgraph_name", "")
ref["locate_is_top_level"] = True
ref["locate_via_promoted_widget"] = True