diff --git a/core/resolver.py b/core/resolver.py index b289cec..ea056c9 100644 --- a/core/resolver.py +++ b/core/resolver.py @@ -31,15 +31,13 @@ from .type_utils import ( prepare_remote_size_probe_url, unique_ordered_strings, ) +from .workflow.analysis import identify_missing_models from .workflow.inventory import get_workflow_model_inventory from .workflow.references import ( get_model_widget_category_hint, should_scan_as_model_reference, ) from .workflow.widgets import NESTED_MODEL_KEYS, NODE_TYPE_TO_CATEGORY_HINTS -from .workflow_analyzer import ( - identify_missing_models, -) from .workflow_updater import update_workflow_nodes # Regex patterns for URL extraction (matches HuggingFace and CivitAI URLs) diff --git a/core/workflow/analysis.py b/core/workflow/analysis.py index 43894c0..12a01db 100644 --- a/core/workflow/analysis.py +++ b/core/workflow/analysis.py @@ -464,3 +464,62 @@ def analyze_workflow_models( ) return all_model_refs + +def identify_missing_models( + workflow_models: List[Dict[str, Any]], available_models: List[Dict[str, str]] = None +) -> List[Dict[str, Any]]: + """ + Identify which models from the workflow are missing. + Deduplicates by category and filename - same model file only appears once even if + referenced by multiple nodes. + + Args: + workflow_models: List of model references from analyze_workflow_models + available_models: Optional list of available models (if None, checks via folder_paths) + + Returns: + List of missing model references (deduplicated by category and filename). + Each entry has 'all_node_refs' containing all node references for that model. + """ + # Group missing models by category and filename to deduplicate workflow + # references without merging unrelated model folders that happen to share a name. + missing_by_model: Dict[tuple[str, str], Dict[str, Any]] = {} + + for model_ref in workflow_models: + # If exists is False, it's missing + if not model_ref.get("exists", False): + filename = model_ref.get("original_path", "") + category = model_ref.get("category", "") + group_key = (str(category or ""), str(filename or "")) + + if group_key not in missing_by_model: + # First occurrence - use this as the primary entry + missing_by_model[group_key] = { + **model_ref, + "reference_count": 1, + "all_node_refs": [ + model_ref.copy() + ], # Track all nodes needing this model + } + else: + # Duplicate - just add to the node refs list + existing = missing_by_model[group_key] + existing["all_node_refs"].append(model_ref.copy()) + existing["reference_count"] = len(existing["all_node_refs"]) + if model_ref.get("auto_download_capable"): + existing["auto_download_capable"] = True + if model_ref.get("auto_download_candidate"): + existing["auto_download_candidate"] = True + if model_ref.get("input_choice_matches_value"): + existing["input_choice_matches_value"] = True + existing_source = str(existing.get("input_choice_source") or "").lower() + model_source = str(model_ref.get("input_choice_source") or "").lower() + if existing_source != "hybrid" and model_source in { + "static", + "hybrid", + "workflow_schema", + }: + existing["input_choice_source"] = model_source + + # Return deduplicated list + return list(missing_by_model.values()) diff --git a/tests/test_workflow_analyzer.py b/tests/test_workflow_analyzer.py index 1631d0f..af99c03 100644 --- a/tests/test_workflow_analyzer.py +++ b/tests/test_workflow_analyzer.py @@ -5,16 +5,14 @@ from types import SimpleNamespace from unittest.mock import patch from core import resolver as resolver_core -from core import workflow_analyzer from core.scanner import invalidate_model_files_cache, scan_directory from core.workflow import analysis, dynamic_widgets, references -from core.workflow.analysis import analyze_workflow_models +from core.workflow.analysis import analyze_workflow_models, identify_missing_models from core.workflow.dynamic_widgets import get_lora_model_strength from core.workflow.inventory import ( get_workflow_model_inventory, invalidate_workflow_model_inventory_cache, ) -from core.workflow_analyzer import identify_missing_models def _workflow_with_model(model_path):