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",