feat(inspect): port legacy parser rules the new resolver missed (upstream nodes)
R6 and R9 for nodes not installed beside ComfyUI here. Their input names and output slots come from each pack's source at the commit the saved workflows record in the node's cnr_id and ver properties (the task report lists repository, commit, file and line): - samplers ClownsharKSampler_Beta (RES4LYF), KSampler (Efficient) and ttN KSampler_v2 (latent input "latent"); - pass-throughs SeedVarianceEnhancer and ApplyInstantIDAdvanced (positive 1, negative 2; output 0 is the model); - selector Switch conditioning [Crystools]; - encoder smZ CLIPTextEncode: its text when with_SDXL is false; when true the connected CLIP decides at run time which inputs are read, so the prompt is known only when text, text_g and text_l agree; - strings ttN text, CR Text, CR Prompt Text and TextBoxMira; - facts model pass-through CR Apply LoRA Stack. Inputs that replace what a sampler reads are not followed: RES4LYF options (may carry a guider, and with sigmas replace the settings), an Efficiency script and a ttN XY plot leave the prompt unsupported and the facts unresolved. The overwritten PromptManager refusal (R1) still holds behind SeedVarianceEnhancer. The default profile does not read the table.
This commit is contained in:
@@ -258,7 +258,7 @@ test("stage walks fail on unlisted sampler-shaped nodes like the resolver", () =
|
||||
];
|
||||
for (const inputs of shaped) {
|
||||
const graph = detailerFactGraph();
|
||||
graph.efficient = { class_type: "KSampler (Efficient)", inputs: { ...inputs, image: ["decode", 0] } };
|
||||
graph.efficient = { class_type: "MysterySampler", inputs: { ...inputs, image: ["decode", 0] } };
|
||||
graph.d.inputs.image = ["efficient", 0];
|
||||
for (const sampler_id of ["d", null]) {
|
||||
const facts = factsFor(graph, { resolution: { state: "ambiguous", sampler_id } });
|
||||
@@ -304,7 +304,7 @@ test("stage walks follow only the resolver's links", () => {
|
||||
// Review M-2: `["eff", -1]` is not a link to the server, so it is not walked.
|
||||
for (const slot of [-1, 0.5, "0"]) {
|
||||
const graph = detailerFactGraph();
|
||||
graph.eff = { class_type: "KSampler (Efficient)", inputs: { positive: ["model", 0], model: ["model", 0] } };
|
||||
graph.eff = { class_type: "MysterySampler", inputs: { positive: ["model", 0], model: ["model", 0] } };
|
||||
graph.decode.inputs.extra = ["eff", slot];
|
||||
assert.equal(factsFor(graph, { resolution: { state: "ambiguous", sampler_id: "d" } }).seed.status, "ambiguous", String(slot));
|
||||
const shaped = detailerFactGraph();
|
||||
@@ -313,7 +313,7 @@ test("stage walks follow only the resolver's links", () => {
|
||||
assert.equal(factsFor(shaped, { resolution: { state: "ambiguous", sampler_id: "d" } }).seed.status, "ambiguous", `shaped ${slot}`);
|
||||
}
|
||||
const linked = detailerFactGraph();
|
||||
linked.eff = { class_type: "KSampler (Efficient)", inputs: { positive: ["model", 0], model: ["model", 0] } };
|
||||
linked.eff = { class_type: "MysterySampler", inputs: { positive: ["model", 0], model: ["model", 0] } };
|
||||
linked.decode.inputs.extra = ["eff", 0];
|
||||
assert.equal(factsFor(linked, { resolution: { state: "ambiguous", sampler_id: "d" } }).seed.status, "unresolved");
|
||||
});
|
||||
|
||||
@@ -96,3 +96,24 @@ test("R6: upscale and SEGS detailers supply stage facts like FaceDetailer", () =
|
||||
assert.deepEqual([merged.seed[0], merged.steps[0], merged.cfg], ["ambiguous", "ambiguous", ["known", "7"]], class_type);
|
||||
}
|
||||
});
|
||||
|
||||
test("R6: upstream-verified samplers supply facts unless an override input is set", () => {
|
||||
for (const [class_type, overrides] of [["ClownsharKSampler_Beta", ["options", "sigmas"]], ["KSampler (Efficient)", ["script"]], ["ttN KSampler_v2", ["adv_xyPlot"]]]) {
|
||||
const graph = modelGraph("CR Apply LoRA Stack");
|
||||
graph.s.class_type = class_type;
|
||||
assert.deepEqual(graphFacts(graph), {
|
||||
seed: ["known", "5"], steps: ["known", "20"], cfg: ["known", "7"], sampler: ["known", "euler"],
|
||||
scheduler: ["known", "normal"], model: ["known", "base.safetensors"],
|
||||
}, class_type);
|
||||
for (const name of overrides) {
|
||||
const overridden = structuredClone(graph);
|
||||
overridden.s.inputs[name] = ["script", 0];
|
||||
overridden.script = { class_type: "SomeScript", inputs: {} };
|
||||
assert.deepEqual(graphFacts(overridden), ALL_UNRESOLVED, `${class_type} ${name}`);
|
||||
for (const unset of [null, ""]) {
|
||||
overridden.s.inputs[name] = unset;
|
||||
assert.equal(graphFacts(overridden).seed[0], "known", `${class_type} ${name} ${unset}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -38,8 +38,12 @@ test("the facts tables are not part of the legacy page contract", () => {
|
||||
// (which classes are samplers, their facts shape and a detailer's image input).
|
||||
test("sampler facts table equals semantics.samplers", () => {
|
||||
assert.equal(typeof metadata._samplerTable, "object", "comfy-metadata exposes its sampler table for this test");
|
||||
const expected = Object.fromEntries(Object.entries(semantics.samplers).map(([name, entry]) => [name,
|
||||
entry.image_stage === undefined ? { facts: entry.facts } : { facts: entry.facts, image_stage: entry.image_stage }]));
|
||||
// Task 2.7b: plus `fact_overrides`; `prompt_overrides` is read only by the resolver.
|
||||
const expected = Object.fromEntries(Object.entries(semantics.samplers).map(([name, entry]) => [name, {
|
||||
facts: entry.facts,
|
||||
...(entry.image_stage === undefined ? {} : { image_stage: entry.image_stage }),
|
||||
...(entry.fact_overrides === undefined ? {} : { fact_overrides: entry.fact_overrides }),
|
||||
}]));
|
||||
assert.deepStrictEqual(plain(metadata._samplerTable), expected);
|
||||
assert.ok(Object.isFrozen(metadata._samplerTable));
|
||||
});
|
||||
|
||||
@@ -625,5 +625,194 @@ class TestInstalledNodeSemantics(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
def _sampled(class_type: str, latent: str = "latent_image", **inputs) -> dict:
|
||||
"""``f02`` with its KSampler (node 3) replaced by ``class_type``."""
|
||||
prompt = _f02()
|
||||
sampler = dict(prompt["3"]["inputs"], **inputs)
|
||||
sampler[latent] = sampler.pop("latent_image")
|
||||
prompt["3"] = {"class_type": class_type, "inputs": sampler}
|
||||
return prompt
|
||||
|
||||
|
||||
SAMPLERS = (
|
||||
("ClownsharKSampler_Beta", "latent_image", "options"),
|
||||
("KSampler (Efficient)", "latent_image", "script"),
|
||||
("ttN KSampler_v2", "latent", "adv_xyPlot"),
|
||||
)
|
||||
|
||||
|
||||
class TestUpstreamVerifiedNodeSemantics(unittest.TestCase):
|
||||
"""R6 entries for nodes not installed here, read from their pinned sources.
|
||||
|
||||
Each source is the commit the maintainer's saved workflows name in the
|
||||
node's ``cnr_id``/``ver`` properties (the report lists repository, commit,
|
||||
file and line).
|
||||
"""
|
||||
|
||||
def test_ksampler_shaped_samplers_resolve(self):
|
||||
for class_type, latent, _ in SAMPLERS:
|
||||
with self.subTest(node=class_type):
|
||||
result = _inspect({"prompt": _sampled(class_type, latent)})
|
||||
self.assertEqual(result["state"], "resolved")
|
||||
self.assertEqual(result["sampler_id"], "3")
|
||||
self.assertEqual(_texts(result), (parity.POS, parity.NEG))
|
||||
|
||||
def test_inputs_that_replace_the_prompt_are_not_followed(self):
|
||||
# RES4LYF options may carry a guider; Efficiency scripts and the ttN
|
||||
# XY plot re-run the sampler with other settings.
|
||||
for class_type, latent, override in SAMPLERS:
|
||||
with self.subTest(node=class_type):
|
||||
prompt = _sampled(class_type, latent, **{override: ["30", 0]})
|
||||
prompt["30"] = {"class_type": "SomeScript", "inputs": {}}
|
||||
result = _inspect({"prompt": prompt})
|
||||
self.assertEqual(result["reason_code"], "unsupported-node")
|
||||
self.assertEqual(
|
||||
result["node"], {"id": "30", "class_type": "SomeScript"}
|
||||
)
|
||||
|
||||
def test_chained_samplers_are_stages(self):
|
||||
prompt = _sampled("ClownsharKSampler_Beta")
|
||||
prompt["31"] = dict(prompt["3"], inputs=dict(prompt["3"]["inputs"]))
|
||||
prompt["31"]["inputs"]["latent_image"] = ["3", 0]
|
||||
prompt["8"]["inputs"]["samples"] = ["31", 0]
|
||||
# Unanchored, two listed samplers are two branches, as for KSampler.
|
||||
self.assertEqual(_inspect({"prompt": prompt})["state"], "ambiguous")
|
||||
result = _inspect({"prompt": prompt}, output_node_id="9")
|
||||
self.assertEqual(result["reason"], resolver.MULTISTAGE_UNATTRIBUTED_REASON)
|
||||
|
||||
def test_seed_variance_enhancer_passes_its_conditioning(self):
|
||||
prompt = _f02(
|
||||
**{
|
||||
"20": {
|
||||
"class_type": "SeedVarianceEnhancer",
|
||||
"inputs": {"conditioning": ["6", 0], "seed": 1, "strength": 20.0},
|
||||
}
|
||||
}
|
||||
)
|
||||
prompt["3"]["inputs"]["positive"] = ["20", 0]
|
||||
self.assertEqual(_texts(_inspect({"prompt": prompt})), (parity.POS, parity.NEG))
|
||||
|
||||
def test_overwritten_text_behind_seed_variance_is_still_refused(self):
|
||||
# R1 before R6: the recon's 24 files differ from this only in names.
|
||||
metadata = _overwritten()
|
||||
metadata["prompt"]["20"] = {
|
||||
"class_type": "SeedVarianceEnhancer",
|
||||
"inputs": {"conditioning": ["17", 0], "seed": 1},
|
||||
}
|
||||
metadata["prompt"]["3"]["inputs"]["positive"] = ["20", 0]
|
||||
result = _inspect(metadata)
|
||||
self.assertEqual(result["state"], "unresolved")
|
||||
self.assertEqual(result["reason"], resolver.OVERWRITTEN_PROMPT_REASON)
|
||||
|
||||
def test_instantid_advanced_outputs(self):
|
||||
prompt = _f02(
|
||||
**{
|
||||
"20": {
|
||||
"class_type": "ApplyInstantIDAdvanced",
|
||||
"inputs": {
|
||||
"model": ["4", 0],
|
||||
"positive": ["6", 0],
|
||||
"negative": ["7", 0],
|
||||
"ip_weight": 0.8,
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
prompt["3"]["inputs"].update(positive=["20", 1], negative=["20", 2])
|
||||
self.assertEqual(_texts(_inspect({"prompt": prompt})), (parity.POS, parity.NEG))
|
||||
prompt["3"]["inputs"]["positive"] = ["20", 0] # output 0 is the MODEL
|
||||
self.assertEqual(
|
||||
_inspect({"prompt": prompt})["reason_code"], "unsupported-node"
|
||||
)
|
||||
|
||||
def test_crystools_conditioning_switch(self):
|
||||
def switch(condition) -> dict:
|
||||
prompt = _f02(
|
||||
**{
|
||||
"11": _pm_encoder("a dog"),
|
||||
"20": {
|
||||
"class_type": "Switch conditioning [Crystools]",
|
||||
"inputs": {
|
||||
"on_true": ["6", 0],
|
||||
"on_false": ["11", 0],
|
||||
"boolean": condition,
|
||||
},
|
||||
},
|
||||
"21": {"class_type": "PrimitiveBoolean", "inputs": {"value": True}},
|
||||
}
|
||||
)
|
||||
prompt["3"]["inputs"]["positive"] = ["20", 0]
|
||||
return _inspect({"prompt": prompt})
|
||||
|
||||
self.assertEqual(_texts(switch(True))[0], parity.POS)
|
||||
self.assertEqual(_texts(switch(False))[0], "a dog")
|
||||
result = switch(["21", 0])
|
||||
self.assertEqual(result["state"], "ambiguous")
|
||||
self.assertEqual(
|
||||
sorted(_texts(item)[0] for item in result["candidates"]),
|
||||
sorted([parity.POS, "a dog"]),
|
||||
)
|
||||
|
||||
def test_smz_encoder_reads_text_unless_sdxl_may_apply(self):
|
||||
def smz(**inputs) -> dict:
|
||||
node = {
|
||||
"class_type": "smZ CLIPTextEncode",
|
||||
"inputs": {
|
||||
"clip": ["4", 1],
|
||||
"text": "a cat",
|
||||
"text_g": "a cat",
|
||||
"text_l": "a cat",
|
||||
"with_SDXL": False,
|
||||
"parser": "comfy",
|
||||
**inputs,
|
||||
},
|
||||
}
|
||||
return _inspect({"prompt": _f02(**{"6": node})})
|
||||
|
||||
self.assertEqual(_texts(smz(text_g="", text_l="x")), ("a cat", parity.NEG))
|
||||
# with_SDXL: the connected CLIP decides at run time which inputs are read.
|
||||
self.assertEqual(_texts(smz(with_SDXL=True)), ("a cat", parity.NEG))
|
||||
for parts in ({"text_g": "x"}, {"text_l": ""}, {"text": "x"}):
|
||||
with self.subTest(parts=parts):
|
||||
result = smz(with_SDXL=True, **parts)
|
||||
self.assertEqual(result["reason_code"], "runtime-value")
|
||||
self.assertEqual(
|
||||
result["node"], {"id": "6", "class_type": "smZ CLIPTextEncode"}
|
||||
)
|
||||
linked = smz(with_SDXL=["21", 0])
|
||||
self.assertEqual(linked["reason_code"], "unsupported-node")
|
||||
for broken in ({"with_SDXL": "yes"}, {"with_SDXL": None}):
|
||||
with self.subTest(flag=broken):
|
||||
self.assertEqual(smz(**broken)["reason_code"], "malformed")
|
||||
|
||||
def test_string_sources(self):
|
||||
cases = (
|
||||
("ttN text", "text"),
|
||||
("CR Text", "text"),
|
||||
("CR Prompt Text", "prompt"),
|
||||
("TextBoxMira", "text"),
|
||||
)
|
||||
for class_type, name in cases:
|
||||
with self.subTest(node=class_type):
|
||||
prompt = _f02(
|
||||
**{"10": {"class_type": class_type, "inputs": {name: parity.POS}}}
|
||||
)
|
||||
prompt["6"]["inputs"]["text"] = ["10", 0]
|
||||
self.assertEqual(
|
||||
_texts(_inspect({"prompt": prompt})), (parity.POS, parity.NEG)
|
||||
)
|
||||
# Output 1 of the Comfyroll nodes is their help URL.
|
||||
prompt["6"]["inputs"]["text"] = ["10", 1]
|
||||
self.assertEqual(
|
||||
_inspect({"prompt": prompt})["reason_code"], "unsupported-node"
|
||||
)
|
||||
|
||||
def test_default_profile_keeps_them_unsupported(self):
|
||||
for class_type, latent, _ in SAMPLERS:
|
||||
with self.subTest(node=class_type):
|
||||
result = resolve_prompt_pair({"prompt": _sampled(class_type, latent)})
|
||||
self.assertEqual(result["reason"], "No supported sampler pair found")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -304,7 +304,6 @@ class SpecEntriesTest(unittest.TestCase):
|
||||
"ImpactWildcardProcessor",
|
||||
"RegexReplace",
|
||||
"LoRAfromText",
|
||||
"TextBoxMira",
|
||||
"XYZPrompt",
|
||||
):
|
||||
self.assertNotIn(name, table.all_classes)
|
||||
@@ -431,6 +430,7 @@ class SpecEntriesTest(unittest.TestCase):
|
||||
"ModelPatchTorchSettings",
|
||||
"CFGNorm",
|
||||
"PerturbedAttentionGuidance",
|
||||
"CR Apply LoRA Stack",
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
@@ -494,7 +494,9 @@ class ValidationTest(_TableCase):
|
||||
"conditioning": frozenset({"passthrough", "zeroed"}),
|
||||
"selectors": frozenset({"boolean-select"}),
|
||||
"composites": frozenset({"combine"}),
|
||||
"encoders": frozenset({"single", "parts-equal", "pm-compose"}),
|
||||
"encoders": frozenset(
|
||||
{"single", "parts-equal", "pm-compose", "flag-parts"}
|
||||
),
|
||||
"strings": frozenset(
|
||||
{"literal", "strip-newlines", "join-multi", "concat"}
|
||||
),
|
||||
@@ -714,6 +716,100 @@ def _depth(value, level=1):
|
||||
class SamplerOptionsTest(_TableCase):
|
||||
"""Task 2.7b: optional sampler lists are input names, or the table fails."""
|
||||
|
||||
def test_upstream_verified_ports_are_listed_with_their_source_inputs(self):
|
||||
# Not installed here: read from the pinned upstream commits the saved
|
||||
# workflows name (the report lists repository, commit, file and line).
|
||||
table = load_semantics()
|
||||
pair = {"positive": "positive", "negative": "negative"}
|
||||
for name, latent, prompt_overrides, fact_overrides in (
|
||||
(
|
||||
"ClownsharKSampler_Beta",
|
||||
"latent_image",
|
||||
("options",),
|
||||
("options", "sigmas"),
|
||||
),
|
||||
("KSampler (Efficient)", "latent_image", ("script",), ("script",)),
|
||||
("ttN KSampler_v2", "latent", ("adv_xyPlot",), ("adv_xyPlot",)),
|
||||
):
|
||||
self.assertEqual(
|
||||
table.samplers[name],
|
||||
{
|
||||
"strategy": "ksampler",
|
||||
"conditioning": pair,
|
||||
"latent": latent,
|
||||
"facts": "ksampler",
|
||||
"prompt_overrides": prompt_overrides,
|
||||
"fact_overrides": fact_overrides,
|
||||
},
|
||||
name,
|
||||
)
|
||||
for (name, slot), source in {
|
||||
("SeedVarianceEnhancer", 0): "conditioning",
|
||||
("ApplyInstantIDAdvanced", 1): "positive",
|
||||
("ApplyInstantIDAdvanced", 2): "negative",
|
||||
}.items():
|
||||
self.assertEqual(
|
||||
table.conditioning[(name, slot)],
|
||||
{"strategy": "passthrough", "input": source},
|
||||
)
|
||||
self.assertNotIn(("ApplyInstantIDAdvanced", 0), table.conditioning)
|
||||
self.assertEqual(
|
||||
table.selectors["Switch conditioning [Crystools]"],
|
||||
table.selectors["easy ifElse"],
|
||||
)
|
||||
self.assertEqual(
|
||||
table.encoders[("smZ CLIPTextEncode", 0)],
|
||||
{
|
||||
"strategy": "flag-parts",
|
||||
"inputs": ("text", "text_g", "text_l"),
|
||||
"params": {"flag": "with_SDXL"},
|
||||
},
|
||||
)
|
||||
for name, source in (
|
||||
("ttN text", "text"),
|
||||
("CR Text", "text"),
|
||||
("CR Prompt Text", "prompt"),
|
||||
("TextBoxMira", "text"),
|
||||
):
|
||||
self.assertEqual(
|
||||
table.strings[(name, 0)], {"strategy": "literal", "inputs": (source,)}
|
||||
)
|
||||
self.assertNotIn((name, 1), table.strings)
|
||||
|
||||
def test_flag_parts_needs_a_flag_and_two_inputs(self):
|
||||
doc = _minimal()
|
||||
entry = {
|
||||
"class": "smZ CLIPTextEncode",
|
||||
"slot": 0,
|
||||
"strategy": "flag-parts",
|
||||
"inputs": ["text", "text_g"],
|
||||
"params": {"flag": "with_SDXL"},
|
||||
}
|
||||
doc["encoders"].append(entry)
|
||||
self.assertEqual(
|
||||
self.load_doc(doc).encoders[("smZ CLIPTextEncode", 0)]["params"],
|
||||
{"flag": "with_SDXL"},
|
||||
)
|
||||
for change in (
|
||||
{"params": {}},
|
||||
{"params": {"flag": "x", "other": "y"}},
|
||||
{"params": {"flag": ""}},
|
||||
{"params": None},
|
||||
{"inputs": ["text"]},
|
||||
{"compose": True},
|
||||
):
|
||||
with self.subTest(change=change):
|
||||
broken = _minimal()
|
||||
broken["encoders"].append({**entry, **change})
|
||||
with self.assertRaises(SemanticsError):
|
||||
self.load_doc(broken)
|
||||
broken = _minimal()
|
||||
broken["encoders"].append(
|
||||
{key: item for key, item in entry.items() if key != "params"}
|
||||
)
|
||||
with self.assertRaises(SemanticsError):
|
||||
self.load_doc(broken)
|
||||
|
||||
def test_prompt_overrides_are_input_names(self):
|
||||
doc = _minimal()
|
||||
doc["samplers"]["KSampler"]["prompt_overrides"] = ["wildcard"]
|
||||
@@ -725,6 +821,13 @@ class SamplerOptionsTest(_TableCase):
|
||||
with self.assertRaises(SemanticsError):
|
||||
self.load_doc(doc)
|
||||
doc["samplers"]["KSampler"].pop("prompt_overrides")
|
||||
doc["samplers"]["KSampler"]["fact_overrides"] = ["script", "sigmas"]
|
||||
sampler = self.load_doc(doc).samplers["KSampler"]
|
||||
self.assertEqual(sampler["fact_overrides"], ("script", "sigmas"))
|
||||
doc["samplers"]["KSampler"]["fact_overrides"] = ["script", "script"]
|
||||
with self.assertRaises(SemanticsError):
|
||||
self.load_doc(doc)
|
||||
doc["samplers"]["KSampler"].pop("fact_overrides")
|
||||
doc["samplers"]["KSampler"]["overrides"] = ["wildcard"]
|
||||
self.assertRejected(doc, "unknown field")
|
||||
|
||||
|
||||
@@ -2031,7 +2031,8 @@ class TestEncoderAndStringSemantics(unittest.TestCase):
|
||||
self.assertEqual(result["reason_code"], "malformed")
|
||||
|
||||
def test_unverified_text_nodes_stay_unsupported_and_named(self):
|
||||
for name in ("LoRAfromText", "RegexReplace", "XYZPrompt", "TextBoxMira"):
|
||||
# TextBoxMira was verified in its source in Task 2.7b and is listed.
|
||||
for name in ("LoRAfromText", "RegexReplace", "XYZPrompt"):
|
||||
with self.subTest(node=name):
|
||||
node = {"class_type": name, "inputs": {"text": parity.POS}}
|
||||
result = _inspect(_string_positive(node))
|
||||
|
||||
@@ -41,7 +41,7 @@ STRATEGIES: Mapping[str, FrozenSet[str]] = MappingProxyType(
|
||||
"conditioning": frozenset({"passthrough", "zeroed"}),
|
||||
"selectors": frozenset({"boolean-select"}),
|
||||
"composites": frozenset({"combine"}),
|
||||
"encoders": frozenset({"single", "parts-equal", "pm-compose"}),
|
||||
"encoders": frozenset({"single", "parts-equal", "pm-compose", "flag-parts"}),
|
||||
"strings": frozenset({"literal", "strip-newlines", "join-multi", "concat"}),
|
||||
"savers": frozenset({"comfy-counter", "vhs-counter"}),
|
||||
}
|
||||
@@ -54,7 +54,7 @@ SAVER_PATTERNS = MappingProxyType(
|
||||
"vhs-counter": "{name}_{counter:05}.{ext}",
|
||||
}
|
||||
)
|
||||
_SAMPLER_OPTIONS = frozenset({"prompt_overrides"})
|
||||
_SAMPLER_OPTIONS = frozenset({"prompt_overrides", "fact_overrides"})
|
||||
_CONFIGURATION_KINDS = frozenset({"noise", "sampler", "sigmas", "guider"})
|
||||
_SCALAR_KINDS = frozenset({"number", "string"})
|
||||
_TYPE_FIELDS = frozenset({"type_fields"})
|
||||
@@ -183,8 +183,10 @@ def _check_sampler(entry: dict, strategy: str, where: str) -> None:
|
||||
_name(entry[stage], where)
|
||||
# Inputs that, when set, replace the prompt the sampler's conditioning
|
||||
# inputs carry (a detailer's wildcard); the prompt is then not read.
|
||||
if "prompt_overrides" in entry:
|
||||
_names(entry["prompt_overrides"], where + ".prompt_overrides")
|
||||
# ``fact_overrides`` (read by comfy-metadata.js): inputs that, when set,
|
||||
# replace the settings the facts would read (a script or an XY plot).
|
||||
for option in sorted(_SAMPLER_OPTIONS & entry.keys()):
|
||||
_names(entry[option], f"{where}.{option}")
|
||||
keys = {"guider"} if strategy == "custom-advanced" else {"positive", "negative"}
|
||||
conditioning = entry["conditioning"]
|
||||
_fields(conditioning, where + ".conditioning", frozenset(keys), frozenset())
|
||||
@@ -246,6 +248,16 @@ def _check_conditioning(entry: dict, strategy: str, where: str) -> None:
|
||||
|
||||
|
||||
def _check_encoder(entry: dict, strategy: str, where: str) -> None:
|
||||
if strategy == "flag-parts":
|
||||
# A literal boolean ``flag`` false reads the first input; true needs
|
||||
# every input equal (smZ CLIPTextEncode's with_SDXL).
|
||||
_fields(entry, where, frozenset({"strategy", "inputs", "params"}), frozenset())
|
||||
params = entry["params"]
|
||||
if not isinstance(params, dict) or set(params) != {"flag"}:
|
||||
raise SemanticsError(f"{where}: params must be exactly ['flag']")
|
||||
_name(params["flag"], where + ".params")
|
||||
_names(entry["inputs"], where, minimum=2)
|
||||
return
|
||||
_fields(entry, where, frozenset({"strategy", "inputs"}), frozenset({"compose"}))
|
||||
_names(entry["inputs"], where, minimum=2 if strategy == "parts-equal" else 1)
|
||||
if "compose" in entry and not isinstance(entry["compose"], bool):
|
||||
|
||||
@@ -759,6 +759,8 @@ class _GraphResolver:
|
||||
names = entry["inputs"]
|
||||
if entry["strategy"] == "single":
|
||||
return self.text(inputs.get(names[0]), "string", depth + 1)
|
||||
if entry["strategy"] == "flag-parts":
|
||||
return self.flagged(node_id, entry, inputs, depth)
|
||||
if entry["strategy"] != "parts-equal":
|
||||
raise self.unsupported("Unsupported encoder strategy", node_id)
|
||||
parts = [self.text(inputs.get(name), "string", depth + 1) for name in names]
|
||||
@@ -771,6 +773,24 @@ class _GraphResolver:
|
||||
return self.joined_paths(filled, parts)
|
||||
raise self.named("Encoder parts have different texts", "composite", node_id)
|
||||
|
||||
def flagged(
|
||||
self, node_id: str, entry: Mapping, inputs: dict, depth: int
|
||||
) -> Tuple[str, list, str]:
|
||||
"""smZ CLIPTextEncode: ``text`` unless its SDXL flag may apply.
|
||||
|
||||
With the literal flag false the first input is encoded. With it true,
|
||||
the connected CLIP's model class decides at run time which inputs are
|
||||
read, so the prompt is known only when every input is the same text.
|
||||
"""
|
||||
names = entry["inputs"]
|
||||
if not self.setting(node_id, inputs, entry["params"]["flag"], bool):
|
||||
return self.text(inputs.get(names[0]), "string", depth + 1)
|
||||
parts = [self.text(inputs.get(name), "string", depth + 1) for name in names]
|
||||
self.scan(sum(len(part[0]) for part in parts))
|
||||
if len({part[0] for part in parts}) == 1:
|
||||
return self.joined_paths(parts, parts)
|
||||
raise self.named(_RUNTIME_REASON, "runtime-value", node_id)
|
||||
|
||||
def stringed(
|
||||
self, node_id: str, entry: Mapping, inputs: dict, depth: int
|
||||
) -> Tuple[str, list, str]:
|
||||
|
||||
@@ -417,6 +417,10 @@
|
||||
DetailerForEach: { facts: "ksampler", image_stage: "image" },
|
||||
UltimateSDUpscale: { facts: "ksampler", image_stage: "image" },
|
||||
UltimateSDUpscaleNoUpscale: { facts: "ksampler", image_stage: "upscaled_image" },
|
||||
// `fact_overrides`: inputs that, when set, replace the settings read here.
|
||||
ClownsharKSampler_Beta: { facts: "ksampler", fact_overrides: ["options", "sigmas"] },
|
||||
"KSampler (Efficient)": { facts: "ksampler", fact_overrides: ["script"] },
|
||||
"ttN KSampler_v2": { facts: "ksampler", fact_overrides: ["adv_xyPlot"] },
|
||||
});
|
||||
// Stages merged for one result: the resolver's candidate cap. Together with
|
||||
// MAX_VISITS per candidate walk these are deliberately tighter than the server's
|
||||
@@ -436,7 +440,9 @@
|
||||
"JoinStringMulti", "PromptSearchList", "PreviewImage", "PreviewAny", "SaveImageWebsocket",
|
||||
"Image Comparer (rgthree)", "DetailerForEach", "UltimateSDUpscale", "UltimateSDUpscaleNoUpscale",
|
||||
"ControlNetInpaintingAliMamaApply", "LTXVConditioning", "FluxKontextMultiReferenceLatentMethod", "Text to Conditioning",
|
||||
"TextInput", "DisplayText",
|
||||
"TextInput", "DisplayText", "ClownsharKSampler_Beta", "KSampler (Efficient)", "ttN KSampler_v2",
|
||||
"SeedVarianceEnhancer", "ApplyInstantIDAdvanced", "Switch conditioning [Crystools]", "smZ CLIPTextEncode", "ttN text",
|
||||
"CR Text", "CR Prompt Text", "TextBoxMira",
|
||||
]);
|
||||
// The table's `stage_inputs` (the resolver reads the same lists): inputs that make an
|
||||
// unlisted node a possible stage, and those that do so beside a linked `model`.
|
||||
@@ -456,6 +462,7 @@
|
||||
const MODEL_PASSTHROUGHS = new Set([
|
||||
"LoraLoader", "LoraLoaderModelOnly", "ModelSamplingFlux", "ModelSamplingAuraFlow", "ModelSamplingSD3",
|
||||
"Lora Loader (LoraManager)", "Power Lora Loader (rgthree)", "ModelPatchTorchSettings", "CFGNorm", "PerturbedAttentionGuidance",
|
||||
"CR Apply LoRA Stack",
|
||||
]);
|
||||
const CONFIGURATION_OUTPUTS = Object.freeze({
|
||||
RandomNoise: { kind: "noise", fields: ["noise_seed"] },
|
||||
@@ -579,7 +586,11 @@
|
||||
function samplerFacts(ctx, samplerId, source) {
|
||||
const sampler = ctx.graph[samplerId];
|
||||
const inputs = sampler.inputs;
|
||||
const shape = SAMPLERS[sampler.class_type].facts;
|
||||
const { facts: shape, fact_overrides: overrides = [] } = SAMPLERS[sampler.class_type];
|
||||
// A set override (a script, an XY plot, custom sigmas) replaces the settings below.
|
||||
if (overrides.some(name => Object.hasOwn(inputs, name) && inputs[name] !== null && inputs[name] !== "")) {
|
||||
return Object.fromEntries(FACT_FIELDS.map(name => [name, fact("unresolved", source, samplerId)]));
|
||||
}
|
||||
const advanced = shape === "custom-advanced";
|
||||
const custom = advanced || shape === "custom";
|
||||
let settings = inputs;
|
||||
|
||||
@@ -50,6 +50,30 @@
|
||||
"conditioning": { "positive": "positive", "negative": "negative" },
|
||||
"image_stage": "upscaled_image",
|
||||
"facts": "ksampler"
|
||||
},
|
||||
"ClownsharKSampler_Beta": {
|
||||
"strategy": "ksampler",
|
||||
"conditioning": { "positive": "positive", "negative": "negative" },
|
||||
"latent": "latent_image",
|
||||
"facts": "ksampler",
|
||||
"prompt_overrides": ["options"],
|
||||
"fact_overrides": ["options", "sigmas"]
|
||||
},
|
||||
"KSampler (Efficient)": {
|
||||
"strategy": "ksampler",
|
||||
"conditioning": { "positive": "positive", "negative": "negative" },
|
||||
"latent": "latent_image",
|
||||
"facts": "ksampler",
|
||||
"prompt_overrides": ["script"],
|
||||
"fact_overrides": ["script"]
|
||||
},
|
||||
"ttN KSampler_v2": {
|
||||
"strategy": "ksampler",
|
||||
"conditioning": { "positive": "positive", "negative": "negative" },
|
||||
"latent": "latent",
|
||||
"facts": "ksampler",
|
||||
"prompt_overrides": ["adv_xyPlot"],
|
||||
"fact_overrides": ["adv_xyPlot"]
|
||||
}
|
||||
},
|
||||
"guiders": {
|
||||
@@ -82,6 +106,9 @@
|
||||
{ "class": "LTXVConditioning", "slot": 0, "strategy": "passthrough", "input": "positive" },
|
||||
{ "class": "LTXVConditioning", "slot": 1, "strategy": "passthrough", "input": "negative" },
|
||||
{ "class": "FluxKontextMultiReferenceLatentMethod", "slot": 0, "strategy": "passthrough", "input": "conditioning" },
|
||||
{ "class": "SeedVarianceEnhancer", "slot": 0, "strategy": "passthrough", "input": "conditioning" },
|
||||
{ "class": "ApplyInstantIDAdvanced", "slot": 1, "strategy": "passthrough", "input": "positive" },
|
||||
{ "class": "ApplyInstantIDAdvanced", "slot": 2, "strategy": "passthrough", "input": "negative" },
|
||||
{ "class": "ConditioningZeroOut", "slot": 0, "strategy": "zeroed", "input": "conditioning" }
|
||||
],
|
||||
"selectors": {
|
||||
@@ -89,6 +116,11 @@
|
||||
"strategy": "boolean-select",
|
||||
"condition": "boolean",
|
||||
"branches": { "true": "on_true", "false": "on_false" }
|
||||
},
|
||||
"Switch conditioning [Crystools]": {
|
||||
"strategy": "boolean-select",
|
||||
"condition": "boolean",
|
||||
"branches": { "true": "on_true", "false": "on_false" }
|
||||
}
|
||||
},
|
||||
"composites": {
|
||||
@@ -106,12 +138,23 @@
|
||||
{ "class": "TextEncodeQwenImage21", "slot": 0, "strategy": "single", "inputs": ["prompt"] },
|
||||
{ "class": "TextEncodeQwenImage21", "slot": 1, "strategy": "single", "inputs": ["negative_prompt"] },
|
||||
{ "class": "KikoTextEncode", "slot": 0, "strategy": "single", "inputs": ["text"] },
|
||||
{ "class": "Text to Conditioning", "slot": 0, "strategy": "single", "inputs": ["text"] }
|
||||
{ "class": "Text to Conditioning", "slot": 0, "strategy": "single", "inputs": ["text"] },
|
||||
{
|
||||
"class": "smZ CLIPTextEncode",
|
||||
"slot": 0,
|
||||
"strategy": "flag-parts",
|
||||
"inputs": ["text", "text_g", "text_l"],
|
||||
"params": { "flag": "with_SDXL" }
|
||||
}
|
||||
],
|
||||
"strings": [
|
||||
{ "class": "StringConstant", "slot": 0, "strategy": "literal", "inputs": ["string"] },
|
||||
{ "class": "TextInput", "slot": 0, "strategy": "literal", "inputs": ["text"] },
|
||||
{ "class": "DisplayText", "slot": 0, "strategy": "literal", "inputs": ["text"] },
|
||||
{ "class": "ttN text", "slot": 0, "strategy": "literal", "inputs": ["text"] },
|
||||
{ "class": "CR Text", "slot": 0, "strategy": "literal", "inputs": ["text"] },
|
||||
{ "class": "CR Prompt Text", "slot": 0, "strategy": "literal", "inputs": ["prompt"] },
|
||||
{ "class": "TextBoxMira", "slot": 0, "strategy": "literal", "inputs": ["text"] },
|
||||
{
|
||||
"class": "StringConstantMultiline",
|
||||
"slot": 0,
|
||||
@@ -214,7 +257,8 @@
|
||||
"Power Lora Loader (rgthree)",
|
||||
"ModelPatchTorchSettings",
|
||||
"CFGNorm",
|
||||
"PerturbedAttentionGuidance"
|
||||
"PerturbedAttentionGuidance",
|
||||
"CR Apply LoRA Stack"
|
||||
],
|
||||
"scalar_outputs": {
|
||||
"PrimitiveInt": "number",
|
||||
|
||||
Reference in New Issue
Block a user