From 5bc6d8286c70a3e0ff3d42537daee885e86a1ec9 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Wed, 12 Aug 2026 19:18:46 +0800 Subject: [PATCH] fix(metadata): exclude scalar fields from conditioning provenance inputs --- py/metadata_collector/node_extractors.py | 32 +++-- .../test_metadata_collector.py | 128 ++++++++++++++++++ 2 files changed, 148 insertions(+), 12 deletions(-) diff --git a/py/metadata_collector/node_extractors.py b/py/metadata_collector/node_extractors.py index c1dbdc00..4352d441 100644 --- a/py/metadata_collector/node_extractors.py +++ b/py/metadata_collector/node_extractors.py @@ -82,11 +82,7 @@ class GenericNodeExtractor(NodeMetadataExtractor): text = val.strip() break - input_conditionings = [ - value - for input_name, value in inputs.items() - if input_name.startswith("conditioning") and value is not None - ] + input_conditionings = _collect_conditioning_inputs(inputs) if text or input_conditionings: prompt_metadata = _ensure_prompt_metadata(metadata, node_id) if text: @@ -434,6 +430,24 @@ def _first_output_tuple(outputs): return None +def _collect_conditioning_inputs(inputs): + """Collect conditioning object inputs (``conditioning*`` keys). + + Primitive values (None, str, int, float, bool) are excluded so scalar + fields like ``conditioning_strength`` are not mistaken for conditioning + objects during provenance tracking. + """ + if not inputs: + return [] + return [ + value + for input_name, value in inputs.items() + if input_name.startswith("conditioning") + and value is not None + and not isinstance(value, (str, int, float, bool)) + ] + + def _record_conditioning_source( metadata, node_id, output_conditioning, input_conditionings ): @@ -525,13 +539,7 @@ class ConditioningCombineExtractor(NodeMetadataExtractor): if not inputs: return - input_conditionings = [] - for input_name in inputs: - if ( - input_name.startswith("conditioning") - and inputs[input_name] is not None - ): - input_conditionings.append(inputs[input_name]) + input_conditionings = _collect_conditioning_inputs(inputs) if input_conditionings: prompt_metadata = _ensure_prompt_metadata(metadata, node_id) diff --git a/tests/metadata_collector/test_metadata_collector.py b/tests/metadata_collector/test_metadata_collector.py index fe6aaf20..3ace3a2f 100644 --- a/tests/metadata_collector/test_metadata_collector.py +++ b/tests/metadata_collector/test_metadata_collector.py @@ -637,6 +637,134 @@ def test_conditioning_provenance_recovers_transformed_switched_prompts( assert params["negative_prompt"] == "expected negative" +def test_conditioning_provenance_identity_switch_between_encoders( + metadata_registry, monkeypatch +): + """Lock identity-preserving switches placed directly between encoders. + + A switch returns the selected input conditioning verbatim, so provenance + must be recovered through object identity without any transform metadata. + """ + prompt_graph = { + "encode_pos": { + "class_type": "CLIPTextEncode", + "inputs": {"text": "chosen positive", "clip": ["clip", 0]}, + }, + "encode_other_pos": { + "class_type": "CLIPTextEncode", + "inputs": {"text": "unchosen positive", "clip": ["clip", 0]}, + }, + "positive_switch": { + "class_type": "ComfySwitchNode", + "inputs": { + "switch": True, + "on_false": ["encode_other_pos", 0], + "on_true": ["encode_pos", 0], + }, + }, + "sampler": { + "class_type": "ClownsharKSampler_Beta", + "inputs": { + "seed": 123, + "steps": 8, + "cfg": 1.0, + "sampler_name": "linear/euler", + "scheduler": "beta57", + "denoise": 1.0, + "positive": ["positive_switch", 0], + "negative": ["encode_other_pos", 0], + "latent_image": { + "samples": types.SimpleNamespace(shape=(1, 4, 16, 16)) + }, + }, + }, + } + prompt = SimpleNamespace(original_prompt=prompt_graph) + + chosen_conditioning = object() + unchosen_conditioning = object() + + monkeypatch.setattr(metadata_processor, "standalone_mode", False) + + metadata_registry.start_collection("prompt-identity-switch") + metadata_registry.set_current_prompt(prompt) + + metadata_registry.record_node_execution( + "encode_pos", "CLIPTextEncode", {"text": "chosen positive"}, None + ) + metadata_registry.update_node_execution( + "encode_pos", "CLIPTextEncode", [(chosen_conditioning,)] + ) + metadata_registry.record_node_execution( + "encode_other_pos", "CLIPTextEncode", {"text": "unchosen positive"}, None + ) + metadata_registry.update_node_execution( + "encode_other_pos", "CLIPTextEncode", [(unchosen_conditioning,)] + ) + metadata_registry.record_node_execution( + "positive_switch", + "ComfySwitchNode", + { + "switch": True, + "on_false": unchosen_conditioning, + "on_true": chosen_conditioning, + }, + None, + ) + metadata_registry.update_node_execution( + "positive_switch", "ComfySwitchNode", [(chosen_conditioning,)] + ) + metadata_registry.record_node_execution( + "sampler", + "ClownsharKSampler_Beta", + { + "seed": 123, + "steps": 8, + "cfg": 1.0, + "sampler_name": "linear/euler", + "scheduler": "beta57", + "denoise": 1.0, + "positive": chosen_conditioning, + "negative": unchosen_conditioning, + "latent_image": { + "samples": types.SimpleNamespace(shape=(1, 4, 16, 16)) + }, + }, + None, + ) + + metadata = metadata_registry.get_metadata("prompt-identity-switch") + params = MetadataProcessor.extract_generation_params(metadata) + + assert params["prompt"] == "chosen positive" + assert params["negative_prompt"] == "unchosen positive" + + +def test_conditioning_provenance_ignores_scalar_conditioning_fields( + metadata_registry, monkeypatch +): + """Scalar fields like ``conditioning_strength`` must not be collected as + conditioning objects for unregistered transform nodes.""" + monkeypatch.setattr(metadata_processor, "standalone_mode", False) + + metadata_registry.start_collection("prompt-scalar-filter") + metadata_registry.set_current_prompt(SimpleNamespace(original_prompt={})) + + input_conditioning = object() + metadata_registry.record_node_execution( + "strength_node", + "SomeStrengthTransform", + {"conditioning": input_conditioning, "conditioning_strength": 0.8}, + None, + return_types=("CONDITIONING",), + ) + + metadata = metadata_registry.get_metadata("prompt-scalar-filter") + assert metadata[PROMPTS]["strength_node"]["orig_conditionings"] == [ + input_conditioning + ] + + def test_conditioning_provenance_recovers_kj_set_get_prompts( metadata_registry, monkeypatch ):