* 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.
1468 lines
46 KiB
Python
1468 lines
46 KiB
Python
"""Record sets for the local resolver parity test (#249 local Inspect, PR L3 Task 8).
|
||
|
||
Every case is a dict of record texts as ``read_inspection_file`` returns them
|
||
(``prompt``, ``workflow``, ``comfyassets_prompt_pairs``), so the server's
|
||
``inspect_prompt_evidence`` and the browser's ``resolveLocalEvidence`` read the
|
||
same bytes. ``golden_cases`` names one case per traced strategy, bound and
|
||
malformed shape; ``fuzz_cases`` builds coherent single-sampler graphs from the
|
||
same node vocabulary, then corrupts some of them. All content is synthetic.
|
||
``single_chain`` says, from the server's own sampler table, whether a record
|
||
set resolves without candidate selection (the Task 8 subset);
|
||
``local_resolver_selection_cases`` holds the candidate-selection cases.
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import random
|
||
from pathlib import Path
|
||
|
||
from utils.inspection_semantics import get_semantics
|
||
from utils.prompt_inspector import inspect_prompt_evidence
|
||
from utils.prompt_pair_resolver import INSPECTION_PROFILE
|
||
|
||
VECTORS_PATH = (
|
||
Path(__file__).resolve().parent
|
||
/ "fixtures"
|
||
/ "local-inspect"
|
||
/ "resolver-vectors.json"
|
||
)
|
||
# A case may name the inspected file (#381 saver anchoring); it is no record.
|
||
FILE_NAME = "__file_name"
|
||
# Committed record sets stay small; the large bound cases are built at test time.
|
||
MAX_VECTOR_CHARS = 20_000
|
||
COMMITTED_FUZZ = (0, 60)
|
||
COMMITTED_PASSTHROUGH = (0, 40)
|
||
|
||
SPACES = ["", " ", "\t", "\n", "\x1c", "\x85", " ", "", "
"]
|
||
WORDS = ["fox", "snow", "café", "猫", "\U0001f600", "a,", "detailed fur", ""]
|
||
|
||
|
||
def _dump(graph, ensure_ascii=False):
|
||
return json.dumps(graph, ensure_ascii=ensure_ascii)
|
||
|
||
|
||
def _encode(text, name="CLIPTextEncode"):
|
||
return {"class_type": name, "inputs": {"text": text, "clip": ["4", 1]}}
|
||
|
||
|
||
def _graph(positive, negative, extra=None, sampler=None):
|
||
graph = {
|
||
"3": sampler
|
||
or {
|
||
"class_type": "KSampler",
|
||
"inputs": {
|
||
"seed": 1,
|
||
"model": ["4", 0],
|
||
"positive": positive,
|
||
"negative": negative,
|
||
"latent_image": ["5", 0],
|
||
},
|
||
},
|
||
"4": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "m"}},
|
||
"5": {"class_type": "EmptyLatentImage", "inputs": {"width": 64}},
|
||
}
|
||
graph.update(extra or {})
|
||
return graph
|
||
|
||
|
||
def _pair(text="fox", negative="blur", prepend="", append=""):
|
||
return {
|
||
"class_type": "PromptManagerPair",
|
||
"inputs": {
|
||
"text": text,
|
||
"negative_text": negative,
|
||
"prepend_text": prepend,
|
||
"append_text": append,
|
||
},
|
||
}
|
||
|
||
|
||
def _execution(node_id, **changes):
|
||
record = {
|
||
"text": "fox",
|
||
"negative_text": "blur",
|
||
"positive_text": "fox executed",
|
||
"negative_effective_text": "blur",
|
||
"prepend_text": "",
|
||
"append_text": "",
|
||
}
|
||
record.update(changes)
|
||
return json.dumps({"version": 1, "nodes": {node_id: {"records": [record]}}})
|
||
|
||
|
||
def _chain(length, start="6"):
|
||
nodes = {start: _encode("deep")}
|
||
previous = start
|
||
for index in range(length):
|
||
node_id = f"r{index}"
|
||
nodes[node_id] = {"class_type": "Reroute", "inputs": {"value": [previous, 0]}}
|
||
previous = node_id
|
||
return nodes, previous
|
||
|
||
|
||
def _combine_tree(levels):
|
||
"""A full ConditioningCombine tree over equal encoders: many distinct visits."""
|
||
nodes, layer = {}, []
|
||
for index in range(2**levels):
|
||
nodes[f"e{index}"] = _encode("same")
|
||
layer.append(f"e{index}")
|
||
level = 0
|
||
while len(layer) > 1:
|
||
nxt = []
|
||
for index in range(0, len(layer), 2):
|
||
node_id = f"c{level}_{index}"
|
||
nodes[node_id] = {
|
||
"class_type": "ConditioningCombine",
|
||
"inputs": {
|
||
"conditioning_1": [layer[index], 0],
|
||
"conditioning_2": [layer[index + 1], 0],
|
||
},
|
||
}
|
||
nxt.append(node_id)
|
||
layer, level = nxt, level + 1
|
||
return nodes, layer[0]
|
||
|
||
|
||
def golden_cases():
|
||
"""Named record sets, one per strategy, bound and malformed shape."""
|
||
cases = {}
|
||
|
||
def add(name, graph=None, prompt=None, **records):
|
||
cases[name] = {"prompt": prompt if prompt is not None else _dump(graph)}
|
||
cases[name].update(records)
|
||
|
||
neg = ["7", 0]
|
||
base = {"6": _encode("a fox"), "7": _encode("blurry")}
|
||
add("encode", _graph(["6", 0], neg, base))
|
||
add("empty-positive", _graph(["6", 0], neg, {**base, "6": _encode(" \x85")}))
|
||
add("feff-positive", _graph(["6", 0], neg, {**base, "6": _encode("")}))
|
||
add("no-negative", _graph(["6", 0], None, base))
|
||
add(
|
||
"negative-missing-input",
|
||
None,
|
||
prompt=_dump(_graph(["6", 0], neg, base)).replace('"negative": ["7", 0], ', ""),
|
||
)
|
||
add(
|
||
"float-slot",
|
||
None,
|
||
prompt=_dump(_graph(["6", 0], neg, base)).replace('["6", 0]', '["6", 0.0]'),
|
||
)
|
||
add(
|
||
"bool-slot",
|
||
None,
|
||
prompt=_dump(_graph(["6", 0], neg, base)).replace('["6", 0]', '["6", true]'),
|
||
)
|
||
add("int-id", None, prompt=_dump(_graph([6, 0], neg, base)))
|
||
add("dangling", _graph(["60", 0], neg, base))
|
||
add("wrong-slot", _graph(["6", 3], neg, base))
|
||
add(
|
||
"cycle",
|
||
_graph(
|
||
["8", 0],
|
||
neg,
|
||
{**base, "8": {"class_type": "Reroute", "inputs": {"value": ["8", 0]}}},
|
||
),
|
||
)
|
||
add(
|
||
"reroute-ambiguous",
|
||
_graph(
|
||
["8", 0],
|
||
neg,
|
||
{
|
||
**base,
|
||
"8": {
|
||
"class_type": "Reroute",
|
||
"inputs": {"value": ["6", 0], "in": ["6", 0]},
|
||
},
|
||
},
|
||
),
|
||
)
|
||
add(
|
||
"duplicate-key",
|
||
None,
|
||
prompt=_dump(_graph(["6", 0], neg, base))[:-1]
|
||
+ ', "6": '
|
||
+ _dump(_encode("last wins"))
|
||
+ "}",
|
||
)
|
||
add(
|
||
"nan-input",
|
||
None,
|
||
prompt=_dump(_graph(["6", 0], neg, base)).replace('"seed": 1', '"seed": NaN'),
|
||
)
|
||
add(
|
||
"class-type-number",
|
||
_graph(
|
||
["6", 0], neg, {**base, "6": {"class_type": 1.5, "inputs": {"text": "x"}}}
|
||
),
|
||
)
|
||
add(
|
||
"class-type-missing",
|
||
_graph(["6", 0], neg, {**base, "6": {"inputs": {"text": "x"}}}),
|
||
)
|
||
add(
|
||
"inputs-list",
|
||
_graph(
|
||
["6", 0],
|
||
neg,
|
||
{**base, "6": {"class_type": "CLIPTextEncode", "inputs": ["x"]}},
|
||
),
|
||
)
|
||
add("prompt-list", None, prompt="[1, 2]")
|
||
add("prompt-empty", None, prompt="{}")
|
||
add("prompt-bom", None, prompt="" + _dump(_graph(["6", 0], neg, base)))
|
||
add(
|
||
"huge-int",
|
||
None,
|
||
prompt=_dump(_graph(["6", 0], neg, base)).replace(
|
||
'"seed": 1', '"seed": ' + "9" * 4301
|
||
),
|
||
)
|
||
add("only-execution", None, prompt=None)
|
||
cases["only-execution"] = {"comfyassets_prompt_pairs": _execution("8")}
|
||
add(
|
||
"no-sampler",
|
||
{
|
||
"1": _encode("x"),
|
||
"2": {"class_type": "XSampler", "inputs": {"positive": ["1", 0]}},
|
||
"9": {"class_type": "YGuider", "inputs": {"guider": ["1", 0]}},
|
||
},
|
||
)
|
||
add("no-sampler-plain", {"1": _encode("x")})
|
||
sdxl = {
|
||
"class_type": "CLIPTextEncodeSDXL",
|
||
"inputs": {"text_g": "a", "text_l": "a"},
|
||
}
|
||
add("sdxl-equal", _graph(["8", 0], neg, {**base, "8": sdxl}))
|
||
add(
|
||
"sdxl-one",
|
||
_graph(
|
||
["8", 0],
|
||
neg,
|
||
{
|
||
**base,
|
||
"8": {
|
||
"class_type": "CLIPTextEncodeSDXL",
|
||
"inputs": {"text_g": "a", "text_l": ""},
|
||
},
|
||
},
|
||
),
|
||
)
|
||
add(
|
||
"sdxl-differ",
|
||
_graph(
|
||
["8", 0],
|
||
neg,
|
||
{
|
||
**base,
|
||
"8": {
|
||
"class_type": "CLIPTextEncodeSDXL",
|
||
"inputs": {"text_g": "a", "text_l": "b"},
|
||
},
|
||
},
|
||
),
|
||
)
|
||
for flag in (False, True, 1, ["9", 0]):
|
||
smz = {
|
||
"class_type": "smZ CLIPTextEncode",
|
||
"inputs": {"text": "t", "text_g": "g", "text_l": "g", "with_SDXL": flag},
|
||
}
|
||
add(f"smz-{json.dumps(flag)}", _graph(["8", 0], neg, {**base, "8": smz}))
|
||
add(
|
||
"zeroed",
|
||
_graph(
|
||
["6", 0],
|
||
["8", 0],
|
||
{
|
||
**base,
|
||
"8": {
|
||
"class_type": "ConditioningZeroOut",
|
||
"inputs": {"conditioning": ["6", 0]},
|
||
},
|
||
},
|
||
),
|
||
)
|
||
add(
|
||
"passthrough",
|
||
_graph(
|
||
["8", 0],
|
||
neg,
|
||
{
|
||
**base,
|
||
"8": {
|
||
"class_type": "FluxGuidance",
|
||
"inputs": {"conditioning": ["6", 0], "guidance": 3.5},
|
||
},
|
||
},
|
||
),
|
||
)
|
||
add(
|
||
"controlnet-slot1",
|
||
_graph(
|
||
["8", 0],
|
||
["8", 1],
|
||
{
|
||
**base,
|
||
"8": {
|
||
"class_type": "ControlNetApplyAdvanced",
|
||
"inputs": {"positive": ["6", 0], "negative": ["7", 0]},
|
||
},
|
||
},
|
||
),
|
||
)
|
||
add(
|
||
"combine-equal",
|
||
_graph(
|
||
["8", 0],
|
||
neg,
|
||
{
|
||
**base,
|
||
"9": _encode("a fox"),
|
||
"8": {
|
||
"class_type": "ConditioningCombine",
|
||
"inputs": {"conditioning_1": ["6", 0], "conditioning_2": ["9", 0]},
|
||
},
|
||
},
|
||
),
|
||
)
|
||
add(
|
||
"combine-differ",
|
||
_graph(
|
||
["8", 0],
|
||
neg,
|
||
{
|
||
**base,
|
||
"8": {
|
||
"class_type": "ConditioningCombine",
|
||
"inputs": {"conditioning_1": ["6", 0], "conditioning_2": ["7", 0]},
|
||
},
|
||
},
|
||
),
|
||
)
|
||
for condition in (True, False, ["9", 0], 1, "true"):
|
||
select = {
|
||
"class_type": "easy ifElse",
|
||
"inputs": {"boolean": condition, "on_true": ["6", 0], "on_false": ["7", 0]},
|
||
}
|
||
add(
|
||
f"select-{json.dumps(condition)}",
|
||
_graph(
|
||
["8", 0],
|
||
["10", 0],
|
||
{
|
||
**base,
|
||
"8": select,
|
||
"10": _encode("neg"),
|
||
"9": {"class_type": "PrimitiveBoolean", "inputs": {"value": True}},
|
||
},
|
||
),
|
||
)
|
||
same = {
|
||
"class_type": "easy ifElse",
|
||
"inputs": {"boolean": ["9", 0], "on_true": ["6", 0], "on_false": ["11", 0]},
|
||
}
|
||
add(
|
||
"select-converge",
|
||
_graph(["8", 0], neg, {**base, "8": same, "11": _encode("a fox")}),
|
||
)
|
||
# Two selectors on one condition: forced together, so two candidates, not four.
|
||
add(
|
||
"select-shared",
|
||
_graph(
|
||
["8", 0],
|
||
["12", 0],
|
||
{
|
||
**base,
|
||
"8": {
|
||
"class_type": "easy ifElse",
|
||
"inputs": {
|
||
"boolean": ["9", 0],
|
||
"on_true": ["6", 0],
|
||
"on_false": ["7", 0],
|
||
},
|
||
},
|
||
"12": {
|
||
"class_type": "Switch conditioning [Crystools]",
|
||
"inputs": {
|
||
"boolean": ["9", 0],
|
||
"on_true": ["7", 0],
|
||
"on_false": ["6", 0],
|
||
},
|
||
},
|
||
},
|
||
),
|
||
)
|
||
add(
|
||
"runtime",
|
||
_graph(
|
||
["8", 0],
|
||
neg,
|
||
{**base, "8": {"class_type": "PromptSearchList", "inputs": {}}},
|
||
),
|
||
)
|
||
add(
|
||
"unknown-encoder",
|
||
_graph(
|
||
["8", 0],
|
||
neg,
|
||
{
|
||
**base,
|
||
"8": {"class_type": "FancyEncoder" + "x" * 80, "inputs": {"text": "x"}},
|
||
},
|
||
),
|
||
)
|
||
add(
|
||
"primitive",
|
||
_graph(
|
||
["6", 0],
|
||
neg,
|
||
{
|
||
**base,
|
||
"6": _encode(["8", 0]),
|
||
"8": {
|
||
"class_type": "PrimitiveStringMultiline",
|
||
"inputs": {"value": " multi\nline "},
|
||
},
|
||
},
|
||
),
|
||
)
|
||
add(
|
||
"string-kind-mismatch",
|
||
_graph(
|
||
["8", 0],
|
||
neg,
|
||
{**base, "8": {"class_type": "PrimitiveString", "inputs": {"value": "x"}}},
|
||
),
|
||
)
|
||
concat = {
|
||
"class_type": "StringConcatenate",
|
||
"inputs": {"string_a": " a ", "string_b": "", "delimiter": ", "},
|
||
}
|
||
add(
|
||
"string-concatenate",
|
||
_graph(["6", 0], neg, {**base, "6": _encode(["8", 0]), "8": concat}),
|
||
)
|
||
for clean in ("true", "False", True, "TRUE", 0):
|
||
text_concat = {
|
||
"class_type": "Text Concatenate",
|
||
"inputs": {
|
||
"text_b": " b ",
|
||
"text_a": " a",
|
||
"text_aa": "x",
|
||
"text_c": "",
|
||
"clean_whitespace": clean,
|
||
},
|
||
}
|
||
add(
|
||
f"text-concatenate-{json.dumps(clean)}",
|
||
_graph(["6", 0], neg, {**base, "6": _encode(["8", 0]), "8": text_concat}),
|
||
)
|
||
for strip in (True, False, 1):
|
||
multiline = {
|
||
"class_type": "StringConstantMultiline",
|
||
"inputs": {"string": "\n a\nb \n", "strip_newlines": strip},
|
||
}
|
||
add(
|
||
f"strip-newlines-{json.dumps(strip)}",
|
||
_graph(["6", 0], neg, {**base, "6": _encode(["8", 0]), "8": multiline}),
|
||
)
|
||
for count, as_list in (
|
||
(3, False),
|
||
(1, False),
|
||
(0, False),
|
||
(1001, False),
|
||
(2, True),
|
||
(2.0, False),
|
||
(True, False),
|
||
):
|
||
join = {
|
||
"class_type": "JoinStringMulti",
|
||
"inputs": {
|
||
"string_1": "",
|
||
"string_2": "b",
|
||
"string_3": "",
|
||
"string_4": "d",
|
||
"inputcount": count,
|
||
"delimiter": " | ",
|
||
"return_list": as_list,
|
||
},
|
||
}
|
||
add(
|
||
f"join-multi-{json.dumps(count)}-{json.dumps(as_list)}",
|
||
_graph(["6", 0], neg, {**base, "6": _encode(["8", 0]), "8": join}),
|
||
)
|
||
manager = {
|
||
"class_type": "PromptManager",
|
||
"inputs": {
|
||
"text": "main",
|
||
"prepend_text": " before ",
|
||
"append_text": "",
|
||
"clip": ["4", 1],
|
||
},
|
||
}
|
||
add("prompt-manager", _graph(["8", 0], neg, {**base, "8": manager}))
|
||
add(
|
||
"prompt-manager-null-prepend",
|
||
_graph(
|
||
["8", 0],
|
||
neg,
|
||
{
|
||
**base,
|
||
"8": {**manager, "inputs": {**manager["inputs"], "prepend_text": None}},
|
||
},
|
||
),
|
||
)
|
||
for name, slot in (
|
||
("PromptManagerV2", 0),
|
||
("PromptManagerV2", 1),
|
||
("PromptManagerPositive", 0),
|
||
("PromptManagerNegative", 0),
|
||
):
|
||
historical = {
|
||
"class_type": name,
|
||
"inputs": {
|
||
"text": " main ",
|
||
"negative_text": " neg ",
|
||
"prepend_text": "lead,",
|
||
"append_text": None,
|
||
},
|
||
}
|
||
add(
|
||
f"historical-{name}-{slot}",
|
||
_graph(["8", slot], neg, {**base, "8": historical}),
|
||
)
|
||
pair_graph = _graph(["8", 0], ["8", 1], {**base, "8": _pair()})
|
||
add("pair-graph", pair_graph)
|
||
add("pair-execution", pair_graph, comfyassets_prompt_pairs=_execution("8"))
|
||
add(
|
||
"pair-execution-other-node",
|
||
pair_graph,
|
||
comfyassets_prompt_pairs=_execution("99"),
|
||
)
|
||
add(
|
||
"pair-execution-mismatch",
|
||
pair_graph,
|
||
comfyassets_prompt_pairs=_execution("8", text="other"),
|
||
)
|
||
add(
|
||
"pair-execution-negative-changed",
|
||
pair_graph,
|
||
comfyassets_prompt_pairs=_execution("8", negative_effective_text="x"),
|
||
)
|
||
add(
|
||
"pair-execution-version-true",
|
||
pair_graph,
|
||
comfyassets_prompt_pairs=_execution("8").replace(
|
||
'"version": 1', '"version": true'
|
||
),
|
||
)
|
||
add(
|
||
"pair-execution-version-float",
|
||
pair_graph,
|
||
comfyassets_prompt_pairs=_execution("8").replace(
|
||
'"version": 1', '"version": 1.0'
|
||
),
|
||
)
|
||
add(
|
||
"pair-execution-truncated",
|
||
pair_graph,
|
||
comfyassets_prompt_pairs=_execution("8").replace(
|
||
'"version": 1', '"version": 1, "truncated": []'
|
||
),
|
||
)
|
||
add("pair-execution-bad-json", pair_graph, comfyassets_prompt_pairs="{nope")
|
||
two = json.loads(_execution("8"))
|
||
two["nodes"]["8"]["records"].append(
|
||
dict(two["nodes"]["8"]["records"][0], positive_text="other")
|
||
)
|
||
add(
|
||
"pair-execution-two-records",
|
||
pair_graph,
|
||
comfyassets_prompt_pairs=json.dumps(two),
|
||
)
|
||
linked_pair = _graph(
|
||
["8", 0],
|
||
["8", 1],
|
||
{
|
||
**base,
|
||
"8": {**_pair(), "inputs": {**_pair()["inputs"], "text": ["9", 0]}},
|
||
"9": {"class_type": "PromptSearchList", "inputs": {}},
|
||
},
|
||
)
|
||
add(
|
||
"pair-execution-runtime-input",
|
||
linked_pair,
|
||
comfyassets_prompt_pairs=_execution("8"),
|
||
)
|
||
add(
|
||
"pair-execution-runtime-wrong-slot",
|
||
_graph(
|
||
["8", 0],
|
||
["8", 1],
|
||
{
|
||
**base,
|
||
"8": {**_pair(), "inputs": {**_pair()["inputs"], "text": ["9", 2]}},
|
||
"9": {"class_type": "PromptManagerText", "inputs": {"text": "x"}},
|
||
},
|
||
),
|
||
comfyassets_prompt_pairs=_execution("8"),
|
||
)
|
||
guided = {
|
||
"class_type": "SamplerCustomAdvanced",
|
||
"inputs": {
|
||
"noise": ["20", 0],
|
||
"guider": ["21", 0],
|
||
"sampler": ["22", 0],
|
||
"sigmas": ["23", 0],
|
||
"latent_image": ["5", 0],
|
||
},
|
||
}
|
||
add(
|
||
"custom-cfg",
|
||
_graph(
|
||
None,
|
||
None,
|
||
{
|
||
**base,
|
||
"21": {
|
||
"class_type": "CFGGuider",
|
||
"inputs": {
|
||
"positive": ["6", 0],
|
||
"negative": ["7", 0],
|
||
"model": ["4", 0],
|
||
"cfg": 5,
|
||
},
|
||
},
|
||
},
|
||
sampler=guided,
|
||
),
|
||
)
|
||
add(
|
||
"custom-basic",
|
||
_graph(
|
||
None,
|
||
None,
|
||
{
|
||
**base,
|
||
"21": {
|
||
"class_type": "BasicGuider",
|
||
"inputs": {"conditioning": ["6", 0], "model": ["4", 0]},
|
||
},
|
||
},
|
||
sampler=guided,
|
||
),
|
||
)
|
||
add(
|
||
"custom-reroute-guider",
|
||
_graph(
|
||
None,
|
||
None,
|
||
{
|
||
**base,
|
||
"21": {"class_type": "Reroute", "inputs": {"value": ["24", 0]}},
|
||
"24": {
|
||
"class_type": "BasicGuider",
|
||
"inputs": {"conditioning": ["6", 0]},
|
||
},
|
||
},
|
||
sampler=guided,
|
||
),
|
||
)
|
||
add(
|
||
"custom-unknown-guider",
|
||
_graph(
|
||
None,
|
||
None,
|
||
{
|
||
**base,
|
||
"21": {"class_type": "DualCFGGuider", "inputs": {"cond1": ["6", 0]}},
|
||
},
|
||
sampler=guided,
|
||
),
|
||
)
|
||
add(
|
||
"latent-stage",
|
||
_graph(
|
||
["6", 0],
|
||
neg,
|
||
{
|
||
**base,
|
||
"5": {"class_type": "LatentUpscale", "inputs": {"samples": ["30", 0]}},
|
||
"30": {
|
||
"class_type": "KSamplerPreview",
|
||
"inputs": {"model": ["4", 0], "positive": ["6", 0]},
|
||
},
|
||
},
|
||
),
|
||
)
|
||
add(
|
||
"latent-encode",
|
||
_graph(
|
||
["6", 0],
|
||
neg,
|
||
{
|
||
**base,
|
||
"5": {
|
||
"class_type": "VAEEncode",
|
||
"inputs": {"pixels": ["31", 0], "vae": ["4", 2]},
|
||
},
|
||
"31": {"class_type": "LoadImage", "inputs": {"image": "x.png"}},
|
||
},
|
||
),
|
||
)
|
||
add(
|
||
"latent-cycle",
|
||
_graph(
|
||
["6", 0],
|
||
neg,
|
||
{
|
||
**base,
|
||
"5": {"class_type": "LatentUpscale", "inputs": {"samples": ["3", 0]}},
|
||
},
|
||
),
|
||
)
|
||
for override in ("", "wild", ["6", 0]):
|
||
detail = {
|
||
"class_type": "KSampler",
|
||
"inputs": {
|
||
"positive": ["6", 0],
|
||
"negative": neg,
|
||
"latent_image": ["5", 0],
|
||
"wildcard": override,
|
||
},
|
||
}
|
||
add(
|
||
f"override-ignored-{json.dumps(override)}",
|
||
_graph(None, None, base, sampler=detail),
|
||
)
|
||
chain, end = _chain(70)
|
||
add("depth-limit", _graph([end, 0], neg, {**base, **chain}))
|
||
chain, end = _chain(61)
|
||
add("depth-near", _graph([end, 0], neg, {**base, **chain}))
|
||
tree, top = _combine_tree(11)
|
||
add("work-limit-visits", _graph([top, 0], neg, {**base, **tree}))
|
||
tree, top = _combine_tree(9)
|
||
add("diamond-memo", _graph([top, 0], [top, 0], {**base, **tree}))
|
||
add("text-limit", _graph(["6", 0], neg, {**base, "6": _encode("x" * 100_001)}))
|
||
add(
|
||
"text-at-limit",
|
||
_graph(["6", 0], neg, {**base, "6": _encode("\U0001f600" * 100_000)}),
|
||
)
|
||
big = {
|
||
f"s{index}": {
|
||
"class_type": "PrimitiveString",
|
||
"inputs": {"value": "y" * 99_000},
|
||
}
|
||
for index in range(20)
|
||
}
|
||
joined = {
|
||
"class_type": "JoinStringMulti",
|
||
"inputs": {
|
||
"string_1": ["s0", 0],
|
||
"inputcount": 20,
|
||
"delimiter": "",
|
||
"return_list": False,
|
||
},
|
||
}
|
||
joined["inputs"].update(
|
||
{f"string_{index + 1}": [f"s{index}", 0] for index in range(1, 20)}
|
||
)
|
||
add(
|
||
"join-over-length",
|
||
_graph(["6", 0], neg, {**base, **big, "6": _encode(["8", 0]), "8": joined}),
|
||
)
|
||
concats = {
|
||
f"k{index}": {
|
||
"class_type": "StringConcatenate",
|
||
"inputs": {"string_a": "z" * 90_000, "string_b": "", "delimiter": ""},
|
||
}
|
||
for index in range(20)
|
||
}
|
||
combine = {f"q{index}": _encode([f"k{index}", 0]) for index in range(20)}
|
||
add("built-chars", _graph(["q0", 0], ["q1", 0], {**base, **concats, **combine}))
|
||
ceiling = {
|
||
str(index): {"class_type": "Note", "inputs": {}} for index in range(100, 20_102)
|
||
}
|
||
add("graph-ceiling", _graph(["6", 0], neg, {**base, **ceiling}))
|
||
workflow = {
|
||
"nodes": [
|
||
{"id": 6, "type": "PromptManager", "widgets_values": ["own text"]},
|
||
{"id": 7, "type": "PromptManager", "widgets_values": ["other text"]},
|
||
]
|
||
}
|
||
overwritten = {**base, "6": _encode("shared"), "7": _encode("shared")}
|
||
overwritten["6"]["inputs"]["search_text"] = ""
|
||
add(
|
||
"overwritten",
|
||
_graph(["6", 0], ["7", 0], overwritten),
|
||
workflow=json.dumps(workflow),
|
||
)
|
||
nested = {
|
||
"nodes": [{"id": 1, "type": "sub-a", "widgets_values": []}],
|
||
"definitions": {
|
||
"subgraphs": [
|
||
{
|
||
"id": "sub-a",
|
||
"nodes": [
|
||
{
|
||
"id": 6,
|
||
"type": "PromptManagerFocused",
|
||
"widgets_values": {"text": "a"},
|
||
},
|
||
{
|
||
"id": 7,
|
||
"type": "PromptManagerFocused",
|
||
"widgets_values": ["b"],
|
||
},
|
||
],
|
||
}
|
||
]
|
||
},
|
||
}
|
||
nested_graph = {
|
||
**base,
|
||
"1:6": {
|
||
"class_type": "PromptManager",
|
||
"inputs": {"text": "same", "clip": ["4", 1]},
|
||
},
|
||
"1:7": {"class_type": "PromptManager", "inputs": {"text": "same"}},
|
||
}
|
||
add(
|
||
"overwritten-subgraph",
|
||
_graph(["1:6", 0], ["1:7", 0], nested_graph),
|
||
workflow=json.dumps(nested),
|
||
)
|
||
add(
|
||
"overwritten-bad-workflow",
|
||
_graph(["6", 0], ["7", 0], overwritten),
|
||
workflow="{bad",
|
||
)
|
||
# A list or object node ``type`` is skipped like a non-dict node (#286), also
|
||
# inside a subgraph; a skipped node never hides the rest of the walk.
|
||
for label, kind in (("list", ["PromptManager"]), ("dict", {"a": 1})):
|
||
add(
|
||
f"workflow-type-{label}",
|
||
_graph(["6", 0], ["7", 0], overwritten),
|
||
workflow=json.dumps(
|
||
{"nodes": [{"id": 6, "type": "PromptManager"}, {"id": 8, "type": kind}]}
|
||
),
|
||
)
|
||
add(
|
||
"workflow-type-in-subgraph",
|
||
_graph(["1:6", 0], ["1:7", 0], nested_graph),
|
||
workflow=json.dumps(
|
||
{
|
||
**nested,
|
||
"definitions": {
|
||
"subgraphs": [{"id": "sub-a", "nodes": [{"id": 2, "type": [1]}]}]
|
||
},
|
||
}
|
||
),
|
||
)
|
||
add(
|
||
"workflow-type-skipped-keeps-overwritten",
|
||
_graph(["6", 0], ["7", 0], overwritten),
|
||
workflow=json.dumps(
|
||
{
|
||
"nodes": [
|
||
workflow["nodes"][0],
|
||
{"id": 8, "type": ["PromptManager"]},
|
||
workflow["nodes"][1],
|
||
]
|
||
}
|
||
),
|
||
)
|
||
add(
|
||
"workflow-type-without-id",
|
||
_graph(["6", 0], ["7", 0], overwritten),
|
||
workflow=json.dumps({"nodes": [{"id": True, "type": ["x"]}, "skipped"]}),
|
||
)
|
||
add(
|
||
"several-samplers",
|
||
_graph(
|
||
["6", 0],
|
||
neg,
|
||
{
|
||
**base,
|
||
"9": {
|
||
"class_type": "KSampler",
|
||
"inputs": {"positive": ["6", 0], "negative": neg},
|
||
},
|
||
},
|
||
),
|
||
)
|
||
add(
|
||
"detailer",
|
||
_graph(
|
||
None,
|
||
None,
|
||
base,
|
||
sampler={
|
||
"class_type": "FaceDetailer",
|
||
"inputs": {
|
||
"image": ["5", 0],
|
||
"positive": ["6", 0],
|
||
"negative": neg,
|
||
"wildcard": "",
|
||
},
|
||
},
|
||
),
|
||
)
|
||
# #381: unknown nodes between the prompt and the sampler pass through.
|
||
try:
|
||
from tests import unknown_node_graphs
|
||
except ImportError: # pragma: no cover - discover -s tests puts tests/ first
|
||
import unknown_node_graphs
|
||
for name, graph in unknown_node_graphs.shapes().items():
|
||
add(f"pass-through-{name}", graph)
|
||
for name, records in {
|
||
**unknown_node_graphs.recorded_shapes(),
|
||
**unknown_node_graphs.review_shapes(),
|
||
}.items():
|
||
cases[f"pass-through-{name}"] = records
|
||
try:
|
||
from tests import saver_anchor_graphs as anchor
|
||
except ImportError: # pragma: no cover - discover -s tests puts tests/ first
|
||
import saver_anchor_graphs as anchor
|
||
for name, graph, file_name in [
|
||
("branch-3", anchor.branch_bench(), "s45_branch_3_00002_.png"),
|
||
("branch-unanchored", anchor.branch_bench(), None),
|
||
("branch-no-match", anchor.branch_bench(), "photo_00001_.png"),
|
||
("branch-linked", anchor.linked_prefix_bench(), "s45_branch_2_00007_.png"),
|
||
(
|
||
"branch-unknown-saver",
|
||
anchor.branch_bench(saver="VendorSaveImage"),
|
||
"s45_branch_4_00001.webp",
|
||
),
|
||
("shared-prefix", anchor.shared_prefix(), "IMG_GEN_00371_.png"),
|
||
("same-links", anchor.two_stage_same_conditioning(), "final_00003_.png"),
|
||
("same-links-unanchored", anchor.two_stage_same_conditioning(), None),
|
||
(
|
||
"other-links",
|
||
anchor.two_stage_same_conditioning(["91", 0]),
|
||
"final_00003_.png",
|
||
),
|
||
("hidden-earlier", anchor.hidden_earlier_prompt(), "out_00001_.png"),
|
||
(
|
||
"hidden-override",
|
||
anchor.hidden_earlier_prompt(override=True),
|
||
"out_00001_.png",
|
||
),
|
||
] + [
|
||
(f"first-stage-{kind}", anchor.first_stage_variant(kind), "final_00003_.png")
|
||
for kind in (
|
||
"no-latent",
|
||
"literal-latent",
|
||
"missing-latent",
|
||
"cyclic-latent",
|
||
"stage-shaped",
|
||
"detailer",
|
||
)
|
||
]:
|
||
cases[f"anchor-{name}"] = {"prompt": _dump(graph)}
|
||
if file_name is not None:
|
||
cases[f"anchor-{name}"][FILE_NAME] = file_name
|
||
cases["parameters-only"] = {
|
||
"parameters": "a fox\nNegative prompt: blur\nSteps: 20, Sampler: euler"
|
||
}
|
||
cases["prompt-and-parameters"] = {
|
||
"prompt": _dump(_graph(["6", 0], neg, base)),
|
||
"parameters": "ignored",
|
||
}
|
||
return cases
|
||
|
||
|
||
def _visits_graph(count):
|
||
"""``count`` PrimitiveString links joined under one encoder (``count`` visits)."""
|
||
first, second = count // 2, count - count // 2
|
||
nodes = {
|
||
f"p{index}": {"class_type": "PrimitiveString", "inputs": {"value": "w"}}
|
||
for index in range(count)
|
||
}
|
||
for name, start, size in (("j1", 0, first), ("j2", first, second)):
|
||
inputs = {"inputcount": max(size, 1), "delimiter": "", "return_list": False}
|
||
inputs.update(
|
||
{f"string_{index + 1}": [f"p{start + index}", 0] for index in range(size)}
|
||
)
|
||
inputs.setdefault("string_1", "")
|
||
nodes[name] = {"class_type": "JoinStringMulti", "inputs": inputs}
|
||
nodes["k"] = {
|
||
"class_type": "StringConcatenate",
|
||
"inputs": {"string_a": ["j1", 0], "string_b": ["j2", 0], "delimiter": ""},
|
||
}
|
||
nodes["6"] = _encode(["k", 0])
|
||
nodes["7"] = _encode("blurry")
|
||
return {"prompt": _dump(_graph(["6", 0], ["7", 0], nodes))}
|
||
|
||
|
||
def _depth_graph(length):
|
||
chain, end = _chain(length)
|
||
return {
|
||
"prompt": _dump(_graph([end, 0], ["7", 0], {**chain, "7": _encode("blurry")}))
|
||
}
|
||
|
||
|
||
def _scan_graph(tail):
|
||
"""Whitespace-only Text Concatenate parts: scanned before stripping, never built."""
|
||
nodes, parts = {}, []
|
||
for index in range(4):
|
||
if index < 3:
|
||
inputs = {
|
||
f"text_{letter}": " " * 99_000 for letter in "abcdefghijklmnopqrstu"
|
||
}
|
||
else:
|
||
inputs, left = {}, tail
|
||
for letter in "abcdefghijklmnopqrstu":
|
||
inputs[f"text_{letter}"] = " " * min(99_000, left)
|
||
left -= min(99_000, left)
|
||
inputs["text_z"] = "kept" if index == 0 else ""
|
||
nodes[f"t{index}"] = {"class_type": "Text Concatenate", "inputs": inputs}
|
||
parts.append([f"t{index}", 0])
|
||
nodes["k"] = {
|
||
"class_type": "JoinStringMulti",
|
||
"inputs": {
|
||
"string_1": parts[0],
|
||
"string_2": parts[1],
|
||
"string_3": parts[2],
|
||
"string_4": parts[3],
|
||
"inputcount": 4,
|
||
"delimiter": "",
|
||
"return_list": False,
|
||
},
|
||
}
|
||
nodes["6"] = _encode(["k", 0])
|
||
nodes["7"] = _encode("blurry")
|
||
return {"prompt": _dump(_graph(["6", 0], ["7", 0], nodes))}
|
||
|
||
|
||
def _threshold(build, low, high):
|
||
"""The largest parameter in [low, high) the server still resolves."""
|
||
|
||
def resolved(value):
|
||
evidence = inspect_prompt_evidence(
|
||
build(value), None, profile=INSPECTION_PROFILE
|
||
)
|
||
return evidence["resolution"]["state"] == "resolved"
|
||
|
||
assert resolved(low) and not resolved(high)
|
||
while high - low > 1:
|
||
middle = (low + high) // 2
|
||
if resolved(middle):
|
||
low = middle
|
||
else:
|
||
high = middle
|
||
return low
|
||
|
||
|
||
# The nesting the browser's CPython-JSON parsers accept (py-json.mjs JSON_MAX_DEPTH).
|
||
JSON_MAX_DEPTH = 1000
|
||
JSON_DEPTHS = (JSON_MAX_DEPTH - 1, JSON_MAX_DEPTH, JSON_MAX_DEPTH + 1)
|
||
|
||
|
||
def _nested(depth):
|
||
"""``depth`` nested arrays."""
|
||
return "[" * depth + "]" * depth
|
||
|
||
|
||
def depth_cases():
|
||
"""``(name, kind, depth, records)``: a deep record at the JSON depth cap.
|
||
|
||
A ``prompt`` holds a junk input nested to ``depth`` containers in all (the
|
||
graph, its node and the inputs are three); parsed, both roles resolve.
|
||
A ``workflow`` nested to ``depth`` makes two PromptManager nodes
|
||
overwritten (R1) when it is read, and the prompt resolves when it is not.
|
||
"""
|
||
cases = []
|
||
base = {"6": _encode("a fox"), "7": _encode("blurry")}
|
||
for depth in JSON_DEPTHS:
|
||
graph = _dump(_graph(["6", 0], ["7", 0], {**base, "9": {"class_type": "Note"}}))
|
||
junk = '"inputs": {"junk": ' + _nested(depth - 3) + "}"
|
||
prompt = graph.replace('"class_type": "Note"', '"class_type": "Note", ' + junk)
|
||
cases.append((f"prompt-depth-{depth}", "prompt", depth, {"prompt": prompt}))
|
||
overwritten = {**base, "6": _encode("shared"), "7": _encode("shared")}
|
||
overwritten["6"]["inputs"]["search_text"] = ""
|
||
nodes = [
|
||
{"id": 6, "type": "PromptManager", "widgets_values": ["own text"]},
|
||
{"id": 7, "type": "PromptManager", "widgets_values": ["other text"]},
|
||
]
|
||
workflow = json.dumps({"nodes": nodes, "extra": 0}).replace(
|
||
'"extra": 0', '"extra": ' + _nested(depth - 1)
|
||
)
|
||
records = {
|
||
"prompt": _dump(_graph(["6", 0], ["7", 0], overwritten)),
|
||
"workflow": workflow,
|
||
}
|
||
cases.append((f"workflow-depth-{depth}", "workflow", depth, records))
|
||
return cases
|
||
|
||
|
||
def boundary_cases():
|
||
"""Each work bound at its edge: the last value that resolves, then the next."""
|
||
cases = {}
|
||
for name, build, low, high in (
|
||
("visits", _visits_graph, 10, 2000),
|
||
("depth", _depth_graph, 1, 80),
|
||
("scanned", _scan_graph, 0, 2_000_000),
|
||
):
|
||
edge = _threshold(build, low, high)
|
||
cases[f"{name}-edge"] = build(edge)
|
||
cases[f"{name}-over"] = build(edge + 1)
|
||
long_name = "\U0001f600" * 70
|
||
cases["named-astral"] = {
|
||
"prompt": _dump(
|
||
_graph(
|
||
["\U0001f600" * 70, 0],
|
||
["7", 0],
|
||
{
|
||
"\U0001f600"
|
||
# No text and no links: still named, not passed through (#381).
|
||
* 70: {"class_type": long_name, "inputs": {"seed": 1}},
|
||
"7": _encode("b"),
|
||
},
|
||
)
|
||
)
|
||
}
|
||
return cases
|
||
|
||
|
||
class _Builder:
|
||
"""Coherent random graphs over the traced vocabulary, sometimes corrupted."""
|
||
|
||
def __init__(self, rng):
|
||
self.rng = rng
|
||
self.nodes = {}
|
||
self.records = {}
|
||
self.counter = 100
|
||
|
||
def new_id(self):
|
||
self.counter += 1
|
||
return str(self.counter)
|
||
|
||
def word(self):
|
||
rng = self.rng
|
||
parts = [rng.choice(SPACES), rng.choice(WORDS), rng.choice(SPACES)]
|
||
if rng.random() < 0.3:
|
||
parts.append(rng.choice(WORDS))
|
||
return "".join(parts)
|
||
|
||
def string(self, depth):
|
||
"""A string input: a literal, or a link to a string node."""
|
||
rng = self.rng
|
||
if depth > 3 or rng.random() < 0.45:
|
||
return self.word()
|
||
node_id, choice = self.new_id(), rng.randrange(8)
|
||
if choice == 0:
|
||
node = {
|
||
"class_type": rng.choice(
|
||
["PrimitiveString", "PrimitiveStringMultiline"]
|
||
),
|
||
"inputs": {"value": self.string(depth + 1)},
|
||
}
|
||
elif choice == 1:
|
||
node = {
|
||
"class_type": "StringConcatenate",
|
||
"inputs": {
|
||
"string_a": self.string(depth + 1),
|
||
"string_b": self.string(depth + 1),
|
||
"delimiter": rng.choice(["", ", ", " "]),
|
||
},
|
||
}
|
||
elif choice == 2:
|
||
inputs = {
|
||
f"text_{letter}": self.string(depth + 1)
|
||
for letter in rng.sample("abcd", rng.randint(1, 3))
|
||
}
|
||
inputs["clean_whitespace"] = rng.choice(["true", "false", True])
|
||
node = {"class_type": "Text Concatenate", "inputs": inputs}
|
||
elif choice == 3:
|
||
node = {
|
||
"class_type": "StringConstantMultiline",
|
||
"inputs": {
|
||
"string": self.string(depth + 1),
|
||
"strip_newlines": rng.choice([True, False]),
|
||
},
|
||
}
|
||
elif choice == 4:
|
||
count = rng.randint(1, 4)
|
||
inputs = {
|
||
f"string_{index}": self.string(depth + 1)
|
||
for index in range(1, count + 1)
|
||
if index == 1 or rng.random() < 0.8
|
||
}
|
||
inputs.update(
|
||
{
|
||
"inputcount": count,
|
||
"delimiter": rng.choice([" ", ","]),
|
||
"return_list": rng.random() < 0.1,
|
||
}
|
||
)
|
||
node = {"class_type": "JoinStringMulti", "inputs": inputs}
|
||
elif choice == 5:
|
||
node = {
|
||
"class_type": "Reroute",
|
||
"inputs": {"value": self.string_link(depth + 1)},
|
||
}
|
||
elif choice == 6:
|
||
node = {
|
||
"class_type": rng.choice(["StringConstant", "TextInput"]),
|
||
"inputs": {rng.choice(["string", "text"]): self.string(depth + 1)},
|
||
}
|
||
else:
|
||
node = {
|
||
"class_type": "PromptManagerText",
|
||
"inputs": {
|
||
"text": self.string(depth + 1),
|
||
"prepend_text": self.word(),
|
||
"append_text": self.word(),
|
||
},
|
||
}
|
||
self.nodes[node_id] = node
|
||
return [node_id, 0]
|
||
|
||
def string_link(self, depth):
|
||
value = self.string(depth)
|
||
if isinstance(value, list):
|
||
return value
|
||
node_id = self.new_id()
|
||
self.nodes[node_id] = {
|
||
"class_type": "PrimitiveString",
|
||
"inputs": {"value": value},
|
||
}
|
||
return [node_id, 0]
|
||
|
||
def conditioning(self, depth):
|
||
rng = self.rng
|
||
node_id = self.new_id()
|
||
choice = rng.randrange(11) if depth < 4 else 0
|
||
slot = 0
|
||
if choice == 0:
|
||
node = {
|
||
"class_type": "CLIPTextEncode",
|
||
"inputs": {"text": self.string(depth + 1), "clip": ["4", 1]},
|
||
}
|
||
elif choice == 1:
|
||
text = self.string(depth + 1)
|
||
node = {
|
||
"class_type": "CLIPTextEncodeSDXL",
|
||
"inputs": {
|
||
"text_g": text,
|
||
"text_l": (
|
||
text if rng.random() < 0.6 else rng.choice(["", self.word()])
|
||
),
|
||
},
|
||
}
|
||
elif choice == 2:
|
||
node = {
|
||
"class_type": "FluxGuidance",
|
||
"inputs": {"conditioning": self.conditioning(depth + 1)},
|
||
}
|
||
elif choice == 3:
|
||
node = {
|
||
"class_type": "ConditioningZeroOut",
|
||
"inputs": {"conditioning": self.conditioning(depth + 1)},
|
||
}
|
||
elif choice == 4:
|
||
first = self.conditioning(depth + 1)
|
||
node = {
|
||
"class_type": "ConditioningCombine",
|
||
"inputs": {
|
||
"conditioning_1": first,
|
||
"conditioning_2": (
|
||
first if rng.random() < 0.5 else self.conditioning(depth + 1)
|
||
),
|
||
},
|
||
}
|
||
elif choice == 5:
|
||
condition = rng.choice([True, False, ["4", 0], ["4", 1]])
|
||
node = {
|
||
"class_type": "easy ifElse",
|
||
"inputs": {
|
||
"boolean": condition,
|
||
"on_true": self.conditioning(depth + 1),
|
||
"on_false": self.conditioning(depth + 1),
|
||
},
|
||
}
|
||
elif choice == 6:
|
||
node = {
|
||
"class_type": rng.choice(["PromptManager", "PromptManagerFocused"]),
|
||
"inputs": {
|
||
"text": self.string(depth + 1),
|
||
"prepend_text": self.word(),
|
||
"append_text": self.word(),
|
||
"clip": ["4", 1],
|
||
},
|
||
}
|
||
elif choice == 7:
|
||
node = {
|
||
"class_type": rng.choice(["PromptManagerV2", "PromptManagerPositive"]),
|
||
"inputs": {
|
||
"text": self.string(depth + 1),
|
||
"prepend_text": self.word(),
|
||
"negative_text": self.word(),
|
||
},
|
||
}
|
||
elif choice == 8:
|
||
slot = rng.choice([0, 1])
|
||
texts = {
|
||
"text": self.word(),
|
||
"negative_text": self.word(),
|
||
"prepend_text": self.word(),
|
||
"append_text": self.word(),
|
||
}
|
||
node = {"class_type": "PromptManagerPair", "inputs": dict(texts)}
|
||
if rng.random() < 0.6:
|
||
record = {
|
||
**texts,
|
||
"positive_text": self.word(),
|
||
"negative_effective_text": texts["negative_text"],
|
||
}
|
||
record["prepend_text"], record["append_text"] = (
|
||
record["prepend_text"].strip(),
|
||
record["append_text"].strip(),
|
||
)
|
||
if rng.random() < 0.2:
|
||
record["text"] = record["text"] + "!"
|
||
self.records[EXECUTION_KEY] = json.dumps(
|
||
{"version": 1, "nodes": {node_id: {"records": [record]}}}
|
||
)
|
||
elif choice == 9:
|
||
node = {
|
||
"class_type": "Reroute",
|
||
"inputs": {"value": self.conditioning(depth + 1)},
|
||
}
|
||
else:
|
||
node = {
|
||
"class_type": rng.choice(["PromptSearchList", "FancyEncoder"]),
|
||
"inputs": {"text": self.word()},
|
||
}
|
||
self.nodes[node_id] = node
|
||
return [node_id, slot]
|
||
|
||
def corrupt(self):
|
||
rng = self.rng
|
||
node_id = rng.choice(sorted(self.nodes))
|
||
node = self.nodes[node_id]
|
||
links = [
|
||
key
|
||
for key, value in node.get("inputs", {}).items()
|
||
if isinstance(value, list)
|
||
]
|
||
choice = rng.randrange(6)
|
||
if choice == 0 and links:
|
||
node["inputs"][rng.choice(links)][1] = rng.choice([0.0, True, 7, -1])
|
||
elif choice == 1 and links:
|
||
node["inputs"][rng.choice(links)][0] = rng.choice(["missing", node_id])
|
||
elif choice == 2:
|
||
node["class_type"] = rng.choice([None, 3, "Unknown"])
|
||
elif choice == 3:
|
||
node["inputs"] = rng.choice([[], {}])
|
||
elif choice == 4 and links:
|
||
del node["inputs"][rng.choice(links)]
|
||
else:
|
||
node.pop("inputs", None)
|
||
|
||
|
||
EXECUTION_KEY = "comfyassets_prompt_pairs"
|
||
|
||
|
||
def fuzz_cases(seed, count):
|
||
rng = random.Random(seed)
|
||
cases = {}
|
||
for index in range(count):
|
||
builder = _Builder(rng)
|
||
positive = builder.conditioning(0)
|
||
negative = builder.conditioning(0) if rng.random() < 0.9 else None
|
||
graph = _graph(positive, negative, builder.nodes)
|
||
if negative is None:
|
||
del graph["3"]["inputs"]["negative"]
|
||
if rng.random() < 0.25:
|
||
builder.nodes = graph
|
||
builder.corrupt()
|
||
records = {"prompt": json.dumps(graph, ensure_ascii=rng.random() < 0.5)}
|
||
records.update(builder.records)
|
||
cases[f"fuzz-{seed}-{index}"] = records
|
||
return cases
|
||
|
||
|
||
def single_chain(records):
|
||
"""Whether the server answers ``records`` without choosing among samplers."""
|
||
roles = {key for key in records if key in ("prompt", "parameters", EXECUTION_KEY)}
|
||
if "prompt" not in roles:
|
||
return "parameters" not in roles
|
||
try:
|
||
graph = json.loads(records["prompt"])
|
||
except (ValueError, RecursionError):
|
||
return True
|
||
if (
|
||
not isinstance(graph, dict)
|
||
or not graph
|
||
or len(graph) > INSPECTION_PROFILE.node_ceiling
|
||
):
|
||
return True
|
||
samplers = get_semantics().samplers
|
||
found = [
|
||
node["class_type"]
|
||
for node in graph.values()
|
||
if isinstance(node, dict)
|
||
and isinstance(node.get("class_type"), str)
|
||
and node["class_type"] in samplers
|
||
and samplers[node["class_type"]]["strategy"]
|
||
in ("ksampler", "custom-advanced", "detailer-stage")
|
||
]
|
||
return len(found) <= 1 and not any(
|
||
samplers[name]["strategy"] == "detailer-stage" for name in found
|
||
)
|
||
|
||
|
||
def _utf8(records):
|
||
"""Whether the record texts can be written as UTF-8 (no lone surrogate)."""
|
||
try:
|
||
json.dumps(records, ensure_ascii=False).encode("utf-8")
|
||
except UnicodeEncodeError:
|
||
return False
|
||
return True
|
||
|
||
|
||
def committed_vectors():
|
||
"""The committed record sets with the server's evidence, in a stable order."""
|
||
try:
|
||
from tests import local_resolver_selection_cases as selection
|
||
except ImportError: # pragma: no cover - discover -s tests puts tests/ first
|
||
import local_resolver_selection_cases as selection
|
||
|
||
named = {**golden_cases(), **selection.selection_cases()}
|
||
cases = {
|
||
name: records
|
||
for name, records in named.items()
|
||
if len(json.dumps(records)) <= MAX_VECTOR_CHARS and _utf8(records)
|
||
}
|
||
cases.update(fuzz_cases(*COMMITTED_FUZZ))
|
||
cases.update(selection.selection_fuzz_cases(*COMMITTED_FUZZ))
|
||
cases.update(selection.passthrough_fuzz_cases(*COMMITTED_PASSTHROUGH))
|
||
vectors = [
|
||
{
|
||
"name": name,
|
||
"records": records,
|
||
"evidence": inspect_prompt_evidence(
|
||
{key: value for key, value in records.items() if key != FILE_NAME},
|
||
None,
|
||
profile=INSPECTION_PROFILE,
|
||
file_name=records.get(FILE_NAME),
|
||
),
|
||
}
|
||
for name, records in cases.items()
|
||
]
|
||
return {"version": 2, "vectors": vectors}
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||
parser.add_argument("--write", action="store_true", help="rewrite the vectors")
|
||
if parser.parse_args().write:
|
||
text = json.dumps(committed_vectors(), ensure_ascii=False, sort_keys=True)
|
||
VECTORS_PATH.write_text(text + "\n", encoding="utf-8", newline="\n")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|