* fix(inspector): pass text through unknown nodes instead of refusing them (#381) The inspector refused any node class missing from the semantics table, so a PromptManagerPair behind an rgthree Any Switch showed "Prompt uses a node the inspector does not support". The inspection resolver (Python and browser) now reads through unknown nodes: a text input is the text, positive/negative route by output slot, a conditioning input is followed alone, otherwise every linked input except core type inputs (pass_through.skip_inputs). PromptManager nodes are authoritative; several different texts become one candidate each ("Several prompts reach the sampler"). A seeded text node with no conditioning input is generated at run time, and a recorded PromptManager execution is not checked against text that passed through an unknown node. INSPECTION_RESOLVER_VERSION goes to 2; ingestion keeps the default profile. A legacy parity gate records every committed graph whose inspection positive matches the legacy parser's; no graph may leave it. * fix(inspector): keep recorded pair text, string-only seed rule, role inputs and unique candidates (#381) Review of the pass-through: a walk through an unknown node never overrides a PromptManagerPair's execution record (any outcome but work-limit is unverifiable while the record is checked); the run-time seed rule reads only string outputs; a node with positive/negative inputs follows only the role of the requested slot, nothing when it is unlinked; one text offered twice is one candidate. * fix(inspector): anchor on the saver that wrote the file and resolve stages on shared links (#381) An inspected file's name now anchors the inspection on the one saver whose filename_prefix (literal or traced) makes the file's stem <prefix>_<digits>[_]; the samplers in that saver's image ancestry are the candidates. No match, several matches or an unreadable walk anchor nothing, so the file stays "several possible prompts". In a latent walk, an earlier sampler reading the same conditioning links as the later one is the same prompt, not an unattributed stage. Python and the browser resolver behave the same; the legacy parity gate states each graph that differs from legacy on purpose. * fix(inspector): skip only plain first stages on shared links; an unfinished prefix scan anchors nothing (#381) Review of the saver anchor: a same-links earlier stage is skipped only when it is not a detailer, sets no prompt override and continues no other sampler, so an earlier prompt or override behind it is never hidden. A work limit while tracing a saver prefix ends the match scan without an anchor instead of counting that saver as no match. * test(inspector): cover every plain-first-stage outcome on both resolvers (#381) The frontend coverage gate found local-resolver-stages.mjs branches at 92.44% (< 93). Each way an earlier stage stops being a plain first stage (no latent, literal, missing or cyclic latent, a sampler-shaped node, a detailer) is now a resolver test and an anchor-first-stage-* parity vector.
217 lines
7.6 KiB
Python
217 lines
7.6 KiB
Python
"""Synthetic graphs for saver anchoring and shared-conditioning stages (#381).
|
|
|
|
``branch_bench`` reproduces the maintainer's ``gpu_unlock_bench`` run: four
|
|
KSampler branches with different prompts, each saved by its own SaveImage whose
|
|
literal prefix names the branch (``bench/run/s45_branch_N``). ``shared_prefix``
|
|
is the dataset run's shape: every branch's saver uses one prefix, so the file
|
|
name cannot name the branch. ``two_stage_same_conditioning`` reproduces the
|
|
ANIMA run: a hires stage whose latent passes an unlisted node and reads the
|
|
same positive and negative links as the first stage. Texts are neutral.
|
|
"""
|
|
|
|
BRANCH_TEXTS = (
|
|
"a knit beanie on a wooden table",
|
|
"an old fishing boat at dawn",
|
|
"a red vintage car in the rain",
|
|
"a cozy library with tall shelves",
|
|
)
|
|
NEGATIVE = "blurry, lowres"
|
|
|
|
|
|
def _encode(text):
|
|
return {"class_type": "CLIPTextEncode", "inputs": {"text": text, "clip": ["1", 1]}}
|
|
|
|
|
|
def _ksampler(positive, negative, latent, denoise=1.0):
|
|
return {
|
|
"class_type": "KSampler",
|
|
"inputs": {
|
|
"seed": 1,
|
|
"steps": 20,
|
|
"cfg": 7.0,
|
|
"sampler_name": "euler",
|
|
"scheduler": "normal",
|
|
"denoise": denoise,
|
|
"model": ["1", 0],
|
|
"positive": positive,
|
|
"negative": negative,
|
|
"latent_image": latent,
|
|
},
|
|
}
|
|
|
|
|
|
def _base():
|
|
return {
|
|
"1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "m"}},
|
|
"2": {"class_type": "EmptyLatentImage", "inputs": {"width": 64}},
|
|
"3": _encode(NEGATIVE),
|
|
}
|
|
|
|
|
|
def branch_bench(prefixes=None, saver="SaveImage"):
|
|
"""Four branches; ``prefixes`` replaces each saver's ``filename_prefix``."""
|
|
prefixes = prefixes or [
|
|
f"bench/run/s45_branch_{index}" for index in range(1, len(BRANCH_TEXTS) + 1)
|
|
]
|
|
graph = _base()
|
|
for index, text in enumerate(BRANCH_TEXTS, start=1):
|
|
base = index * 10
|
|
graph[str(base)] = _encode(text)
|
|
graph[str(base + 1)] = _ksampler([str(base), 0], ["3", 0], ["2", 0])
|
|
graph[str(base + 2)] = {
|
|
"class_type": "VAEDecode",
|
|
"inputs": {"samples": [str(base + 1), 0], "vae": ["1", 2]},
|
|
}
|
|
graph[str(base + 3)] = {
|
|
"class_type": saver,
|
|
"inputs": {
|
|
"filename_prefix": prefixes[index - 1],
|
|
"images": [str(base + 2), 0],
|
|
},
|
|
}
|
|
return graph
|
|
|
|
|
|
def linked_prefix_bench():
|
|
"""The branch run with each prefix coming from a PrimitiveString."""
|
|
graph = branch_bench(prefixes=[["90", 0], ["91", 0], ["92", 0], ["93", 0]])
|
|
for index in range(4):
|
|
graph[str(90 + index)] = {
|
|
"class_type": "PrimitiveString",
|
|
"inputs": {"value": f"bench/run/s45_branch_{index + 1}"},
|
|
}
|
|
return graph
|
|
|
|
|
|
def shared_prefix():
|
|
"""Every branch saved with one prefix: the file name names no branch."""
|
|
return branch_bench(prefixes=["dataset/IMG_GEN"] * len(BRANCH_TEXTS))
|
|
|
|
|
|
def two_stage_same_conditioning(second_positive=None):
|
|
"""Base and hires stage on the same links; the hires latent passes an
|
|
unlisted node (``VendorGpuCleaner``), so the stages' lineage is unknown."""
|
|
graph = _base()
|
|
graph["90"] = _encode(BRANCH_TEXTS[0])
|
|
graph["63"] = _ksampler(["90", 0], ["3", 0], ["2", 0])
|
|
graph["30"] = {
|
|
"class_type": "VAEDecode",
|
|
"inputs": {"samples": ["63", 0], "vae": ["1", 2]},
|
|
}
|
|
graph["98"] = {
|
|
"class_type": "SaveImage",
|
|
"inputs": {"filename_prefix": "draft", "images": ["30", 0]},
|
|
}
|
|
graph["110"] = {"class_type": "VendorGpuCleaner", "inputs": {"anything": ["63", 0]}}
|
|
graph["108"] = {
|
|
"class_type": "LatentUpscaleBy",
|
|
"inputs": {
|
|
"upscale_method": "nearest-exact",
|
|
"scale_by": 1.2,
|
|
"samples": ["110", 0],
|
|
},
|
|
}
|
|
graph["114"] = _ksampler(second_positive or ["90", 0], ["3", 0], ["108", 0], 0.5)
|
|
graph["113"] = {
|
|
"class_type": "VAEDecode",
|
|
"inputs": {"samples": ["114", 0], "vae": ["1", 2]},
|
|
}
|
|
graph["115"] = {
|
|
"class_type": "VendorGpuCleaner",
|
|
"inputs": {"anything": ["113", 0]},
|
|
}
|
|
graph["107"] = {
|
|
"class_type": "SaveImage",
|
|
"inputs": {"filename_prefix": "final", "images": ["115", 0]},
|
|
}
|
|
if second_positive is not None:
|
|
graph["91"] = _encode(BRANCH_TEXTS[1])
|
|
return graph
|
|
|
|
|
|
def hidden_earlier_prompt(override=False):
|
|
"""Review shape: the skipped same-links stage continues a different prompt.
|
|
|
|
10 (cat) -> 11 (dog) -> unlisted LatentBlend -> 13 (dog, same links as 11)
|
|
-> VAEDecode -> SaveImage ``out``. With ``override``, 11 is a
|
|
``KSampler (Efficient)`` with a linked ``script``.
|
|
"""
|
|
graph = _base()
|
|
graph["5"] = _encode("cat X")
|
|
graph["6"] = _encode("dog Y")
|
|
graph["10"] = _ksampler(["5", 0], ["3", 0], ["2", 0])
|
|
graph["11"] = _ksampler(["6", 0], ["3", 0], ["10", 0])
|
|
if override:
|
|
graph["11"] = _ksampler(["6", 0], ["3", 0], ["2", 0])
|
|
graph["11"]["class_type"] = "KSampler (Efficient)"
|
|
graph["11"]["inputs"]["script"] = ["40", 0]
|
|
graph["40"] = {"class_type": "XY Plot", "inputs": {"grid_spacing": 0}}
|
|
graph["12"] = {
|
|
"class_type": "LatentBlend",
|
|
"inputs": {"samples1": ["11", 0], "samples2": ["2", 0], "blend_factor": 0.5},
|
|
}
|
|
graph["13"] = _ksampler(["6", 0], ["3", 0], ["12", 0], 0.5)
|
|
graph["14"] = {
|
|
"class_type": "VAEDecode",
|
|
"inputs": {"samples": ["13", 0], "vae": ["1", 2]},
|
|
}
|
|
graph["15"] = {
|
|
"class_type": "SaveImage",
|
|
"inputs": {"filename_prefix": "out", "images": ["14", 0]},
|
|
}
|
|
return graph
|
|
|
|
|
|
def deep_prefixes(chain):
|
|
"""Review shape: two branches whose savers both trace the prefix ``dup``.
|
|
|
|
Saver A's prefix runs through ``chain`` DisplayText nodes; saver B's
|
|
through one more, so a short visit allowance can finish only one trace.
|
|
"""
|
|
graph = branch_bench(prefixes=[["p0", 0], ["q", 0], "x_1", "x_2"])
|
|
graph["pz"] = {"class_type": "StringConstant", "inputs": {"string": "dup"}}
|
|
previous = "pz"
|
|
for index in range(chain - 1, -1, -1):
|
|
graph[f"p{index}"] = {
|
|
"class_type": "DisplayText",
|
|
"inputs": {"text": [previous, 0]},
|
|
}
|
|
previous = f"p{index}"
|
|
graph["q"] = {"class_type": "DisplayText", "inputs": {"text": ["p0", 0]}}
|
|
return graph
|
|
|
|
|
|
def first_stage_variant(kind):
|
|
"""``two_stage_same_conditioning`` whose first stage (63) is not plain.
|
|
|
|
``no-latent`` drops its latent input (still plain); ``literal-latent``,
|
|
``missing-latent``, ``cyclic-latent``, ``stage-shaped`` and ``detailer``
|
|
each make it something other than a plain first stage, so the later
|
|
stage stays unattributed.
|
|
"""
|
|
graph = two_stage_same_conditioning()
|
|
inputs = graph["63"]["inputs"]
|
|
if kind == "no-latent":
|
|
del inputs["latent_image"]
|
|
elif kind == "literal-latent":
|
|
inputs["latent_image"] = "latent"
|
|
elif kind == "missing-latent":
|
|
inputs["latent_image"] = ["999", 0]
|
|
elif kind == "cyclic-latent":
|
|
graph["70"] = {"class_type": "VendorLatentOp", "inputs": {"samples": ["63", 0]}}
|
|
inputs["latent_image"] = ["70", 0]
|
|
elif kind == "stage-shaped":
|
|
graph["70"] = {
|
|
"class_type": "VendorSampler",
|
|
"inputs": {
|
|
"model": ["1", 0],
|
|
"positive": ["90", 0],
|
|
"latent_image": ["2", 0],
|
|
},
|
|
}
|
|
inputs["latent_image"] = ["70", 0]
|
|
elif kind == "detailer":
|
|
graph["63"]["class_type"] = "FaceDetailer"
|
|
inputs["image"] = ["30", 0]
|
|
return graph
|