feat(inspect): resolve guider-based samplers
Under INSPECTION_PROFILE the resolver discovers samplers from the semantics table and follows SamplerCustomAdvanced through its guider link (Reroutes allowed) to a listed guider: - CFGGuider: positive and negative, so the pair resolves. - BasicGuider: positive only. The state is "absent" with the reason "Guider has no negative input", the existing missing-negative path. - Any other guider, including DualCFGGuider (D17), is unsupported-node and names that guider. Guider hops draw visits, scan and depth from the existing budgets. A SamplerCustomAdvanced upstream in latent ancestry is an earlier stage, so the result stays multistage-ambiguous. Positive and negative provenance record guider_id. FaceDetailer (detailer-stage) is not a candidate yet: deciding it needs output ancestry. DEFAULT_PROFILE never loads the table and is unchanged. Vector changes (inspection profile only, default vectors unchanged): - f07b: no-sampler -> resolved (CFGGuider). - f17: no-sampler -> unsupported-node (FluxGuidance, Task 2.3). - f01b: no-sampler -> unsupported-node (EmptyFlux2LatentImage latent, Task 2.5). - f01: no-sampler -> ambiguous (24 SamplerCustomAdvanced branches). The no-sampler tests (resolver and inspect route) now use a sampler class the inspector does not know, because f17 has a supported one.
This commit is contained in:
+8
-1
@@ -227,7 +227,14 @@ workflow graph and its facts become limited). The serialized
|
||||
response stays under 8 MiB by omitting, in order, the workflow graph, then the prompt
|
||||
graph, then all raw records atomically. Retry does not raise these limits. Ingestion
|
||||
(scan, live monitor, admin import) and queue-time run counting keep the default
|
||||
profile, including its 500-node gate.
|
||||
profile, including its 500-node gate. Only `INSPECTION_PROFILE` reads the node
|
||||
semantics table `web/js/inspection-semantics.json` (at most 1 MiB and eight nesting
|
||||
levels, loaded on first use). With it, `SamplerCustomAdvanced` follows its `guider`
|
||||
link (through Reroutes) to `CFGGuider` (positive and negative) or `BasicGuider`
|
||||
(positive only; the negative is `absent`, "Guider has no negative input"). Any
|
||||
other guider, such as `DualCFGGuider`, is `unsupported-node`. If the table cannot
|
||||
be loaded, graph inspection is `unresolved` ("Inspection node semantics are
|
||||
unavailable") and ingestion is unaffected, because it never imports the table.
|
||||
|
||||
`tools/inspection_corpus.py --root DIR --out JSONL [--workers N] [--baseline JSONL]
|
||||
[--summary TXT] [--code TREE]` measures a media folder read-only against the #240
|
||||
|
||||
+12
-12
@@ -6,9 +6,9 @@
|
||||
"anchor": null,
|
||||
"expect": {
|
||||
"metadata_status": "available",
|
||||
"state": "unresolved",
|
||||
"reason": "No supported sampler pair found (saw FaceDetailer, SamplerCustomAdvanced)",
|
||||
"reason_code": "no-sampler",
|
||||
"state": "ambiguous",
|
||||
"reason": "Multiple sampler branches",
|
||||
"reason_code": null,
|
||||
"selection": null,
|
||||
"positive": null,
|
||||
"negative": null,
|
||||
@@ -49,8 +49,8 @@
|
||||
"expect": {
|
||||
"metadata_status": "available",
|
||||
"state": "unresolved",
|
||||
"reason": "No supported sampler pair found (saw FaceDetailer, SamplerCustomAdvanced)",
|
||||
"reason_code": "no-sampler",
|
||||
"reason": "Unsupported latent ancestry",
|
||||
"reason_code": "unsupported-node",
|
||||
"selection": null,
|
||||
"positive": null,
|
||||
"negative": null,
|
||||
@@ -510,12 +510,12 @@
|
||||
"anchor": null,
|
||||
"expect": {
|
||||
"metadata_status": "available",
|
||||
"state": "unresolved",
|
||||
"reason": "No supported sampler pair found (saw SamplerCustomAdvanced, CFGGuider)",
|
||||
"reason_code": "no-sampler",
|
||||
"state": "resolved",
|
||||
"reason": "",
|
||||
"reason_code": null,
|
||||
"selection": null,
|
||||
"positive": null,
|
||||
"negative": null,
|
||||
"positive": "a red fox in snow, detailed fur",
|
||||
"negative": "blurry, lowres",
|
||||
"candidate_count": 0,
|
||||
"facts": {
|
||||
"seed": {
|
||||
@@ -889,8 +889,8 @@
|
||||
"expect": {
|
||||
"metadata_status": "available",
|
||||
"state": "unresolved",
|
||||
"reason": "No supported sampler pair found (saw SamplerCustomAdvanced)",
|
||||
"reason_code": "no-sampler",
|
||||
"reason": "Unsupported node semantics or output slot",
|
||||
"reason_code": "unsupported-node",
|
||||
"selection": null,
|
||||
"positive": null,
|
||||
"negative": null,
|
||||
|
||||
@@ -773,8 +773,11 @@ class TestPromptInspector(ImageAPITestCase):
|
||||
self.assertEqual(result["metadata"]["prompt_graph"]["status"], "parsed")
|
||||
|
||||
async def test_inspect_route_reports_no_sampler_instead_of_absent(self):
|
||||
# f17: a BasicGuider-only prompt graph is a record, never "not recorded".
|
||||
self.parity_fixture("f17")
|
||||
# f17 with a sampler class the inspector does not know: a prompt graph
|
||||
# is a record, never "not recorded".
|
||||
built = parity.build("f17")
|
||||
built["prompt"]["13"]["class_type"] = "FutureSampler"
|
||||
parity.write_png(self.output_dir / "f17.png", built)
|
||||
response, result = await self.inspect("f17.png")
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(
|
||||
@@ -785,7 +788,7 @@ class TestPromptInspector(ImageAPITestCase):
|
||||
self.assertEqual(result["resolution"]["reason_code"], "no-sampler")
|
||||
self.assertEqual(
|
||||
result["resolution"]["reason"],
|
||||
"No supported sampler pair found (saw SamplerCustomAdvanced)",
|
||||
"No supported sampler pair found (saw FutureSampler)",
|
||||
)
|
||||
|
||||
async def test_inspect_route_exposes_reason_code_and_blocking_node(self):
|
||||
|
||||
@@ -912,12 +912,15 @@ class TestResolverProfiles(unittest.TestCase):
|
||||
self.assertEqual(result["state"], "resolved")
|
||||
|
||||
def test_prompt_graph_without_supported_sampler_is_unresolved_not_absent(self):
|
||||
metadata = {"prompt": parity.build("f17")["prompt"]}
|
||||
# f17 with its sampler renamed to a class the inspector does not know.
|
||||
source = parity.build("f17")["prompt"]
|
||||
source["13"]["class_type"] = "FutureSampler"
|
||||
metadata = {"prompt": source}
|
||||
result = resolve_prompt_pair(metadata, profile=resolver.INSPECTION_PROFILE)
|
||||
self.assertEqual(
|
||||
(result["state"], result["reason_code"]), ("unresolved", "no-sampler")
|
||||
)
|
||||
self.assertIn("SamplerCustomAdvanced", result["reason"])
|
||||
self.assertIn("FutureSampler", result["reason"])
|
||||
self.assertIsNone(result["positive"])
|
||||
default = resolve_prompt_pair(metadata)
|
||||
self.assertEqual(default["state"], "absent")
|
||||
@@ -1049,6 +1052,203 @@ class TestResolverProfiles(unittest.TestCase):
|
||||
self.assertEqual(result["reason"], "Resolution visit limit reached")
|
||||
|
||||
|
||||
def _guided(guider: dict = None) -> dict:
|
||||
"""``f07b`` (SamplerCustomAdvanced + CFGGuider), optionally another guider."""
|
||||
source = parity.build("f07b")["prompt"]
|
||||
if guider is not None:
|
||||
source["21"] = guider
|
||||
return source
|
||||
|
||||
|
||||
def _basic_guided() -> dict:
|
||||
"""``f17`` with its BasicGuider reading the CLIPTextEncode directly."""
|
||||
source = parity.build("f17")["prompt"]
|
||||
source["22"]["inputs"]["conditioning"] = ["6", 0]
|
||||
del source["26"]
|
||||
return source
|
||||
|
||||
|
||||
def _inspect(source: dict, **options) -> dict:
|
||||
return resolve_prompt_pair(
|
||||
{"prompt": source}, profile=resolver.INSPECTION_PROFILE, **options
|
||||
)
|
||||
|
||||
|
||||
class TestGuiderSamplers(unittest.TestCase):
|
||||
"""SamplerCustomAdvanced follows its guider through the semantics table."""
|
||||
|
||||
def test_sampler_custom_advanced_with_cfg_guider_resolves(self):
|
||||
result = _inspect(_guided())
|
||||
self.assertEqual(result["state"], "resolved")
|
||||
self.assertIsNone(result["reason_code"])
|
||||
self.assertEqual(result["sampler_id"], "3")
|
||||
self.assertEqual(result["positive"]["text"], parity.POS)
|
||||
self.assertEqual(result["negative"]["text"], parity.NEG)
|
||||
provenance = result["positive"]["provenance"]
|
||||
self.assertEqual(
|
||||
(provenance["sampler_id"], provenance["guider_id"]), ("3", "21")
|
||||
)
|
||||
self.assertEqual(provenance["node_id"], "6")
|
||||
self.assertEqual(provenance["path"], [{"node_id": "6", "output_slot": 0}])
|
||||
self.assertEqual(result["negative"]["provenance"]["node_id"], "7")
|
||||
# The saver anchor reaches the same sampler.
|
||||
anchored = _inspect(_guided(), output_node_id="9")
|
||||
self.assertEqual(anchored, result)
|
||||
|
||||
def test_basic_guider_negative_is_absent_by_design(self):
|
||||
from utils.prompt_inspector import inspect_prompt_evidence
|
||||
|
||||
result = _inspect(_basic_guided())
|
||||
self.assertEqual(result["state"], "absent")
|
||||
self.assertEqual(result["reason"], "Guider has no negative input")
|
||||
self.assertIsNone(result["reason_code"])
|
||||
self.assertEqual(result["positive"]["text"], parity.POS)
|
||||
self.assertIsNone(result["negative"])
|
||||
evidence = inspect_prompt_evidence({"prompt": _basic_guided()})
|
||||
self.assertEqual(evidence["prompts"]["positive"]["status"], "resolved")
|
||||
self.assertEqual(evidence["prompts"]["positive"]["text"], parity.POS)
|
||||
self.assertEqual(evidence["prompts"]["negative"]["status"], "absent")
|
||||
self.assertEqual(
|
||||
evidence["resolution"]["reason"], "Guider has no negative input"
|
||||
)
|
||||
|
||||
def test_basic_guider_empty_positive_is_not_absent(self):
|
||||
source = _basic_guided()
|
||||
source["6"]["inputs"]["text"] = " "
|
||||
result = _inspect(source)
|
||||
self.assertEqual(result["state"], "unresolved")
|
||||
self.assertEqual(result["reason"], "Positive prompt is empty")
|
||||
|
||||
def test_dual_cfg_guider_is_unsupported_node(self):
|
||||
dual = {
|
||||
"class_type": "DualCFGGuider",
|
||||
"inputs": {
|
||||
"model": ["4", 0],
|
||||
"cond1": ["6", 0],
|
||||
"cond2": ["6", 0],
|
||||
"negative": ["7", 0],
|
||||
"cfg_conds": 8.0,
|
||||
"cfg_cond2_negative": 8.0,
|
||||
},
|
||||
}
|
||||
result = _inspect(_guided(dual))
|
||||
self.assertEqual(
|
||||
(result["state"], result["reason_code"], result["node"]["class_type"]),
|
||||
("unresolved", "unsupported-node", "DualCFGGuider"),
|
||||
)
|
||||
self.assertEqual(result["node"], {"id": "21", "class_type": "DualCFGGuider"})
|
||||
self.assertEqual(result["sampler_id"], "3")
|
||||
self.assertIsNone(result["positive"])
|
||||
|
||||
def test_unsupported_node_names_first_blocking_node(self):
|
||||
source = _guided()
|
||||
source["30"] = {
|
||||
"class_type": "FutureConditioner",
|
||||
"inputs": {"conditioning": ["6", 0]},
|
||||
}
|
||||
source["31"] = {
|
||||
"class_type": "OtherConditioner",
|
||||
"inputs": {"conditioning": ["7", 0]},
|
||||
}
|
||||
source["21"]["inputs"]["positive"] = ["30", 0]
|
||||
source["21"]["inputs"]["negative"] = ["31", 0]
|
||||
result = _inspect(source)
|
||||
self.assertEqual(result["reason_code"], "unsupported-node")
|
||||
self.assertEqual(
|
||||
result["node"], {"id": "30", "class_type": "FutureConditioner"}
|
||||
)
|
||||
# With the positive fixed, the negative's node is the first blocker.
|
||||
source["21"]["inputs"]["positive"] = ["6", 0]
|
||||
result = _inspect(source)
|
||||
self.assertEqual(result["node"], {"id": "31", "class_type": "OtherConditioner"})
|
||||
self.assertEqual(result["positive"]["text"], parity.POS)
|
||||
|
||||
def test_default_profile_ignores_semantics(self):
|
||||
for source in (_guided(), _basic_guided()):
|
||||
result = resolve_prompt_pair({"prompt": source})
|
||||
self.assertIn(result["state"], ("unresolved", "absent"))
|
||||
self.assertEqual(result["reason"], "No supported sampler pair found")
|
||||
self.assertIsNone(result["reason_code"])
|
||||
self.assertIsNone(result["positive"])
|
||||
# Without semantics the inspection bounds alone do not follow guiders.
|
||||
plain = replace(resolver.INSPECTION_PROFILE, semantics=False)
|
||||
result = resolve_prompt_pair({"prompt": _guided()}, profile=plain)
|
||||
self.assertEqual(result["reason_code"], "no-sampler")
|
||||
|
||||
def test_guider_reached_through_a_reroute(self):
|
||||
source = _guided()
|
||||
source["40"] = {"class_type": "Reroute", "inputs": {"value": ["21", 0]}}
|
||||
source["3"]["inputs"]["guider"] = ["40", 0]
|
||||
result = _inspect(source)
|
||||
self.assertEqual(result["state"], "resolved")
|
||||
self.assertEqual(result["positive"]["provenance"]["guider_id"], "21")
|
||||
|
||||
def test_malformed_guider_links_and_slots_never_guess(self):
|
||||
cases = {
|
||||
"missing": None,
|
||||
"literal": "CFGGuider",
|
||||
"dangling": ["99", 0],
|
||||
"slot": ["21", 1],
|
||||
}
|
||||
for label, value in cases.items():
|
||||
with self.subTest(label=label):
|
||||
source = _guided()
|
||||
if value is None:
|
||||
del source["3"]["inputs"]["guider"]
|
||||
else:
|
||||
source["3"]["inputs"]["guider"] = value
|
||||
result = _inspect(source)
|
||||
self.assertEqual(result["state"], "unresolved")
|
||||
self.assertIsNone(result["positive"])
|
||||
expected = "unsupported-node" if label == "slot" else "malformed"
|
||||
self.assertEqual(result["reason_code"], expected)
|
||||
source = _guided()
|
||||
source["21"]["inputs"]["model"] = ["21", 0]
|
||||
source["40"] = {"class_type": "Reroute", "inputs": {"value": ["40", 0]}}
|
||||
source["3"]["inputs"]["guider"] = ["40", 0]
|
||||
self.assertEqual(_inspect(source)["reason"], "Cyclic guider ancestry")
|
||||
source["40"]["inputs"] = {"value": ["21", 0], "input": ["21", 0]}
|
||||
result = _inspect(source)
|
||||
self.assertEqual(result["reason"], "Reroute input is not uniquely identifiable")
|
||||
self.assertEqual(result["reason_code"], "malformed")
|
||||
|
||||
def test_guider_hops_draw_from_the_work_budgets(self):
|
||||
self.assertEqual(_inspect(_guided(), max_visits=2)["reason_code"], "work-limit")
|
||||
self.assertEqual(_inspect(_guided())["state"], "resolved")
|
||||
source = _guided()
|
||||
previous = "21"
|
||||
for index in range(70):
|
||||
source[f"r{index}"] = {
|
||||
"class_type": "Reroute",
|
||||
"inputs": {"value": [previous, 0]},
|
||||
}
|
||||
previous = f"r{index}"
|
||||
source["3"]["inputs"]["guider"] = [previous, 0]
|
||||
self.assertEqual(_inspect(source)["reason_code"], "depth-limit")
|
||||
|
||||
def test_custom_advanced_upstream_of_a_sampler_is_multistage(self):
|
||||
source = _guided()
|
||||
source["41"] = {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"positive": ["6", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["3", 0],
|
||||
},
|
||||
}
|
||||
source["8"]["inputs"]["samples"] = ["41", 0]
|
||||
result = _inspect(source, output_node_id="9")
|
||||
self.assertEqual(result["sampler_id"], "41")
|
||||
self.assertEqual(result["state"], "unresolved")
|
||||
self.assertEqual(result["reason"], "Multistage sampler ancestry is ambiguous")
|
||||
unanchored = _inspect(source)
|
||||
self.assertEqual(unanchored["state"], "ambiguous")
|
||||
self.assertEqual(
|
||||
sorted(item["sampler_id"] for item in unanchored["candidates"]),
|
||||
["3", "41"],
|
||||
)
|
||||
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
SEMANTICS_UNAVAILABLE = "Inspection node semantics are unavailable"
|
||||
|
||||
|
||||
+113
-24
@@ -132,6 +132,17 @@ INSPECTION_PROFILE = ResolverProfile(
|
||||
# About four passes over the 2 MiB role-record budget.
|
||||
max_scanned_units=8_000_000,
|
||||
)
|
||||
# Without the semantics table every sampler reads its pair directly.
|
||||
_DIRECT_SAMPLER = {
|
||||
"strategy": "ksampler",
|
||||
"conditioning": {"positive": "positive", "negative": "negative"},
|
||||
"latent": "latent_image",
|
||||
}
|
||||
# Strategies whose nodes are prompt candidates in this slice. A detailer stage
|
||||
# re-samples an upstream image; it is judged together with its upstream chain,
|
||||
# which needs output ancestry (later slice), so it is never a candidate alone.
|
||||
_CANDIDATE_STRATEGIES = frozenset({"ksampler", "custom-advanced"})
|
||||
_GUIDER_ABSENT_REASON = "Guider has no negative input"
|
||||
_SAMPLER_LIKE_INPUTS = ("latent_image", "guider", "positive", "negative")
|
||||
_NO_SAMPLER_NAMES = 3
|
||||
_CLASS_NAME_LENGTH = 64
|
||||
@@ -239,6 +250,7 @@ class _GraphResolver:
|
||||
*,
|
||||
budget: Optional[_Budget] = None,
|
||||
profile: ResolverProfile = DEFAULT_PROFILE,
|
||||
semantics: Optional[Any] = None,
|
||||
) -> None:
|
||||
# ``max_visits`` is the per-candidate allowance; ``profile`` supplies only
|
||||
# the path selection and whole-resolution bounds. ``resolve_text_input``
|
||||
@@ -268,6 +280,18 @@ class _GraphResolver:
|
||||
self.active: set = set()
|
||||
self.metadata = metadata or {}
|
||||
self.text_sources: dict = {}
|
||||
# ``semantics`` (inspection) names the samplers and guiders it follows.
|
||||
self.semantics = semantics
|
||||
if semantics is None:
|
||||
self.sampler_classes = self.stage_classes = _SAMPLERS
|
||||
else:
|
||||
self.sampler_classes = frozenset(
|
||||
name
|
||||
for name, entry in semantics.samplers.items()
|
||||
if entry["strategy"] in _CANDIDATE_STRATEGIES
|
||||
)
|
||||
# Any listed sampler upstream in latent ancestry is an earlier stage.
|
||||
self.stage_classes = frozenset(semantics.samplers)
|
||||
|
||||
def node(self, node_id: str) -> Tuple[str, dict]:
|
||||
if self.legacy:
|
||||
@@ -511,22 +535,73 @@ class _GraphResolver:
|
||||
)
|
||||
return record
|
||||
|
||||
def side(self, value: Any, sampler_id: str) -> dict:
|
||||
text, path, storage_text = self.text(value, "conditioning")
|
||||
def side(
|
||||
self,
|
||||
value: Any,
|
||||
sampler_id: str,
|
||||
guider_id: Optional[str] = None,
|
||||
depth: int = 0,
|
||||
) -> dict:
|
||||
text, path, storage_text = self.text(value, "conditioning", depth)
|
||||
origin = path[0]
|
||||
return {
|
||||
"text": text,
|
||||
"storage_text": storage_text,
|
||||
"provenance": {
|
||||
"format": "comfyui",
|
||||
"sampler_id": sampler_id,
|
||||
**origin,
|
||||
"path": path,
|
||||
"text_source": self.text_sources.get(
|
||||
(origin["node_id"], origin["output_slot"]), "graph-inferred"
|
||||
),
|
||||
},
|
||||
provenance = {
|
||||
"format": "comfyui",
|
||||
"sampler_id": sampler_id,
|
||||
**origin,
|
||||
"path": path,
|
||||
"text_source": self.text_sources.get(
|
||||
(origin["node_id"], origin["output_slot"]), "graph-inferred"
|
||||
),
|
||||
}
|
||||
if guider_id is not None:
|
||||
# The conditioning path starts below the guider the sampler reads.
|
||||
provenance["guider_id"] = guider_id
|
||||
return {"text": text, "storage_text": storage_text, "provenance": provenance}
|
||||
|
||||
def guider(self, value: Any) -> Tuple[str, dict, Mapping, str, int]:
|
||||
"""Follow a sampler's ``guider`` link (through Reroutes) to a listed guider.
|
||||
|
||||
Returns the guider's ID and inputs, its conditioning input names, the
|
||||
reason used when it has no negative input, and the depth reached. Each
|
||||
hop draws a visit; an unlisted guider class is ``unsupported-node``.
|
||||
"""
|
||||
seen: set = set()
|
||||
depth = 0
|
||||
while True:
|
||||
if depth >= self.max_depth:
|
||||
raise _Unresolved("Resolution depth limit reached", "depth-limit")
|
||||
if not _link(value):
|
||||
raise _Unresolved("Expected a guider link", "malformed")
|
||||
node_id, slot = str(value[0]), value[1]
|
||||
if node_id in seen:
|
||||
raise _Unresolved("Cyclic guider ancestry", "malformed")
|
||||
seen.add(node_id)
|
||||
self.budget.spend()
|
||||
name, inputs = self.node(node_id)
|
||||
self.scan(len(inputs))
|
||||
depth += 1
|
||||
if name == "Reroute" and slot == 0:
|
||||
keys = [key for key in ("value", "input", "in") if key in inputs]
|
||||
if len(keys) != 1:
|
||||
raise _Unresolved(
|
||||
"Reroute input is not uniquely identifiable", "malformed"
|
||||
)
|
||||
value = inputs[keys[0]]
|
||||
continue
|
||||
entry = self.semantics.guiders.get(name)
|
||||
if entry is None or slot != 0:
|
||||
raise self.unsupported("Unsupported guider", node_id)
|
||||
conditioning = {
|
||||
"positive": entry["positive"],
|
||||
"negative": entry.get("negative"),
|
||||
}
|
||||
reason = entry.get("absent_reason", _GUIDER_ABSENT_REASON)
|
||||
return node_id, inputs, conditioning, reason, depth
|
||||
|
||||
def sampler_entry(self, name: str) -> Mapping:
|
||||
if self.semantics is None:
|
||||
return _DIRECT_SAMPLER
|
||||
return self.semantics.samplers[name]
|
||||
|
||||
def pair(self, sampler_id: str) -> dict:
|
||||
positive = negative = None
|
||||
@@ -539,22 +614,35 @@ class _GraphResolver:
|
||||
reason_code="work-limit",
|
||||
)
|
||||
try:
|
||||
_, inputs = self.node(sampler_id)
|
||||
name, inputs = self.node(sampler_id)
|
||||
entry = self.sampler_entry(name)
|
||||
# A second sampler in the latent ancestry denotes a multistage run;
|
||||
# associating the final image with one prompt pair would be a guess.
|
||||
if "latent_image" in inputs:
|
||||
self.latent_source(inputs["latent_image"], set(), 0)
|
||||
positive = self.side(inputs.get("positive"), sampler_id)
|
||||
if entry["latent"] in inputs:
|
||||
self.latent_source(inputs[entry["latent"]], set(), 0)
|
||||
guider_id, depth = None, 0
|
||||
conditioning = entry["conditioning"]
|
||||
absent_reason = "Negative input is absent"
|
||||
if entry["strategy"] == "custom-advanced":
|
||||
guider_id, inputs, conditioning, guider_reason, depth = self.guider(
|
||||
inputs.get(conditioning["guider"])
|
||||
)
|
||||
if conditioning["negative"] is None:
|
||||
absent_reason = guider_reason
|
||||
positive = self.side(
|
||||
inputs.get(conditioning["positive"]), sampler_id, guider_id, depth
|
||||
)
|
||||
if not positive["text"].strip():
|
||||
raise _Unresolved("Positive prompt is empty")
|
||||
if "negative" not in inputs:
|
||||
negative_name = conditioning["negative"]
|
||||
if negative_name is None or negative_name not in inputs:
|
||||
return _result(
|
||||
"absent",
|
||||
positive,
|
||||
sampler_id=sampler_id,
|
||||
reason="Negative input is absent",
|
||||
reason=absent_reason,
|
||||
)
|
||||
negative = self.side(inputs["negative"], sampler_id)
|
||||
negative = self.side(inputs[negative_name], sampler_id, guider_id, depth)
|
||||
return _result("resolved", positive, negative, sampler_id)
|
||||
except _Unresolved as error:
|
||||
return _result(
|
||||
@@ -581,7 +669,7 @@ class _GraphResolver:
|
||||
self.budget.spend()
|
||||
seen.add(node_id)
|
||||
name, inputs = self.node(node_id)
|
||||
if name in _SAMPLERS:
|
||||
if name in self.stage_classes:
|
||||
# ``multistage-differs`` is reserved for when stage texts are compared.
|
||||
raise _Unresolved("Multistage sampler ancestry is ambiguous")
|
||||
if name in ("EmptyLatentImage", "EmptySD3LatentImage") and slot == 0:
|
||||
@@ -603,7 +691,7 @@ class _GraphResolver:
|
||||
self.budget.spend()
|
||||
seen.add(node_id)
|
||||
name, inputs = self.node(node_id)
|
||||
if name in _SAMPLERS and slot == 0:
|
||||
if name in self.sampler_classes and slot == 0:
|
||||
return node_id
|
||||
key = {
|
||||
"SaveImage": "images",
|
||||
@@ -811,6 +899,7 @@ def _resolve(
|
||||
metadata if "prompt" in metadata else None,
|
||||
budget=_Budget(profile.total_visits),
|
||||
profile=profile,
|
||||
semantics=semantics,
|
||||
)
|
||||
if output_node_id is not None:
|
||||
try:
|
||||
@@ -825,7 +914,7 @@ def _resolve(
|
||||
for node_id, node in normalized.items()
|
||||
if isinstance(node, dict)
|
||||
and isinstance(node.get("class_type"), str)
|
||||
and node["class_type"] in _SAMPLERS
|
||||
and node["class_type"] in resolver.sampler_classes
|
||||
]
|
||||
if not sampler_ids:
|
||||
return _no_sampler(normalized, profile)
|
||||
|
||||
Reference in New Issue
Block a user