feat: improve category resolution and missing models browser
- Expand preferred categories to include clip_vision, ipadapter, sams, ultralytics
- Add clip_gguf alias for text_encoders in both backend and frontend
- Add explicit diffusion_models/text_encoders mappings in CATEGORY_MAP
- Refactor get_model_roots to skip custom_nodes/configs, use template key
aliases, accept new folder keys from known categories, and normalize
ultralytics paths (yolo/bbox, yolo/segm, yolo -> parent)
- Pass missing object through resolve_download_methods and search_panel
so category selection can use widget hints
- Refactor frontend download target resolution:
- getMissingSupportedDownloadCategories respects widget/type hints
- getNodeTypeDownloadCategory splits exact-node map and adds
normalizeNodeCategoryToken / getNodeTypeDownloadCategoryFromTokens
for GGUF and future node families
- getSourceResultDownloadCategory and _sourceSupportLevel accept
missing for unified logic
- Prioritize dynamic widget category hints in workflow analyzer and
include category_hints/folder_key_hints on all model refs
- Add folder count badges and path normalization in missing browser
(normalizeMissingFolderRootPath handles category suffixes / bbox/segm)
This commit is contained in:
+81
-10
@@ -113,6 +113,7 @@ class ModelResolverExtension:
|
||||
)
|
||||
from .core.type_utils import first_non_empty, to_int, to_bool
|
||||
from .core.settings import (
|
||||
TEMPLATE_KEY_ALIASES,
|
||||
bool_setting as resolver_bool_setting,
|
||||
get_default_root_for_category,
|
||||
load_settings as load_resolver_settings,
|
||||
@@ -3542,7 +3543,7 @@ class ModelResolverExtension:
|
||||
import os
|
||||
import folder_paths
|
||||
|
||||
categories = [
|
||||
preferred_categories = [
|
||||
"loras",
|
||||
"checkpoints",
|
||||
"diffusion_models",
|
||||
@@ -3550,24 +3551,81 @@ class ModelResolverExtension:
|
||||
"text_encoders",
|
||||
"vae",
|
||||
"upscale_models",
|
||||
"controlnet",
|
||||
"clip_vision",
|
||||
"ipadapter",
|
||||
"sams",
|
||||
"ultralytics",
|
||||
]
|
||||
skip_categories = {"custom_nodes", "configs"}
|
||||
known_categories = set(folder_paths.folder_names_and_paths.keys())
|
||||
categories = []
|
||||
category_source_keys = {}
|
||||
for cat in [
|
||||
*preferred_categories,
|
||||
*known_categories,
|
||||
]:
|
||||
folder_key = normalize_download_category(cat)
|
||||
if (
|
||||
not folder_key
|
||||
or folder_key in skip_categories
|
||||
or folder_key in categories
|
||||
):
|
||||
if folder_key and folder_key not in skip_categories:
|
||||
category_source_keys.setdefault(folder_key, []).append(cat)
|
||||
continue
|
||||
categories.append(folder_key)
|
||||
category_source_keys.setdefault(folder_key, []).append(cat)
|
||||
|
||||
roots = {}
|
||||
settings = load_resolver_settings()
|
||||
comfy_root = get_comfy_root_path(folder_paths)
|
||||
for cat in categories:
|
||||
folder_key = normalize_download_category(cat)
|
||||
candidate_keys = [folder_key]
|
||||
if folder_key == "diffusion_models":
|
||||
candidate_keys.append("unet")
|
||||
elif folder_key == "text_encoders":
|
||||
candidate_keys.append("clip")
|
||||
if folder_key in known_categories:
|
||||
raw_candidate_keys = [folder_key]
|
||||
else:
|
||||
raw_candidate_keys = [
|
||||
folder_key,
|
||||
*TEMPLATE_KEY_ALIASES.get(folder_key, ()),
|
||||
*category_source_keys.get(folder_key, []),
|
||||
]
|
||||
if folder_key == "ultralytics":
|
||||
raw_candidate_keys = [
|
||||
candidate_key
|
||||
for candidate_key in raw_candidate_keys
|
||||
if str(candidate_key or "").strip().lower() != "yolo"
|
||||
]
|
||||
|
||||
candidate_keys = []
|
||||
for candidate_key in raw_candidate_keys:
|
||||
if (
|
||||
candidate_key
|
||||
and candidate_key in known_categories
|
||||
and candidate_key not in candidate_keys
|
||||
):
|
||||
candidate_keys.append(candidate_key)
|
||||
paths = []
|
||||
for candidate_key in candidate_keys:
|
||||
paths.extend(folder_paths.get_folder_paths(candidate_key) or [])
|
||||
preferred_directory = (
|
||||
get_default_root_for_category(folder_key, settings)
|
||||
or get_download_directory(folder_key)
|
||||
or ""
|
||||
if folder_key == "ultralytics":
|
||||
normalized_ultralytics_paths = []
|
||||
for path in paths:
|
||||
normalized_path = os.path.normpath(str(path or ""))
|
||||
basename = os.path.basename(normalized_path).lower()
|
||||
if basename == "yolo":
|
||||
continue
|
||||
parent_dir = os.path.dirname(normalized_path)
|
||||
if (
|
||||
basename in {"bbox", "segm"}
|
||||
and os.path.basename(parent_dir).lower()
|
||||
== "ultralytics"
|
||||
):
|
||||
normalized_path = parent_dir
|
||||
normalized_ultralytics_paths.append(normalized_path)
|
||||
paths = normalized_ultralytics_paths
|
||||
preferred_directory = get_default_root_for_category(
|
||||
folder_key, settings
|
||||
)
|
||||
normalized_paths = dedupe_local_base_directories(
|
||||
paths,
|
||||
@@ -3576,6 +3634,19 @@ class ModelResolverExtension:
|
||||
)
|
||||
roots[folder_key] = normalized_paths
|
||||
|
||||
for raw_key in known_categories:
|
||||
if (
|
||||
not raw_key
|
||||
or raw_key in skip_categories
|
||||
or raw_key in roots
|
||||
):
|
||||
continue
|
||||
raw_paths = folder_paths.get_folder_paths(raw_key) or []
|
||||
roots[raw_key] = dedupe_local_base_directories(
|
||||
raw_paths,
|
||||
comfy_root=comfy_root,
|
||||
)
|
||||
|
||||
return web.json_response(roots)
|
||||
|
||||
@routes.get("/model_resolver/path-template-suggestions")
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ TEMPLATE_KEY_ALIASES = {
|
||||
"checkpoints": ("checkpoints", "checkpoint"),
|
||||
"embeddings": ("embeddings", "embedding"),
|
||||
"diffusion_models": ("diffusion_models", "diffusion_model", "unet"),
|
||||
"text_encoders": ("text_encoders", "text_encoder", "clip", "clips"),
|
||||
"text_encoders": ("text_encoders", "text_encoder", "clip", "clips", "clip_gguf"),
|
||||
"controlnet": ("controlnet", "control_net"),
|
||||
"vae": ("vae",),
|
||||
"upscale_models": ("upscale_models", "upscale_model", "upscaler"),
|
||||
|
||||
@@ -141,6 +141,14 @@ CATEGORY_MAP = {
|
||||
"style_models": "style_models",
|
||||
"gligen": "gligen",
|
||||
"diffusers": "diffusers",
|
||||
"unet": "diffusion_models",
|
||||
"diffusion_model": "diffusion_models",
|
||||
"diffusion_models": "diffusion_models",
|
||||
"clip": "text_encoders",
|
||||
"clips": "text_encoders",
|
||||
"clip_gguf": "text_encoders",
|
||||
"text_encoder": "text_encoders",
|
||||
"text_encoders": "text_encoders",
|
||||
"vae_approx": "vae_approx",
|
||||
"sam": "sams",
|
||||
"sam_model": "sams",
|
||||
|
||||
+35
-10
@@ -714,9 +714,7 @@ def get_model_widget_category_hints(
|
||||
node_type = node.get("type", "")
|
||||
indexed_category_hint = get_node_model_widget_category_hint(node_type, widget_index)
|
||||
widget_category_hint = get_widget_category_hint(node, widget_index)
|
||||
dynamic_category_hints: List[str] = []
|
||||
if not indexed_category_hint and not widget_category_hint:
|
||||
dynamic_category_hints = get_dynamic_widget_category_hints(node, widget_index)
|
||||
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", [])
|
||||
@@ -724,15 +722,24 @@ def get_model_widget_category_hints(
|
||||
output_widget_category_hint = (
|
||||
output_category_hint
|
||||
if (
|
||||
indexed_category_hint
|
||||
or widget_category_hint
|
||||
or dynamic_category_hints
|
||||
or has_single_widget_value
|
||||
not dynamic_category_hints
|
||||
and (
|
||||
indexed_category_hint
|
||||
or widget_category_hint
|
||||
or has_single_widget_value
|
||||
)
|
||||
)
|
||||
else None
|
||||
)
|
||||
|
||||
hints_list = []
|
||||
for h in dynamic_category_hints:
|
||||
if h:
|
||||
hints_list.append(h)
|
||||
|
||||
if dynamic_category_hints:
|
||||
return _ordered_unique_categories(hints_list)
|
||||
|
||||
if indexed_category_hint:
|
||||
hints_list.append(indexed_category_hint)
|
||||
|
||||
@@ -744,9 +751,6 @@ def get_model_widget_category_hints(
|
||||
else:
|
||||
if widget_category_hint:
|
||||
hints_list.append(widget_category_hint)
|
||||
for h in dynamic_category_hints:
|
||||
if h:
|
||||
hints_list.append(h)
|
||||
if output_widget_category_hint:
|
||||
hints_list.append(output_widget_category_hint)
|
||||
|
||||
@@ -1092,6 +1096,8 @@ def get_node_model_info(
|
||||
"active": active,
|
||||
"node_title": node_title,
|
||||
"category": "loras",
|
||||
"category_hints": ["loras"],
|
||||
"folder_key_hints": ["loras"],
|
||||
"full_path": lora_full_path,
|
||||
"exists": lora_exists,
|
||||
"is_urn": False,
|
||||
@@ -1104,6 +1110,7 @@ def get_node_model_info(
|
||||
# For each widget value, check if it looks like a model file or URN
|
||||
for idx, value in enumerate(widgets_values):
|
||||
widget_name = get_widget_name_hint(node, idx)
|
||||
model_widget_folder_key_hints = get_dynamic_widget_category_hints(node, idx)
|
||||
model_widget_category_hints = get_model_widget_category_hints(node, idx)
|
||||
model_widget_category_hint = (
|
||||
model_widget_category_hints[0] if model_widget_category_hints else None
|
||||
@@ -1161,6 +1168,8 @@ def get_node_model_info(
|
||||
"original_path": value_str,
|
||||
"node_title": node_title,
|
||||
"category": category,
|
||||
"category_hints": nested_categories or ([category] if category else []),
|
||||
"folder_key_hints": nested_categories or ([category] if category else []),
|
||||
"full_path": full_path,
|
||||
"exists": exists,
|
||||
"is_urn": False,
|
||||
@@ -1192,6 +1201,16 @@ def get_node_model_info(
|
||||
effective_category_hint
|
||||
or URN_TYPE_MAP.get(typ.lower(), "unknown")
|
||||
)
|
||||
urn_category_hints = (
|
||||
categories_to_try_for_widget
|
||||
if categories_to_try_for_widget
|
||||
else ([category] if category else [])
|
||||
)
|
||||
urn_folder_key_hints = (
|
||||
model_widget_folder_key_hints
|
||||
if model_widget_folder_key_hints
|
||||
else urn_category_hints
|
||||
)
|
||||
|
||||
model_refs.append(
|
||||
{
|
||||
@@ -1210,6 +1229,8 @@ def get_node_model_info(
|
||||
},
|
||||
"node_title": node_title,
|
||||
"category": category,
|
||||
"category_hints": urn_category_hints,
|
||||
"folder_key_hints": urn_folder_key_hints,
|
||||
"full_path": None,
|
||||
"exists": False,
|
||||
"is_urn": True,
|
||||
@@ -1242,6 +1263,10 @@ def get_node_model_info(
|
||||
"original_path": value_str,
|
||||
"node_title": node_title,
|
||||
"category": category,
|
||||
"category_hints": categories_to_try_for_widget or ([category] if category else []),
|
||||
"folder_key_hints": model_widget_folder_key_hints
|
||||
or categories_to_try_for_widget
|
||||
or ([category] if category else []),
|
||||
"full_path": full_path,
|
||||
"exists": exists,
|
||||
"is_urn": False,
|
||||
|
||||
@@ -1282,6 +1282,32 @@
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.mr-node-folder-count-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(114, 211, 138, 0.28);
|
||||
background: rgba(114, 211, 138, 0.105);
|
||||
color: #c9f6d0;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
cursor: help;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mr-node-folder-count-badge:hover,
|
||||
.mr-node-folder-count-badge:focus-visible {
|
||||
border-color: rgba(114, 211, 138, 0.46);
|
||||
background: rgba(114, 211, 138, 0.16);
|
||||
color: #e6ffe9;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.mr-missing-browser .mr-node-chip.is-locatable:hover {
|
||||
background: var(--mr-missing-chip-bg-hover, rgba(255, 255, 255, 0.06));
|
||||
border-color: var(--mr-missing-chip-border-hover, rgba(255, 255, 255, 0.18));
|
||||
@@ -6257,7 +6283,8 @@ a.mr-image-preview-resource-name:hover {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
white-space: normal;
|
||||
white-space: pre-line;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: left;
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.32);
|
||||
pointer-events: none;
|
||||
|
||||
@@ -2297,7 +2297,8 @@ export const resolveDownloadMethods = {
|
||||
const archiveName = civarchiveResult.name || archiveFilename || 'Model';
|
||||
const archiveCategory = this.getSourceResultDownloadCategory?.(
|
||||
civarchiveResult,
|
||||
this.getMissingDownloadCategory?.(missing, 'checkpoints') || 'checkpoints'
|
||||
this.getMissingDownloadCategory?.(missing, 'checkpoints') || 'checkpoints',
|
||||
missing
|
||||
) || this.getMissingDownloadCategory?.(missing, 'checkpoints') || 'checkpoints';
|
||||
const archiveSecondary = [
|
||||
archiveName && archiveName !== archiveFilename ? archiveFilename : '',
|
||||
@@ -2332,7 +2333,8 @@ export const resolveDownloadMethods = {
|
||||
const archiveName = loraManagerArchiveResult.name || archiveFilename;
|
||||
const archiveCategory = this.getSourceResultDownloadCategory?.(
|
||||
loraManagerArchiveResult,
|
||||
this.getMissingDownloadCategory?.(missing, 'loras') || 'loras'
|
||||
this.getMissingDownloadCategory?.(missing, 'loras') || 'loras',
|
||||
missing
|
||||
) || this.getMissingDownloadCategory?.(missing, 'loras') || 'loras';
|
||||
addRow({
|
||||
sourceKey: 'lora-archive',
|
||||
@@ -2364,7 +2366,8 @@ export const resolveDownloadMethods = {
|
||||
const modelName = civitaiResult.name || missing.civitai_info?.model_name || downloadFilename || 'Model';
|
||||
const civitaiCategory = this.getSourceResultDownloadCategory?.(
|
||||
civitaiResult,
|
||||
this.getMissingDownloadCategory?.(missing, 'checkpoints') || 'checkpoints'
|
||||
this.getMissingDownloadCategory?.(missing, 'checkpoints') || 'checkpoints',
|
||||
missing
|
||||
) || this.getMissingDownloadCategory?.(missing, 'checkpoints') || 'checkpoints';
|
||||
const civitaiSecondary = [
|
||||
civitaiResult.type || '',
|
||||
|
||||
@@ -106,6 +106,7 @@ export const downloadTargetMethods = {
|
||||
diffusion_models: 'diffusion_models',
|
||||
clip: 'text_encoders',
|
||||
clips: 'text_encoders',
|
||||
clip_gguf: 'text_encoders',
|
||||
text_encoder: 'text_encoders',
|
||||
text_encoders: 'text_encoders',
|
||||
ip_adapter: 'ipadapter',
|
||||
@@ -137,19 +138,127 @@ export const downloadTargetMethods = {
|
||||
].map(key => this.normalizeDownloadCategory(key)).filter(Boolean));
|
||||
},
|
||||
|
||||
getSourceResultDownloadCategory(source = {}, fallbackCategory = '') {
|
||||
getMissingSupportedDownloadCategories(missing = {}) {
|
||||
const knownCategories = this.getKnownDownloadCategorySet();
|
||||
const collectCategories = (...values) => {
|
||||
const categories = [];
|
||||
const addCategory = (value) => {
|
||||
if (value === undefined || value === null || String(value).trim() === '') return;
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(addCategory);
|
||||
return;
|
||||
}
|
||||
String(value).split(/[,|;]/).forEach(part => {
|
||||
const normalized = this.normalizeDownloadCategory(part);
|
||||
if (normalized && knownCategories.has(normalized) && !categories.includes(normalized)) {
|
||||
categories.push(normalized);
|
||||
}
|
||||
});
|
||||
};
|
||||
values.forEach(addCategory);
|
||||
return categories;
|
||||
};
|
||||
const chooseSupported = (supported = []) => {
|
||||
if (!supported.length) return [];
|
||||
const nodeCategory = this.getMissingNodeTypeDownloadCategory(missing);
|
||||
if (nodeCategory && supported.includes(nodeCategory)) {
|
||||
return [nodeCategory];
|
||||
}
|
||||
return supported;
|
||||
};
|
||||
|
||||
const widgetSupported = collectCategories(
|
||||
missing.folder_key_hints,
|
||||
missing.folderKeyHints,
|
||||
missing.model_widget_folder_key_hints,
|
||||
missing.modelWidgetFolderKeyHints,
|
||||
missing.model_widget_category_hints,
|
||||
missing.modelWidgetCategoryHints
|
||||
);
|
||||
if (widgetSupported.length) {
|
||||
return chooseSupported(widgetSupported);
|
||||
}
|
||||
|
||||
const supported = collectCategories(
|
||||
missing.category_hints,
|
||||
missing.categoryHints,
|
||||
missing.supported_categories,
|
||||
missing.supportedCategories
|
||||
);
|
||||
if (supported.length) {
|
||||
return chooseSupported(supported);
|
||||
}
|
||||
|
||||
const fallbackSupported = [];
|
||||
const addCategory = (value) => {
|
||||
if (value === undefined || value === null || String(value).trim() === '') return;
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(addCategory);
|
||||
return;
|
||||
}
|
||||
String(value).split(/[,|;]/).forEach(part => {
|
||||
const normalized = this.normalizeDownloadCategory(part);
|
||||
if (normalized && knownCategories.has(normalized) && !fallbackSupported.includes(normalized)) {
|
||||
fallbackSupported.push(normalized);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
addCategory(this.getMissingNodeTypeDownloadCategory(missing));
|
||||
if (fallbackSupported.length) {
|
||||
return fallbackSupported;
|
||||
}
|
||||
|
||||
addCategory(missing.category);
|
||||
addCategory(missing.directory);
|
||||
|
||||
return fallbackSupported;
|
||||
},
|
||||
|
||||
getSourceResultDownloadCategory(source = {}, fallbackCategory = '', missing = null) {
|
||||
const sourceData = source && typeof source === 'object' ? source : {};
|
||||
const knownCategories = this.getKnownDownloadCategorySet();
|
||||
const candidates = [
|
||||
const fallback = this.normalizeDownloadCategory(fallbackCategory || '');
|
||||
const supportedCategories = missing
|
||||
? this.getMissingSupportedDownloadCategories(missing)
|
||||
: [];
|
||||
const sourceCandidates = [
|
||||
sourceData.model_type,
|
||||
sourceData.modelType,
|
||||
sourceData.type,
|
||||
sourceData.category,
|
||||
sourceData.directory,
|
||||
fallbackCategory
|
||||
sourceData.directory
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
for (const candidate of sourceCandidates) {
|
||||
if (candidate === undefined || candidate === null || String(candidate).trim() === '') continue;
|
||||
const normalized = this.normalizeDownloadCategory(candidate);
|
||||
if (
|
||||
normalized
|
||||
&& knownCategories.has(normalized)
|
||||
&& supportedCategories.includes(normalized)
|
||||
) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
fallback
|
||||
&& knownCategories.has(fallback)
|
||||
&& (!supportedCategories.length || supportedCategories.includes(fallback))
|
||||
) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (supportedCategories.length) {
|
||||
return supportedCategories[0];
|
||||
}
|
||||
|
||||
if (fallback && knownCategories.has(fallback)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
for (const candidate of sourceCandidates) {
|
||||
if (candidate === undefined || candidate === null || String(candidate).trim() === '') continue;
|
||||
const normalized = this.normalizeDownloadCategory(candidate);
|
||||
if (normalized && knownCategories.has(normalized)) {
|
||||
@@ -163,7 +272,7 @@ export const downloadTargetMethods = {
|
||||
: this.normalizeDownloadCategory(fallbackCategory || 'checkpoints');
|
||||
},
|
||||
|
||||
getNodeTypeDownloadCategory(nodeType = '') {
|
||||
getExactNodeTypeDownloadCategory(nodeType = '') {
|
||||
const normalizedNodeType = String(nodeType || '').trim();
|
||||
const exactMap = {
|
||||
CheckpointLoaderSimple: 'checkpoints',
|
||||
@@ -181,13 +290,30 @@ export const downloadTargetMethods = {
|
||||
CLIPLoader: 'text_encoders',
|
||||
DualCLIPLoader: 'text_encoders',
|
||||
TripleCLIPLoader: 'text_encoders',
|
||||
CLIPLoaderGGUF: 'text_encoders',
|
||||
ClipLoaderGGUF: 'text_encoders',
|
||||
DualCLIPLoaderGGUF: 'text_encoders',
|
||||
DualClipLoaderGGUF: 'text_encoders',
|
||||
TripleCLIPLoaderGGUF: 'text_encoders',
|
||||
TripleClipLoaderGGUF: 'text_encoders',
|
||||
QuadrupleCLIPLoader: 'text_encoders',
|
||||
QuadrupleClipLoader: 'text_encoders',
|
||||
QuadrupleCLIPLoaderGGUF: 'text_encoders',
|
||||
QuadrupleClipLoaderGGUF: 'text_encoders',
|
||||
UpscaleModelLoader: 'upscale_models',
|
||||
LTXVAudioVAELoader: 'checkpoints',
|
||||
LowVRAMAudioVAELoader: 'checkpoints'
|
||||
};
|
||||
if (exactMap[normalizedNodeType]) {
|
||||
return exactMap[normalizedNodeType];
|
||||
}
|
||||
return exactMap[normalizedNodeType] || '';
|
||||
},
|
||||
|
||||
getNodeTypeDownloadCategory(nodeType = '') {
|
||||
const normalizedNodeType = String(nodeType || '').trim();
|
||||
const exactCategory = this.getExactNodeTypeDownloadCategory(normalizedNodeType);
|
||||
if (exactCategory) return exactCategory;
|
||||
|
||||
const tokenCategory = this.getNodeTypeDownloadCategoryFromTokens(normalizedNodeType);
|
||||
if (tokenCategory) return tokenCategory;
|
||||
|
||||
const token = normalizedNodeType.toLowerCase();
|
||||
if (!token) return '';
|
||||
@@ -202,6 +328,53 @@ export const downloadTargetMethods = {
|
||||
return '';
|
||||
},
|
||||
|
||||
normalizeNodeCategoryToken(value = '') {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '');
|
||||
},
|
||||
|
||||
getNodeTypeDownloadCategoryFromTokens(nodeType = '') {
|
||||
const nodeToken = this.normalizeNodeCategoryToken(nodeType);
|
||||
if (!nodeToken) return '';
|
||||
|
||||
const ignoredTokens = new Set(['model', 'models', 'loader', 'provider']);
|
||||
const candidates = [];
|
||||
const seen = new Set();
|
||||
const addCandidate = (category, tokenValue) => {
|
||||
const token = this.normalizeNodeCategoryToken(tokenValue);
|
||||
if (!token || token.length < 3 || ignoredTokens.has(token)) return;
|
||||
const key = `${category}:${token}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
candidates.push({ category, token });
|
||||
};
|
||||
const addTokenParts = (category, tokenValue) => {
|
||||
String(tokenValue || '').split(/[_\s-]+/).forEach(part => addCandidate(category, part));
|
||||
};
|
||||
|
||||
this.getDefaultDownloadCategoryKeys().forEach(rawCategory => {
|
||||
const category = this.normalizeDownloadCategory(rawCategory);
|
||||
if (!category) return;
|
||||
const tokenName = this.getCategoryTokenName(category);
|
||||
addCandidate(category, category);
|
||||
addCandidate(category, category.replace(/s$/, ''));
|
||||
addCandidate(category, tokenName);
|
||||
addTokenParts(category, category);
|
||||
addTokenParts(category, tokenName);
|
||||
});
|
||||
|
||||
candidates.sort((a, b) => b.token.length - a.token.length);
|
||||
const match = candidates.find(candidate => (
|
||||
candidate.token.length <= 3
|
||||
? nodeToken.startsWith(candidate.token)
|
||||
: nodeToken.includes(candidate.token)
|
||||
));
|
||||
return match?.category || '';
|
||||
},
|
||||
|
||||
getMissingNodeTypeDownloadCategory(missing = {}) {
|
||||
return this.getNodeTypeDownloadCategory(missing.locate_node_type)
|
||||
|| this.getNodeTypeDownloadCategory(missing.promoted_inner_node_type)
|
||||
@@ -233,7 +406,9 @@ export const downloadTargetMethods = {
|
||||
|
||||
getMissingDownloadCategory(missing = {}, fallbackCategory = 'checkpoints') {
|
||||
const knownCategories = this.getKnownDownloadCategorySet();
|
||||
const supportedCategories = this.getMissingSupportedDownloadCategories?.(missing) || [];
|
||||
const candidates = [
|
||||
...supportedCategories,
|
||||
this.getMissingNodeTypeDownloadCategory(missing),
|
||||
missing.category,
|
||||
missing.directory,
|
||||
@@ -1117,7 +1292,8 @@ export const downloadTargetMethods = {
|
||||
});
|
||||
merged.category = this.getSourceResultDownloadCategory(
|
||||
result,
|
||||
merged.category || missing.category || ''
|
||||
merged.category || missing.category || '',
|
||||
missing
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1367,7 +1543,8 @@ export const downloadTargetMethods = {
|
||||
const searchSuggestion = this.getCachedSearchSuggestionData(missing);
|
||||
return this.getSourceResultDownloadCategory(
|
||||
searchSuggestion || {},
|
||||
this.getMissingDownloadCategory(missing, fallbackCategory || 'checkpoints')
|
||||
this.getMissingDownloadCategory(missing, fallbackCategory || 'checkpoints'),
|
||||
missing
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@@ -1671,7 +1671,8 @@ export const searchPanelMethods = {
|
||||
const baseModel = downloadSource.base_model || missing.civitai_info?.base_model || '';
|
||||
const rowCategory = this.getSourceResultDownloadCategory?.(
|
||||
downloadSource,
|
||||
downloadSource.directory || downloadSource.category || this.getMissingDownloadCategory?.(missing, 'checkpoints') || 'checkpoints'
|
||||
downloadSource.directory || downloadSource.category || this.getMissingDownloadCategory?.(missing, 'checkpoints') || 'checkpoints',
|
||||
missing
|
||||
) || downloadSource.directory || downloadSource.category || this.getMissingDownloadCategory?.(missing, 'checkpoints') || 'checkpoints';
|
||||
const secondaryParts = [
|
||||
sourceSecondary,
|
||||
|
||||
@@ -58,6 +58,165 @@ export const missingBrowserMethods = {
|
||||
};
|
||||
},
|
||||
|
||||
getMissingSupportedFolderCategories(missing = {}) {
|
||||
const categories = [];
|
||||
const addCategory = (value) => {
|
||||
if (value === undefined || value === null || String(value).trim() === '') return;
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(addCategory);
|
||||
return;
|
||||
}
|
||||
String(value).split(/[,|;]/).forEach(part => {
|
||||
const normalized = this.normalizeDownloadCategory?.(part) || String(part || '').trim();
|
||||
if (
|
||||
normalized
|
||||
&& normalized !== 'unknown'
|
||||
&& !categories.includes(normalized)
|
||||
) {
|
||||
categories.push(normalized);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
addCategory(this.getMissingSupportedDownloadCategories?.(missing) || []);
|
||||
if (!categories.length) {
|
||||
addCategory(missing.category_hints);
|
||||
addCategory(missing.categoryHints);
|
||||
addCategory(missing.model_widget_category_hints);
|
||||
addCategory(missing.modelWidgetCategoryHints);
|
||||
addCategory(missing.supported_categories);
|
||||
addCategory(missing.supportedCategories);
|
||||
addCategory(missing.category);
|
||||
}
|
||||
return categories;
|
||||
},
|
||||
|
||||
getMissingSupportedFolderKeys(missing = {}) {
|
||||
const keys = [];
|
||||
const addKey = (value) => {
|
||||
if (value === undefined || value === null || String(value).trim() === '') return;
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(addKey);
|
||||
return;
|
||||
}
|
||||
String(value).split(/[,|;]/).forEach(part => {
|
||||
const key = String(part || '').trim();
|
||||
if (key && key !== 'unknown' && !keys.includes(key)) {
|
||||
keys.push(key);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
addKey(missing.folder_key_hints);
|
||||
addKey(missing.folderKeyHints);
|
||||
addKey(missing.model_widget_folder_key_hints);
|
||||
addKey(missing.modelWidgetFolderKeyHints);
|
||||
|
||||
if (keys.length) {
|
||||
const nodeCategory = this.getMissingNodeTypeDownloadCategory?.(missing) || '';
|
||||
if (nodeCategory) {
|
||||
const filteredKeys = keys.filter(key => (
|
||||
(this.normalizeDownloadCategory?.(key) || key) === nodeCategory
|
||||
));
|
||||
if (filteredKeys.length) return filteredKeys;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
return this.getMissingSupportedFolderCategories(missing);
|
||||
},
|
||||
|
||||
normalizeMissingFolderRootPath(folderKey = '', path = '') {
|
||||
const cleanPath = String(path || '').trim();
|
||||
if (!cleanPath) return '';
|
||||
|
||||
const normalizedCategory = this.normalizeDownloadCategory?.(folderKey) || folderKey;
|
||||
const rawKey = String(folderKey || '').trim();
|
||||
if (!normalizedCategory || normalizedCategory === rawKey) {
|
||||
return cleanPath;
|
||||
}
|
||||
|
||||
const usesBackslash = cleanPath.includes('\\');
|
||||
const normalizedPath = cleanPath.replace(/\\/g, '/').replace(/\/+$/g, '');
|
||||
const parts = normalizedPath.split('/').filter(Boolean);
|
||||
const leaf = (parts[parts.length - 1] || '').toLowerCase();
|
||||
const parent = (parts[parts.length - 2] || '').toLowerCase();
|
||||
const canonicalLeaf = String(normalizedCategory || '').toLowerCase();
|
||||
const rawSuffix = rawKey.toLowerCase().startsWith(`${canonicalLeaf}_`)
|
||||
? rawKey.slice(canonicalLeaf.length + 1).toLowerCase()
|
||||
: '';
|
||||
|
||||
if (rawSuffix && leaf === rawSuffix && parent === canonicalLeaf) {
|
||||
const parentPath = parts.slice(0, -1).join('/');
|
||||
return usesBackslash ? parentPath.replace(/\//g, '\\') : parentPath;
|
||||
}
|
||||
return cleanPath;
|
||||
},
|
||||
|
||||
getMissingSupportedFolderDetails(missing = {}) {
|
||||
const folderKeys = this.getMissingSupportedFolderKeys(missing);
|
||||
const seenPaths = new Set();
|
||||
const details = [];
|
||||
|
||||
const normalizePathIdentity = (value = '') => String(value || '')
|
||||
.replace(/[\\/]+/g, '/')
|
||||
.replace(/\/+$/g, '')
|
||||
.toLowerCase();
|
||||
|
||||
folderKeys.forEach(folderKey => {
|
||||
const normalizedCategory = this.normalizeDownloadCategory?.(folderKey) || folderKey;
|
||||
const label = this.getCategoryDisplayName?.(normalizedCategory) || folderKey;
|
||||
const roots = this.downloadRootDirectories?.[folderKey]
|
||||
|| this.downloadRootDirectories?.[normalizedCategory];
|
||||
const paths = Array.isArray(roots)
|
||||
? roots
|
||||
: (roots ? [roots] : []);
|
||||
|
||||
if (!paths.length) {
|
||||
details.push({
|
||||
category: normalizedCategory,
|
||||
folderKey,
|
||||
label,
|
||||
path: '',
|
||||
display: label && label !== folderKey ? `${label} (${folderKey})` : folderKey
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
paths.forEach(path => {
|
||||
const cleanPath = this.normalizeMissingFolderRootPath(folderKey, path);
|
||||
if (!cleanPath) return;
|
||||
const identity = `${normalizedCategory}::${normalizePathIdentity(cleanPath)}`;
|
||||
if (seenPaths.has(identity)) return;
|
||||
seenPaths.add(identity);
|
||||
details.push({
|
||||
category: normalizedCategory,
|
||||
folderKey,
|
||||
label,
|
||||
path: cleanPath,
|
||||
display: `${label}: ${cleanPath}`
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return details;
|
||||
},
|
||||
|
||||
renderMissingNodeFolderBadge(missing = {}) {
|
||||
const details = this.getMissingSupportedFolderDetails(missing);
|
||||
if (!details.length) return '';
|
||||
|
||||
const folderCount = details.length;
|
||||
const tooltip = folderCount === 1
|
||||
? `This node reads models from 1 folder:\n${details[0].display}`
|
||||
: `This node reads models from ${folderCount} folders:\n${details.map(detail => detail.display).join('\n')}`;
|
||||
const ariaLabel = folderCount === 1
|
||||
? `This node reads models from 1 folder`
|
||||
: `This node reads models from ${folderCount} folders`;
|
||||
|
||||
return `<span class="mr-node-folder-count-badge" data-tooltip="${this.escapeHtml(tooltip)}" tabindex="0" aria-label="${this.escapeHtml(ariaLabel)}">${this.escapeHtml(String(folderCount))}</span>`;
|
||||
},
|
||||
|
||||
getBestLocalMatch(missing = {}, minConfidence = 0) {
|
||||
const matches = Array.isArray(missing.matches) ? missing.matches : [];
|
||||
return matches
|
||||
@@ -1297,11 +1456,23 @@ export const missingBrowserMethods = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.downloadDirectories) {
|
||||
const needsDownloadDirectories = !this.downloadDirectories;
|
||||
const needsDownloadRootDirectories = !this.downloadRootDirectories
|
||||
&& typeof this.ensureDownloadRootDirectoriesLoaded === 'function';
|
||||
if (needsDownloadDirectories || needsDownloadRootDirectories) {
|
||||
const renderToken = `download-dirs-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
this._downloadDirectoriesRenderToken = renderToken;
|
||||
container.innerHTML = this.renderStatusMessage('Loading download folders...', 'info');
|
||||
this.ensureDownloadDirectoriesLoaded?.().then(() => {
|
||||
container.innerHTML = this.renderStatusMessage('Loading model folders...', 'info');
|
||||
|
||||
const loaders = [];
|
||||
if (needsDownloadDirectories) {
|
||||
loaders.push(this.ensureDownloadDirectoriesLoaded?.());
|
||||
}
|
||||
if (needsDownloadRootDirectories) {
|
||||
loaders.push(this.ensureDownloadRootDirectoriesLoaded());
|
||||
}
|
||||
|
||||
Promise.allSettled(loaders).then(() => {
|
||||
if (
|
||||
this._downloadDirectoriesRenderToken === renderToken &&
|
||||
this.activeTab === 'missing' &&
|
||||
@@ -1469,6 +1640,7 @@ export const missingBrowserMethods = {
|
||||
html += this.getLocateIconHtml();
|
||||
}
|
||||
html += `${nodeChipText}</span>`;
|
||||
html += this.renderMissingNodeFolderBadge(missing);
|
||||
html += `</div>`;
|
||||
html += `</div>`;
|
||||
html += `</div>`;
|
||||
|
||||
Reference in New Issue
Block a user