fix: deduplicate missing models by category+filename and expand per-node resolutions
Previously, identify_missing_models grouped missing entries only by filename, which could merge unrelated models from different categories that happen to share a filename. This change deduplicates by (category, filename) and tracks all_node_refs with a reference_count so the frontend can preserve one resolution per node reference instead of collapsing duplicates. Frontend changes in resolve_download_methods.js iterate over all_node_refs, and missing_browser_methods.js now shows '+ N refs' in the missing-model label and clarifies the locate tooltip when multiple workflow references share the same missing model.
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user