From 46aeff92caa05e82c4366ddf32c8ddd290b9a49b Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 19 Oct 2026 14:23:17 -0700 Subject: [PATCH] feat(inspect): port legacy parser rules the new resolver missed (installed nodes) R6 and R9 for nodes whose sources are installed beside ComfyUI, each entry's input names and output slots read from the node's code (the task report lists file and line): - detailer stages UltimateSDUpscale (image), UltimateSDUpscaleNoUpscale (upscaled_image) and DetailerForEach (image), judged with their upstream samplers like FaceDetailer; - pass-throughs ControlNetInpaintingAliMamaApply and LTXVConditioning (positive 0, negative 1) and FluxKontextMultiReferenceLatentMethod; - encoder Text to Conditioning, strings TextInput and DisplayText; - facts model pass-throughs Lora Loader (LoraManager), Power Lora Loader (rgthree), ModelPatchTorchSettings, CFGNorm, PerturbedAttentionGuidance. Impact Pack encodes a detailer's non-empty wildcard instead of its positive input, so FaceDetailer and DetailerForEach now list it as a prompt override: a literal wildcard is decided at run time and a linked one names its source, instead of resolving to the positive input. The overwritten PromptManager refusal (R1) still holds behind the new pass-throughs. The default profile does not read the table. --- tests/js/inspection-legacy-ports.test.js | 39 +++++ tests/test_inspection_legacy_ports.py | 175 +++++++++++++++++++++++ tests/test_inspection_semantics.py | 75 ++++++++++ tests/test_prompt_pair_resolver.py | 12 +- utils/inspection_semantics.py | 7 +- utils/prompt_pair_resolver.py | 16 +++ web/js/comfy-metadata.js | 12 +- web/js/inspection-semantics.json | 37 ++++- 8 files changed, 364 insertions(+), 9 deletions(-) diff --git a/tests/js/inspection-legacy-ports.test.js b/tests/js/inspection-legacy-ports.test.js index 04283c3..f0d1c22 100644 --- a/tests/js/inspection-legacy-ports.test.js +++ b/tests/js/inspection-legacy-ports.test.js @@ -57,3 +57,42 @@ test("R10: commas inside quoted settings values do not split them", () => { // An unbalanced quote is not a quoted value: the historical split, as A1111 reads it. assert.deepEqual(parameterFacts(`a cat\n${FOOTER}, Note: "open, Model: base`).model, ["known", "base"]); }); + +function graphFacts(graph, resolution) { + const facts = extractGenerationFacts({ + prompt_graph: { status: "parsed", value: graph }, + workflow_graph: { status: "absent", value: null }, + metadataStatus: "available", + resolution, + }); + return Object.fromEntries(fields.map(field => [field, [facts[field].status, facts[field].value]])); +} + +function modelGraph(between) { + return { + ckpt: { class_type: "CheckpointLoaderSimple", inputs: { ckpt_name: "base.safetensors" } }, + hop: { class_type: between, inputs: { model: ["ckpt", 0], clip: ["ckpt", 1] } }, + s: { class_type: "KSampler", inputs: { seed: 5, steps: 20, cfg: 7, sampler_name: "euler", scheduler: "normal", model: ["hop", 0] } }, + }; +} + +test("R9: model pass-throughs verified in their sources lead to the loader", () => { + for (const name of ["Lora Loader (LoraManager)", "Power Lora Loader (rgthree)", "ModelPatchTorchSettings", "CFGNorm", "PerturbedAttentionGuidance"]) { + assert.deepEqual(graphFacts(modelGraph(name)).model, ["known", "base.safetensors"], name); + // Only output 0 is the model. + const other = modelGraph(name); other.s.inputs.model = ["hop", 1]; + assert.deepEqual(graphFacts(other).model, ["unresolved", null], name); + } + assert.deepEqual(graphFacts(modelGraph("ModelMergeSDXL")).model, ["unresolved", null]); +}); + +test("R6: upscale and SEGS detailers supply stage facts like FaceDetailer", () => { + for (const [class_type, image] of [["UltimateSDUpscale", "image"], ["UltimateSDUpscaleNoUpscale", "upscaled_image"], ["DetailerForEach", "image"]]) { + const graph = modelGraph("CFGNorm"); + graph.decode = { class_type: "VAEDecode", inputs: { samples: ["s", 0], vae: ["ckpt", 2] } }; + graph.d = { class_type, inputs: { [image]: ["decode", 0], model: ["hop", 0], seed: 7, steps: 12, cfg: 7, sampler_name: "euler", scheduler: "normal" } }; + assert.deepEqual(graphFacts(graph, { state: "resolved", sampler_id: "d" }).seed, ["known", "7"], class_type); + const merged = graphFacts(graph, { state: "ambiguous", sampler_id: "d" }); + assert.deepEqual([merged.seed[0], merged.steps[0], merged.cfg], ["ambiguous", "ambiguous", ["known", "7"]], class_type); + } +}); diff --git a/tests/test_inspection_legacy_ports.py b/tests/test_inspection_legacy_ports.py index 35268b7..1651624 100644 --- a/tests/test_inspection_legacy_ports.py +++ b/tests/test_inspection_legacy_ports.py @@ -450,5 +450,180 @@ class TestBatchIndexDuplicates(unittest.TestCase): read_inspection_file(path) +def _f02(**changes) -> dict: + """``f02`` (KSampler 3, encoders 6 and 7) with nodes added or replaced.""" + prompt = parity.build("f02")["prompt"] + prompt.update(changes) + return prompt + + +def _stage(class_type: str, image_input: str, positive=("6", 0), **inputs) -> dict: + """``f19``'s shape with ``class_type`` as the detailer stage (node 50).""" + prompt = parity.build("f19")["prompt"] + if "upscale_model" in inputs: + prompt["12"] = {"class_type": "UpscaleModelLoader", "inputs": {}} + if "segs" in inputs: + prompt["13"] = {"class_type": "SegsDetector", "inputs": {"image": ["8", 0]}} + prompt["50"] = { + "class_type": class_type, + "inputs": { + image_input: ["8", 0], + "model": ["4", 0], + "vae": ["4", 2], + "positive": list(positive), + "negative": ["7", 0], + "seed": 7, + "steps": 12, + "cfg": 4.5, + "sampler_name": "euler", + "scheduler": "normal", + "denoise": 0.4, + **inputs, + }, + } + return prompt + + +DETAILERS = ( + ("UltimateSDUpscale", "image", {"upscale_model": ["12", 0], "upscale_by": 2}), + ("UltimateSDUpscaleNoUpscale", "upscaled_image", {}), + ("DetailerForEach", "image", {"segs": ["13", 0], "wildcard": ""}), +) + + +class TestInstalledNodeSemantics(unittest.TestCase): + """R6 entries verified against the installed node sources (see the report).""" + + def test_upscale_and_segs_detailers_are_stages(self): + for class_type, image, extra in DETAILERS: + with self.subTest(node=class_type): + same = _inspect({"prompt": _stage(class_type, image, **extra)}) + self.assertEqual(same["state"], "resolved") + self.assertEqual(same["sampler_id"], "50") + self.assertEqual(_texts(same), (parity.POS, parity.NEG)) + other = _inspect( + {"prompt": _stage(class_type, image, ("10", 0), **extra)} + ) + self.assertEqual(other["state"], "ambiguous") + self.assertEqual(other["reason_code"], "multistage-differs") + self.assertEqual( + sorted(item["sampler_id"] for item in other["candidates"]), + ["3", "50"], + ) + + def test_a_detailer_wildcard_replaces_its_prompt(self): + # Impact Pack core.py: a non-empty wildcard is encoded instead of positive. + for class_type in ("DetailerForEach", "FaceDetailer"): + with self.subTest(node=class_type): + result = _inspect( + {"prompt": _stage(class_type, "image", wildcard="a smiling face")} + ) + self.assertEqual(result["state"], "unresolved") + self.assertEqual(result["reason_code"], "runtime-value") + self.assertEqual(result["node"], {"id": "50", "class_type": class_type}) + linked = _stage(class_type, "image", wildcard=["14", 0]) + linked["14"] = {"class_type": "WildcardSource", "inputs": {}} + result = _inspect({"prompt": linked}) + self.assertEqual(result["reason_code"], "unsupported-node") + self.assertEqual( + result["node"], {"id": "14", "class_type": "WildcardSource"} + ) + for empty in ("", None): + result = _inspect( + {"prompt": _stage(class_type, "image", wildcard=empty)} + ) + self.assertEqual(result["state"], "resolved", empty) + + def test_conditioning_passthroughs_keep_each_role(self): + pair = {"positive": ["6", 0], "negative": ["7", 0]} + cases = { + "ControlNetInpaintingAliMamaApply": ( + { + **pair, + "control_net": ["11", 0], + "vae": ["4", 2], + "image": ["12", 0], + "mask": ["12", 1], + "strength": 1.0, + }, + 0, + 1, + ), + "LTXVConditioning": ({**pair, "frame_rate": 25.0}, 0, 1), + "FluxKontextMultiReferenceLatentMethod": ( + {"conditioning": ["6", 0], "reference_latents_method": "offset"}, + 0, + None, + ), + } + for class_type, (inputs, positive, negative) in cases.items(): + with self.subTest(node=class_type): + prompt = _f02(**{"20": {"class_type": class_type, "inputs": inputs}}) + prompt["3"]["inputs"]["positive"] = ["20", positive] + if negative is not None: + prompt["3"]["inputs"]["negative"] = ["20", negative] + result = _inspect({"prompt": prompt}) + self.assertEqual(result["state"], "resolved") + self.assertEqual(_texts(result), (parity.POS, parity.NEG)) + self.assertEqual( + result["positive"]["provenance"]["path"][0]["node_id"], "20" + ) + if negative is None: + prompt["3"]["inputs"]["negative"] = ["20", 1] + self.assertEqual( + _inspect({"prompt": prompt})["reason_code"], "unsupported-node" + ) + + def test_text_nodes(self): + prompt = _f02( + **{ + "6": { + "class_type": "Text to Conditioning", + "inputs": {"clip": ["4", 1], "text": ["21", 0]}, + }, + "21": {"class_type": "DisplayText", "inputs": {"text": ["22", 0]}}, + "22": {"class_type": "TextInput", "inputs": {"text": parity.POS}}, + } + ) + result = _inspect({"prompt": prompt}) + self.assertEqual(_texts(result), (parity.POS, parity.NEG)) + self.assertEqual( + [step["node_id"] for step in result["positive"]["provenance"]["path"]], + ["6", "21", "22"], + ) + # Text to Conditioning has one output; TextInput reads only ``text``. + prompt["3"]["inputs"]["positive"] = ["6", 1] + self.assertEqual( + _inspect({"prompt": prompt})["reason_code"], "unsupported-node" + ) + + def test_overwritten_text_behind_a_new_passthrough_is_still_refused(self): + # R1 before R6: the pass-through must not let overwritten text resolve. + metadata = _overwritten() + metadata["prompt"]["20"] = { + "class_type": "FluxKontextMultiReferenceLatentMethod", + "inputs": {"conditioning": ["17", 0], "reference_latents_method": "offset"}, + } + metadata["prompt"]["3"]["inputs"]["positive"] = ["20", 0] + result = _inspect(metadata) + self.assertEqual(result["reason"], resolver.OVERWRITTEN_PROMPT_REASON) + + def test_default_profile_keeps_them_unsupported(self): + for class_type, image, extra in DETAILERS: + with self.subTest(node=class_type): + result = resolve_prompt_pair( + {"prompt": _stage(class_type, image, **extra)} + ) + # The historical rule: the one KSampler, whatever reads its image. + self.assertEqual(result["sampler_id"], "3") + prompt = _f02( + **{"6": {"class_type": "Text to Conditioning", "inputs": {"text": "a"}}} + ) + self.assertEqual( + resolve_prompt_pair({"prompt": prompt})["reason"], + "Unsupported node semantics or output slot", + ) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_inspection_semantics.py b/tests/test_inspection_semantics.py index 583fa2c..d3caa7a 100644 --- a/tests/test_inspection_semantics.py +++ b/tests/test_inspection_semantics.py @@ -125,6 +125,7 @@ class SpecEntriesTest(unittest.TestCase): }, ) # D19: FaceDetailer is a stage over its upstream image, ksampler-shaped facts. + # A non-empty wildcard replaces the detail prompt (Impact core.py:267). self.assertEqual( table.samplers["FaceDetailer"], { @@ -132,6 +133,7 @@ class SpecEntriesTest(unittest.TestCase): "conditioning": pair, "image_stage": "image", "facts": "ksampler", + "prompt_overrides": ("wildcard",), }, ) @@ -310,6 +312,55 @@ class SpecEntriesTest(unittest.TestCase): for name in ("PromptManager", "PromptManagerPair", "PromptManagerText"): self.assertNotIn(name, table.all_classes) + def test_legacy_parser_ports_are_listed_with_their_source_inputs(self): + # Task 2.7b (R6): input names and slots read from each node's source; + # the report lists the file and line for every one. + table = load_semantics() + pair = {"positive": "positive", "negative": "negative"} + for name, image in ( + ("DetailerForEach", "image"), + ("UltimateSDUpscale", "image"), + ("UltimateSDUpscaleNoUpscale", "upscaled_image"), + ): + entry = dict(table.samplers[name]) + self.assertEqual( + entry.pop("prompt_overrides", None) is not None, + name == "DetailerForEach", + name, + ) + self.assertEqual( + entry, + { + "strategy": "detailer-stage", + "conditioning": pair, + "image_stage": image, + "facts": "ksampler", + }, + name, + ) + for (name, slot), source in { + ("ControlNetInpaintingAliMamaApply", 0): "positive", + ("ControlNetInpaintingAliMamaApply", 1): "negative", + ("LTXVConditioning", 0): "positive", + ("LTXVConditioning", 1): "negative", + ("FluxKontextMultiReferenceLatentMethod", 0): "conditioning", + }.items(): + self.assertEqual( + table.conditioning[(name, slot)], + {"strategy": "passthrough", "input": source}, + ) + self.assertNotIn( + ("FluxKontextMultiReferenceLatentMethod", 1), table.conditioning + ) + self.assertEqual( + table.encoders[("Text to Conditioning", 0)], + {"strategy": "single", "inputs": ("text",)}, + ) + for name in ("TextInput", "DisplayText"): + self.assertEqual( + table.strings[(name, 0)], {"strategy": "literal", "inputs": ("text",)} + ) + def test_stage_inputs_are_the_one_source_for_both_stage_walks(self): # Review I-2: Python's resolver and comfy-metadata.js read these lists here. stage_inputs = load_semantics().stage_inputs @@ -374,6 +425,12 @@ class SpecEntriesTest(unittest.TestCase): "ModelSamplingFlux", "ModelSamplingAuraFlow", "ModelSamplingSD3", + # Task 2.7b (R9): model in, model out, verified in their sources. + "Lora Loader (LoraManager)", + "Power Lora Loader (rgthree)", + "ModelPatchTorchSettings", + "CFGNorm", + "PerturbedAttentionGuidance", ), ) self.assertEqual( @@ -654,5 +711,23 @@ def _depth(value, level=1): return max([level] + [_depth(item, level + 1) for item in children]) +class SamplerOptionsTest(_TableCase): + """Task 2.7b: optional sampler lists are input names, or the table fails.""" + + def test_prompt_overrides_are_input_names(self): + doc = _minimal() + doc["samplers"]["KSampler"]["prompt_overrides"] = ["wildcard"] + sampler = self.load_doc(doc).samplers["KSampler"] + self.assertEqual(sampler["prompt_overrides"], ("wildcard",)) + for value in ([], "wildcard", [""], ["a", "a"], [1]): + with self.subTest(value=value): + doc["samplers"]["KSampler"]["prompt_overrides"] = value + with self.assertRaises(SemanticsError): + self.load_doc(doc) + doc["samplers"]["KSampler"].pop("prompt_overrides") + doc["samplers"]["KSampler"]["overrides"] = ["wildcard"] + self.assertRejected(doc, "unknown field") + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_prompt_pair_resolver.py b/tests/test_prompt_pair_resolver.py index fdc5166..ebcb137 100644 --- a/tests/test_prompt_pair_resolver.py +++ b/tests/test_prompt_pair_resolver.py @@ -2826,7 +2826,9 @@ class TestLatentAncestry(unittest.TestCase): { "50": {"class_type": "LoadImage", "inputs": {"image": "a.png"}}, "60": { - "class_type": "UltimateSDUpscale", + # Task 2.7b lists UltimateSDUpscale; an unlisted upscaler + # of the same shape keeps this rule's coverage. + "class_type": "MysteryUpscaler", "inputs": { "image": ["50", 0], "model": ["4", 0], @@ -2838,9 +2840,11 @@ class TestLatentAncestry(unittest.TestCase): ) result = _inspect(source, output_node_id="9") self.assertEqual(result["reason_code"], "unsupported-node") - self.assertEqual( - result["node"], {"id": "60", "class_type": "UltimateSDUpscale"} - ) + self.assertEqual(result["node"], {"id": "60", "class_type": "MysteryUpscaler"}) + # The listed stage is now an earlier sampler stage in the ancestry. + source["60"]["class_type"] = "UltimateSDUpscale" + result = _inspect(source, output_node_id="9") + self.assertEqual(result["reason"], resolver.MULTISTAGE_UNATTRIBUTED_REASON) def test_latent_walk_budget_exhaustion_is_work_limit(self): # 2,100 distinct images in the latent ancestry: more than one diff --git a/utils/inspection_semantics.py b/utils/inspection_semantics.py index 24ba0c6..8905be9 100644 --- a/utils/inspection_semantics.py +++ b/utils/inspection_semantics.py @@ -54,6 +54,7 @@ SAVER_PATTERNS = MappingProxyType( "vhs-counter": "{name}_{counter:05}.{ext}", } ) +_SAMPLER_OPTIONS = frozenset({"prompt_overrides"}) _CONFIGURATION_KINDS = frozenset({"noise", "sampler", "sigmas", "guider"}) _SCALAR_KINDS = frozenset({"number", "string"}) _TYPE_FIELDS = frozenset({"type_fields"}) @@ -177,9 +178,13 @@ def _check_sampler(entry: dict, strategy: str, where: str) -> None: entry, where, frozenset({"strategy", "conditioning", stage, "facts"}), - frozenset(), + _SAMPLER_OPTIONS, ) _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") keys = {"guider"} if strategy == "custom-advanced" else {"positive", "negative"} conditioning = entry["conditioning"] _fields(conditioning, where + ".conditioning", frozenset(keys), frozenset()) diff --git a/utils/prompt_pair_resolver.py b/utils/prompt_pair_resolver.py index b8ec3fa..5e2b29a 100644 --- a/utils/prompt_pair_resolver.py +++ b/utils/prompt_pair_resolver.py @@ -1241,6 +1241,7 @@ class _GraphResolver: try: name, inputs = self.node(sampler_id) entry = self.sampler_entry(name) + self.check_overrides(sampler_id, entry, inputs) # A second sampler in the latent ancestry denotes a multistage run; # associating the final image with one prompt pair would be a guess. latent = entry.get("latent") @@ -1284,6 +1285,21 @@ class _GraphResolver: node=error.node, ) + def check_overrides(self, sampler_id: str, entry: Mapping, inputs: dict) -> None: + """A set prompt override replaces the conditioning inputs' prompt. + + Exact ``""`` (or a missing input) leaves the prompt alone. A literal is + processed at run time (a detailer wildcard); a link is a source the + inspector does not follow, so it is named. + """ + for name in entry.get("prompt_overrides", ()): + value = inputs.get(name) + if value is None or value == "": + continue + if _link(value): + raise self.unsupported("Unsupported prompt override", str(value[0])) + raise self.named(_RUNTIME_REASON, "runtime-value", sampler_id) + def latent_source(self, value: Any, seen: set, depth: int) -> None: if not _link(value) or depth >= self.max_depth: code = "malformed" if not _link(value) else "depth-limit" diff --git a/web/js/comfy-metadata.js b/web/js/comfy-metadata.js index d2fdbf6..6fe331d 100644 --- a/web/js/comfy-metadata.js +++ b/web/js/comfy-metadata.js @@ -414,6 +414,9 @@ SamplerCustom: { facts: "custom" }, SamplerCustomAdvanced: { facts: "custom-advanced" }, FaceDetailer: { facts: "ksampler", image_stage: "image" }, + DetailerForEach: { facts: "ksampler", image_stage: "image" }, + UltimateSDUpscale: { facts: "ksampler", image_stage: "image" }, + UltimateSDUpscaleNoUpscale: { facts: "ksampler", image_stage: "upscaled_image" }, }); // 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 @@ -431,7 +434,9 @@ "CLIPTextEncode", "CLIPTextEncodeSDXL", "CLIPTextEncodeFlux", "CLIPTextEncodeSD3", "TextEncodeQwenImageEdit", "TextEncodeQwenImageEditPlus", "TextEncodeQwenImage21", "KikoTextEncode", "StringConstant", "StringConstantMultiline", "JoinStringMulti", "PromptSearchList", "PreviewImage", "PreviewAny", "SaveImageWebsocket", - "Image Comparer (rgthree)", + "Image Comparer (rgthree)", "DetailerForEach", "UltimateSDUpscale", "UltimateSDUpscaleNoUpscale", + "ControlNetInpaintingAliMamaApply", "LTXVConditioning", "FluxKontextMultiReferenceLatentMethod", "Text to Conditioning", + "TextInput", "DisplayText", ]); // 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`. @@ -448,7 +453,10 @@ CheckpointLoader: "ckpt_name", CheckpointLoaderSimple: "ckpt_name", unCLIPCheckpointLoader: "ckpt_name", UNETLoader: "unet_name", }); - const MODEL_PASSTHROUGHS = new Set(["LoraLoader", "LoraLoaderModelOnly", "ModelSamplingFlux", "ModelSamplingAuraFlow", "ModelSamplingSD3"]); + const MODEL_PASSTHROUGHS = new Set([ + "LoraLoader", "LoraLoaderModelOnly", "ModelSamplingFlux", "ModelSamplingAuraFlow", "ModelSamplingSD3", + "Lora Loader (LoraManager)", "Power Lora Loader (rgthree)", "ModelPatchTorchSettings", "CFGNorm", "PerturbedAttentionGuidance", + ]); const CONFIGURATION_OUTPUTS = Object.freeze({ RandomNoise: { kind: "noise", fields: ["noise_seed"] }, KSamplerSelect: { kind: "sampler", fields: ["sampler_name"] }, diff --git a/web/js/inspection-semantics.json b/web/js/inspection-semantics.json index d16dfdf..183ce13 100644 --- a/web/js/inspection-semantics.json +++ b/web/js/inspection-semantics.json @@ -29,6 +29,26 @@ "strategy": "detailer-stage", "conditioning": { "positive": "positive", "negative": "negative" }, "image_stage": "image", + "facts": "ksampler", + "prompt_overrides": ["wildcard"] + }, + "DetailerForEach": { + "strategy": "detailer-stage", + "conditioning": { "positive": "positive", "negative": "negative" }, + "image_stage": "image", + "facts": "ksampler", + "prompt_overrides": ["wildcard"] + }, + "UltimateSDUpscale": { + "strategy": "detailer-stage", + "conditioning": { "positive": "positive", "negative": "negative" }, + "image_stage": "image", + "facts": "ksampler" + }, + "UltimateSDUpscaleNoUpscale": { + "strategy": "detailer-stage", + "conditioning": { "positive": "positive", "negative": "negative" }, + "image_stage": "upscaled_image", "facts": "ksampler" } }, @@ -57,6 +77,11 @@ { "class": "InpaintModelConditioning", "slot": 1, "strategy": "passthrough", "input": "negative" }, { "class": "WanImageToVideo", "slot": 0, "strategy": "passthrough", "input": "positive" }, { "class": "WanImageToVideo", "slot": 1, "strategy": "passthrough", "input": "negative" }, + { "class": "ControlNetInpaintingAliMamaApply", "slot": 0, "strategy": "passthrough", "input": "positive" }, + { "class": "ControlNetInpaintingAliMamaApply", "slot": 1, "strategy": "passthrough", "input": "negative" }, + { "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": "ConditioningZeroOut", "slot": 0, "strategy": "zeroed", "input": "conditioning" } ], "selectors": { @@ -80,10 +105,13 @@ { "class": "TextEncodeQwenImageEditPlus", "slot": 0, "strategy": "single", "inputs": ["prompt"] }, { "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": "KikoTextEncode", "slot": 0, "strategy": "single", "inputs": ["text"] }, + { "class": "Text to Conditioning", "slot": 0, "strategy": "single", "inputs": ["text"] } ], "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": "StringConstantMultiline", "slot": 0, @@ -181,7 +209,12 @@ "LoraLoaderModelOnly", "ModelSamplingFlux", "ModelSamplingAuraFlow", - "ModelSamplingSD3" + "ModelSamplingSD3", + "Lora Loader (LoraManager)", + "Power Lora Loader (rgthree)", + "ModelPatchTorchSettings", + "CFGNorm", + "PerturbedAttentionGuidance" ], "scalar_outputs": { "PrimitiveInt": "number",