diff --git a/core/workflow_analyzer.py b/core/workflow_analyzer.py index 3f3c392..6f11658 100644 --- a/core/workflow_analyzer.py +++ b/core/workflow_analyzer.py @@ -2044,7 +2044,7 @@ def identify_missing_models( ) -> List[Dict[str, Any]]: """ Identify which models from the workflow are missing. - Deduplicates by filename - same model file only appears once even if + Deduplicates by category and filename - same model file only appears once even if referenced by multiple nodes. Args: @@ -2052,29 +2052,34 @@ def identify_missing_models( available_models: Optional list of available models (if None, checks via folder_paths) Returns: - List of missing model references (deduplicated by filename). + 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 filename to deduplicate - missing_by_filename: Dict[str, Dict[str, Any]] = {} + # 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 filename not in missing_by_filename: + if group_key not in missing_by_model: # First occurrence - use this as the primary entry - missing_by_filename[filename] = { + 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_filename[filename] + 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"): @@ -2091,4 +2096,4 @@ def identify_missing_models( existing["input_choice_source"] = model_source # Return deduplicated list - return list(missing_by_filename.values()) + return list(missing_by_model.values()) diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index bfb7174..65c5eb9 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -44,7 +44,8 @@ export const resolveDownloadMethods = { subgraph_id: ref.subgraph_id, is_top_level: ref.is_top_level, is_lora_v2: ref.is_lora_v2, - original_lora_name: ref.name || ref.original_path + original_lora_name: ref.name || ref.original_path, + nested_key: ref.nested_key })); const data = await this.fetchJson('/model_resolver/resolve', { @@ -104,24 +105,42 @@ export const resolveDownloadMethods = { // Collect all 100% matches const resolutions = []; + const seenResolutionKeys = new Set(); for (const missing of missingModels) { const matches = missing.matches || []; const perfectMatch = matches.find((m) => m.confidence === 100); if (perfectMatch && perfectMatch.model) { - resolutions.push({ - missing_key: this.getMissingModelKey?.(missing), - missing_search_key: this.getMissingSearchKey?.(missing), - node_id: missing.node_id, - widget_index: missing.widget_index, - resolved_path: perfectMatch.model.path, - category: missing.category, - resolved_model: perfectMatch.model, - subgraph_id: missing.subgraph_id, // Include subgraph_id for subgraph nodes - is_top_level: missing.is_top_level, // True for top-level nodes, False for nodes in subgraph definitions - is_lora_v2: missing.is_lora_v2, - original_lora_name: missing.name || missing.original_path - }); + const nodeRefs = Array.isArray(missing.all_node_refs) && missing.all_node_refs.length + ? missing.all_node_refs + : [missing]; + for (const ref of nodeRefs) { + const resolutionKey = [ + ref.node_id ?? '', + ref.widget_index ?? '', + ref.subgraph_id || '', + ref.is_top_level !== false ? 'T' : 'F', + ref.nested_key || '', + ref.name || ref.original_path || '' + ].join(':'); + if (seenResolutionKeys.has(resolutionKey)) continue; + seenResolutionKeys.add(resolutionKey); + + resolutions.push({ + missing_key: this.getMissingModelKey?.(missing), + missing_search_key: this.getMissingSearchKey?.(missing), + node_id: ref.node_id, + widget_index: ref.widget_index, + resolved_path: perfectMatch.model.path, + category: ref.category || missing.category, + resolved_model: perfectMatch.model, + subgraph_id: ref.subgraph_id, // Include subgraph_id for subgraph nodes + is_top_level: ref.is_top_level, // True for top-level nodes, False for nodes in subgraph definitions + is_lora_v2: ref.is_lora_v2, + original_lora_name: ref.name || ref.original_path, + nested_key: ref.nested_key + }); + } } } diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index 9cb84d0..de6cc3e 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -27,6 +27,15 @@ export const missingBrowserMethods = { }; }, + getMissingReferenceCount(missing = {}) { + if (Array.isArray(missing.all_node_refs) && missing.all_node_refs.length > 0) { + return missing.all_node_refs.length; + } + + const count = Number(missing.reference_count || 0); + return Number.isFinite(count) && count > 0 ? count : 1; + }, + getMissingNodeDisplay(missing = {}) { const locateTarget = this.getMissingLocateTarget(missing); const isSubgraphNode = locateTarget.nodeType && locateTarget.nodeType.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); @@ -45,20 +54,29 @@ export const missingBrowserMethods = { : (missing.promoted_inner_node_title || missing.promoted_inner_node_type || ''); const customNodeTitle = String(promotedDetail || locateTarget.nodeTitle || missing.node_title || '').trim(); const hasCustomNodeTitle = customNodeTitle && customNodeTitle !== nodeLabel; - const text = hasCustomNodeTitle + const baseText = hasCustomNodeTitle ? `${nodeLabel} #${nodeId} ยท ${customNodeTitle}` : `${nodeLabel} #${nodeId}`; + const referenceCount = this.getMissingReferenceCount(missing); + const extraReferenceCount = Math.max(0, referenceCount - 1); + const text = extraReferenceCount > 0 + ? `${baseText} + ${extraReferenceCount} ref${extraReferenceCount === 1 ? '' : 's'}` + : baseText; + const locateTooltip = missing.locate_via_promoted_widget + ? 'Center this subgraph node in the ComfyUI graph.' + : locateTarget.isTopLevel === false + ? 'Open this subgraph and center the node in the ComfyUI graph.' + : 'Center this node in the ComfyUI graph.'; return { label: nodeLabel, text, canLocate: nodeId !== '', locateTarget, - locateTooltip: missing.locate_via_promoted_widget - ? 'Center this subgraph node in the ComfyUI graph.' - : locateTarget.isTopLevel === false - ? 'Open this subgraph and center the node in the ComfyUI graph.' - : 'Center this node in the ComfyUI graph.' + referenceCount, + locateTooltip: extraReferenceCount > 0 + ? `${locateTooltip} This missing model is used by ${referenceCount} workflow references; this centers the first one.` + : locateTooltip }; },