Files

199 lines
6.1 KiB
Python

from __future__ import annotations
from collections import deque
from dataclasses import dataclass
from .adapters.samplers import is_sampler
from .graph import GraphIndex
from .models import NodeRecord
OUTPUT_INPUT_FIELDS = ("images", "image")
UPSTREAM_FIELDS = (
"images",
"image",
"samples",
"sample",
"latent",
"latent_image",
"pixels",
)
@dataclass
class SamplerSelection:
sampler: NodeRecord
confidence: float
debug_trace: str
selected_by: str
def select_final_sampler(graph: GraphIndex) -> SamplerSelection:
output_nodes = _output_nodes(graph)
output_chain_diagnostics: list[str] = []
for output_node in output_nodes:
selection, diagnostic = _select_from_output_node(graph, output_node)
if diagnostic:
output_chain_diagnostics.append(diagnostic)
if selection is not None:
return selection
fallback = _fallback_sampler(graph)
if fallback is None:
raise ValueError(_no_sampler_diagnostic(output_nodes, output_chain_diagnostics))
return SamplerSelection(
sampler=fallback,
confidence=0.5,
debug_trace=(
"SAMPLER_SELECTION:\n"
f"sampler selected by fallback heuristic: "
f"{fallback.class_type} {fallback.node_id}; "
"no final SaveImage/PreviewImage output chain was found"
),
selected_by="fallback",
)
def _output_nodes(graph: GraphIndex) -> list[NodeRecord]:
return [node for node in graph.nodes() if _is_output_image_node(node)]
def _is_output_image_node(node: NodeRecord) -> bool:
lowered = node.class_type.lower()
if lowered in {"saveimage", "previewimage", "saveanimatedwebp"}:
return True
has_output_image_field = any(field in node.inputs for field in OUTPUT_INPUT_FIELDS)
return has_output_image_field and (
("save" in lowered and ("image" in lowered or "webp" in lowered))
or ("preview" in lowered and ("image" in lowered or "webp" in lowered))
)
def _select_from_output_node(
graph: GraphIndex,
output_node: NodeRecord,
) -> tuple[SamplerSelection | None, str]:
queue: deque[tuple[NodeRecord, list[str]]] = deque()
for field in OUTPUT_INPUT_FIELDS:
target = graph.link_target(output_node.inputs.get(field))
if target is None:
continue
target_node = graph.get_node(target[0])
if target_node is None:
continue
queue.append(
(
target_node,
[f"{output_node.class_type} {output_node.node_id}.{field}"],
)
)
visited: set[str] = set()
visited_chains: list[tuple[NodeRecord, list[str]]] = []
while queue:
node, chain = queue.popleft()
if node.node_id in visited:
continue
visited.add(node.node_id)
visited_chains.append((node, chain))
if is_sampler(node):
trace_chain = " <- ".join(chain + [f"{node.class_type} {node.node_id}"])
return (
SamplerSelection(
sampler=node,
confidence=1.0,
debug_trace=(
"SAMPLER_SELECTION:\n"
f"{trace_chain}\n"
f"Selected {node.class_type} {node.node_id} because it is "
f"upstream of final {output_node.class_type}."
),
selected_by="output_chain",
),
"",
)
for field in UPSTREAM_FIELDS:
target = graph.link_target(node.inputs.get(field))
if target is None:
continue
target_node = graph.get_node(target[0])
if target_node is None:
continue
queue.append(
(
target_node,
chain + [f"{node.class_type} {node.node_id}.{field}"],
)
)
return None, _format_output_chain_diagnostic(output_node, visited_chains)
def _fallback_sampler(graph: GraphIndex) -> NodeRecord | None:
candidates = [
node
for node in graph.nodes()
if is_sampler(node) and _has_conditioning_inputs(node)
]
if candidates:
return candidates[-1]
samplers = [node for node in graph.nodes() if is_sampler(node)]
if samplers:
return samplers[-1]
return None
def _has_conditioning_inputs(node: NodeRecord) -> bool:
return "positive" in node.inputs or "negative" in node.inputs
def _no_sampler_diagnostic(
output_nodes: list[NodeRecord],
output_chain_diagnostics: list[str],
) -> str:
lines = ["no supported sampler candidate found"]
if output_nodes:
output_summary = ", ".join(
f"{node.class_type} {node.node_id}" for node in output_nodes
)
lines.append(f"final output nodes found: {output_summary}")
else:
lines.append("final output nodes found: none")
if output_chain_diagnostics:
lines.append("final output chain diagnostics:")
lines.extend(output_chain_diagnostics)
return "\n".join(lines)
def _format_output_chain_diagnostic(
output_node: NodeRecord,
visited_chains: list[tuple[NodeRecord, list[str]]],
) -> str:
lines = [f"- {output_node.class_type} {output_node.node_id}:"]
if not visited_chains:
lines.append(" no image/images upstream link found")
return "\n".join(lines)
for node, chain in visited_chains:
trace_chain = " <- ".join(chain + [f"{node.class_type} {node.node_id}"])
input_keys = ", ".join(sorted(node.inputs)) or "none"
lines.append(f" {trace_chain}; input keys: {input_keys}")
if _looks_like_unsupported_generator(node):
lines.append(
f" unsupported terminal generator node: "
f"{node.class_type} {node.node_id}; input keys: {input_keys}"
)
return "\n".join(lines)
def _looks_like_unsupported_generator(node: NodeRecord) -> bool:
lowered = node.class_type.lower().replace("_", " ")
return (
("ultimate" in lowered and "upscale" in lowered)
or "usdu" in lowered
or "generator" in lowered
)