diff --git a/core/workflow_analyzer.py b/core/workflow_analyzer.py index 0d56ec0..7b6d6a4 100644 --- a/core/workflow_analyzer.py +++ b/core/workflow_analyzer.py @@ -1012,17 +1012,11 @@ def get_model_widget_category_hints( dynamic_category_hints = get_dynamic_widget_category_hints(node, widget_index) output_category_hint = get_node_output_category_hint(node) - widgets_values = node.get("widgets_values", []) - has_single_widget_value = len(widgets_values) == 1 output_widget_category_hint = ( output_category_hint if ( not dynamic_category_hints - and ( - indexed_category_hint - or widget_category_hint - or has_single_widget_value - ) + and (indexed_category_hint or widget_category_hint) ) else None ) @@ -1068,13 +1062,16 @@ def is_placeholder_model_value(value: Any) -> bool: def should_scan_as_model_reference(value: Any, declared_model_widget: bool) -> bool: - """Detect model references, including extensionless known model widgets.""" - if is_model_filename(value): + """Detect references only when the value or its widget provides model evidence.""" + if isinstance(value, str) and URN_REGEX.match(value.strip()): return True if not declared_model_widget or not isinstance(value, str): return False + if is_model_filename(value): + return True + return bool(value.strip()) and not is_placeholder_model_value(value) @@ -1268,8 +1265,8 @@ def get_node_model_info( """ Extract model references from a single node. - This scans all widgets_values entries and tries to identify which ones - are model file references by attempting to resolve them. + This scans widget values backed by model metadata or a model-like widget name. + Explicit model URNs are also accepted without widget metadata. Args: node: Node dictionary from workflow JSON @@ -1454,11 +1451,19 @@ def get_node_model_info( if model_widget_category_hints else ([effective_category_hint] if effective_category_hint else None) ) + category_backed_model_widget = bool( + model_widget_category_hint + or schema_output_category_hint + ) + named_model_file_widget = bool( + is_workflow_model_widget_candidate(node, idx) + and is_model_filename(value) + ) if not should_scan_as_model_reference( value, - declared_model_widget=bool( - model_widget_category_hint or schema_output_category_hint + declared_model_widget=( + category_backed_model_widget or named_model_file_widget ), ): # Check for dict-type widget values containing model references (e.g. Power Lora Loader) diff --git a/tests/test_workflow_analyzer.py b/tests/test_workflow_analyzer.py index cef743b..430f644 100644 --- a/tests/test_workflow_analyzer.py +++ b/tests/test_workflow_analyzer.py @@ -80,6 +80,135 @@ class WorkflowAnalyzerCaseSensitivityTests(unittest.TestCase): class WorkflowAnalyzerCategoryHintTests(unittest.TestCase): + def test_show_anything_cached_model_text_is_not_a_model_reference(self): + with tempfile.TemporaryDirectory() as tmpdir: + model_dir = os.path.join(tmpdir, "KREA2") + os.makedirs(model_dir) + model_path = os.path.join( + model_dir, "krea2_turbo_fp8_scaled.safetensors" + ) + with open(model_path, "wb"): + pass + + workflow = { + "nodes": [ + { + "id": 141, + "type": "easy showAnything", + "title": "Diffusion model", + "inputs": [{"name": "anything", "type": "*", "link": 700}], + "outputs": [{"name": "output", "type": "*", "links": []}], + "widgets": [{"name": "text"}], + "widgets_values": [ + r"KREA2\krea2_turbo_fp8_scaled.safetensors" + ], + } + ] + } + available_models = [ + { + "filename": "krea2_turbo_fp8_scaled.safetensors", + "path": model_path, + "relative_path": r"KREA2\krea2_turbo_fp8_scaled.safetensors", + "category": "diffusion_models", + "base_directory": tmpdir, + } + ] + + refs = analyze_workflow_models( + workflow, available_models=available_models + ) + + self.assertEqual([], refs) + + def test_model_like_widget_name_keeps_custom_model_reference_detection(self): + workflow = { + "nodes": [ + { + "id": 199, + "type": "CustomModelSelector", + "widgets": [{"name": "filename"}], + "widgets_values": ["custom_model.safetensors"], + "outputs": [], + } + ] + } + + refs = analyze_workflow_models(workflow, available_models=[]) + + self.assertEqual(1, len(refs)) + self.assertEqual("custom_model.safetensors", refs[0]["original_path"]) + + def test_set_node_model_slot_name_does_not_turn_constant_into_model(self): + workflow = { + "nodes": [ + { + "id": 3, + "type": "SetNode", + "title": "Set_MODEL_BASE", + "inputs": [{"name": "MODEL", "type": "MODEL", "link": 701}], + "outputs": [{"name": "MODEL", "type": "MODEL", "links": []}], + "widgets": [{"name": "Constant"}], + "widgets_values": ["MODEL_BASE"], + }, + { + "id": 19, + "type": "SetNode", + "title": "Set_MODEL_LORA", + "inputs": [{"name": "MODEL", "type": "MODEL", "link": 702}], + "outputs": [{"name": "MODEL", "type": "MODEL", "links": []}], + "widgets": [{"name": "Constant"}], + "widgets_values": ["MODEL_LORA"], + }, + ] + } + + refs = analyze_workflow_models(workflow, available_models=[]) + + self.assertEqual([], refs) + + def test_model_slot_does_not_turn_device_widget_into_model(self): + workflow = { + "nodes": [ + { + "id": 104, + "type": "CustomVideoUpscaler", + "inputs": [ + {"name": "model", "type": "MODEL", "link": 703}, + { + "name": "offload_device", + "type": "COMBO", + "widget": {"name": "offload_device"}, + }, + ], + "outputs": [{"name": "IMAGE", "type": "IMAGE", "links": []}], + "widgets": [{"name": "offload_device"}], + "widgets_values": ["cuda:0"], + } + ] + } + + refs = analyze_workflow_models(workflow, available_models=[]) + + self.assertEqual([], refs) + + def test_filename_widget_template_without_model_extension_is_ignored(self): + workflow = { + "nodes": [ + { + "id": 155, + "type": "Image Saver", + "widgets": [{"name": "filename"}], + "widgets_values": ["Krea2_%time_%seed"], + "outputs": [], + } + ] + } + + refs = analyze_workflow_models(workflow, available_models=[]) + + self.assertEqual([], refs) + def test_upscale_model_loader_widget_named_model_name_stays_upscale(self): workflow = { "nodes": [