diff --git a/__init__.py b/__init__.py index e937ef8..6aa945f 100644 --- a/__init__.py +++ b/__init__.py @@ -258,6 +258,7 @@ class ModelResolverExtension: search_lora_manager_archive_for_file, clear_search_cache as clear_lora_manager_archive_search_cache, ) + from .core.sources import clear_all_search_caches download_available = True except ImportError as e: @@ -3152,10 +3153,7 @@ class ModelResolverExtension: @json_api_endpoint("Clear search cache") async def clear_search_cache_route(request): """Clear backend search caches after token/settings changes.""" - clear_huggingface_search_cache() - clear_civitai_search_cache() - clear_civarchive_search_cache() - clear_lora_manager_archive_search_cache() + clear_all_search_caches() reload_popular_databases() reload_model_list() invalidate_model_files_cache() @@ -3236,10 +3234,7 @@ class ModelResolverExtension: async def model_list_update_route(request): """Download latest ComfyUI-Manager model-list.json.""" result = await asyncio.to_thread(update_model_list_from_remote) - clear_huggingface_search_cache() - clear_civitai_search_cache() - clear_civarchive_search_cache() - clear_lora_manager_archive_search_cache() + clear_all_search_caches() self.search_result_timestamps.clear() return web.json_response(result) diff --git a/core/resolver.py b/core/resolver.py index da013c7..8434184 100644 --- a/core/resolver.py +++ b/core/resolver.py @@ -696,7 +696,7 @@ def extract_workflow_urls(workflow_json: Dict[str, Any]) -> Dict[str, Dict[str, return url_map -def parse_huggingface_url(url: str) -> Tuple[Optional[str], Optional[str]]: +def get_huggingface_repo_and_file_from_url(url: str) -> Tuple[Optional[str], Optional[str]]: """ Extract HuggingFace repo and path from URL. diff --git a/core/sources/__init__.py b/core/sources/__init__.py index 4ec901d..345cb90 100644 --- a/core/sources/__init__.py +++ b/core/sources/__init__.py @@ -12,8 +12,14 @@ from .huggingface import ( search_huggingface, search_huggingface_for_file, get_huggingface_download_url, + clear_search_cache as clear_huggingface_search_cache, +) +from .civitai import ( + search_civitai, + search_civitai_for_file, + get_civitai_download_url, + clear_search_cache as clear_civitai_search_cache, ) -from .civitai import search_civitai, search_civitai_for_file, get_civitai_download_url from .civarchive import ( clear_search_cache as clear_civarchive_search_cache, is_civarchive_available, @@ -30,6 +36,13 @@ from .lora_manager_archive import ( search_lora_manager_archive_for_file, ) +def clear_all_search_caches() -> None: + """Clear search caches for all external sources.""" + clear_huggingface_search_cache() + clear_civitai_search_cache() + clear_civarchive_search_cache() + clear_lora_manager_archive_search_cache() + __all__ = [ "search_popular_models", "get_popular_model_url", @@ -52,4 +65,5 @@ __all__ = [ "is_lora_manager_archive_available", "search_lora_manager_archive", "search_lora_manager_archive_for_file", + "clear_all_search_caches", ] diff --git a/core/sources/civarchive.py b/core/sources/civarchive.py index b9e24a8..7bb7d60 100644 --- a/core/sources/civarchive.py +++ b/core/sources/civarchive.py @@ -34,6 +34,7 @@ from ..type_utils import ( parse_size_header as _parse_size_header, parse_content_range_size as _parse_content_range_size, extract_response_file_size as _response_file_size, + parse_civitai_model_path, ) from ..progress import report_progress, get_progress_reporter from ..log_system.log_funcs import create_module_logger @@ -509,16 +510,7 @@ def parse_civarchive_url(url: str) -> Optional[Dict[str, Any]]: if sha_match: return {"sha256": sha_match.group(1).lower()} - model_match = re.search(r"/models/(\d+)", parsed.path) - if model_match: - result: Dict[str, Any] = {"model_id": int(model_match.group(1))} - query = parse_qs(parsed.query) - version_id = query.get("modelVersionId", [None])[0] - if version_id: - result["version_id"] = _coerce_int(version_id) - return result - - return None + return parse_civitai_model_path(parsed.path, parsed.query) def _extract_sha256(value: str) -> Optional[str]: diff --git a/core/sources/civitai.py b/core/sources/civitai.py index 232cf18..46e92d3 100644 --- a/core/sources/civitai.py +++ b/core/sources/civitai.py @@ -28,6 +28,7 @@ from ..type_utils import ( get_version_sort_key, check_credential_http, DEFAULT_BROWSER_USER_AGENT, + parse_civitai_model_path, ) from ..progress import report_progress, get_progress_reporter from ..path_utils import calculate_file_sha256, get_filename_from_path @@ -684,15 +685,7 @@ def parse_civitai_url(url: str) -> Optional[Dict[str, Any]]: if match: return {"version_id": int(match.group(1))} - match = re.search(r"/models/(\d+)", parsed.path) - if match: - result = {"model_id": int(match.group(1))} - query = parse_qs(parsed.query) - if "modelVersionId" in query: - result["version_id"] = int(query["modelVersionId"][0]) - return result - - return None + return parse_civitai_model_path(parsed.path, parsed.query) def get_civitai_download_url(version_id: int, api_key: Optional[str] = None) -> str: diff --git a/core/type_utils.py b/core/type_utils.py index 8c843f6..7cc4d1a 100644 --- a/core/type_utils.py +++ b/core/type_utils.py @@ -503,3 +503,26 @@ def extract_response_file_size(response: Any) -> Optional[int]: return size return None + + +def parse_civitai_model_path(path: str, query_string: str) -> Optional[Dict[str, int]]: + """ + Common helper to parse `/models/{model_id}?modelVersionId={version_id}` URLs + for CivitAI and CivArchive. + """ + import re + from urllib.parse import parse_qs + + model_match = re.search(r"/models/(\d+)", path) + if model_match: + result = {"model_id": int(model_match.group(1))} + query = parse_qs(query_string) + version_id_list = query.get("modelVersionId") + if version_id_list: + try: + result["version_id"] = int(version_id_list[0]) + except (ValueError, TypeError): + pass + return result + return None + diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index 06bf059..835ecc0 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -2296,7 +2296,7 @@ export const queueMethods = { text = text.split('/').pop() || text; } if (stem) { - text = text.replace(/\.(safetensors|ckpt|pt|pth|bin|gguf|onnx)$/i, ''); + text = this.stripModelExtension(text); } return text; }, diff --git a/web/resolver/search/search_panel.js b/web/resolver/search/search_panel.js index fdbba59..d85c67d 100644 --- a/web/resolver/search/search_panel.js +++ b/web/resolver/search/search_panel.js @@ -150,7 +150,7 @@ export const searchPanelMethods = { .split(/[\\/]+/) .map(part => part.trim()) .filter(Boolean); - const directoryParts = parts.filter(part => !/\.(safetensors|ckpt|pt|pth|bin|gguf|onnx)$/i.test(part)); + const directoryParts = parts.filter(part => !this.hasModelExtension(part)); for (const part of directoryParts) { const exact = this.resolveBaseModelAliasExact(part); @@ -311,7 +311,7 @@ export const searchPanelMethods = { if (/^urn:/i.test(value.trim())) { return; } - if (/\.(safetensors|ckpt|pt|pth|bin|gguf|onnx)\b/i.test(value) || /model|checkpoint|unet|diffusion/i.test(key)) { + if (this.hasModelExtension(value) || /model|checkpoint|unet|diffusion/i.test(key)) { values.push(value); } return; diff --git a/web/resolver/utils/render_format_methods.js b/web/resolver/utils/render_format_methods.js index 4296661..59b14ef 100644 --- a/web/resolver/utils/render_format_methods.js +++ b/web/resolver/utils/render_format_methods.js @@ -335,5 +335,26 @@ export const renderFormatMethods = { normalizePathToBackward(value) { return String(value || '').trim().replace(/\//g, '\\'); + }, + + stripModelExtension(value) { + return stripModelExtension(value); + }, + + hasModelExtension(value) { + return hasModelExtension(value); } }; + +export const MODEL_EXTENSIONS_REGEX = /\.(safetensors|ckpt|pt2?|bin|pth|pkl|sft|onnx|gguf)(?:$|[?#])/i; + +export function stripModelExtension(value) { + if (!value) return ''; + return String(value).replace(MODEL_EXTENSIONS_REGEX, ''); +} + +export function hasModelExtension(value) { + if (!value) return false; + return MODEL_EXTENSIONS_REGEX.test(value); +} + diff --git a/web/resolver/views/model_info_methods.js b/web/resolver/views/model_info_methods.js index 5492cb7..2462c4b 100644 --- a/web/resolver/views/model_info_methods.js +++ b/web/resolver/views/model_info_methods.js @@ -1056,7 +1056,7 @@ export const modelInfoMethods = { } } catch (e) { // Fall back to direct search - const searchName = name.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, ''); + const searchName = this.stripModelExtension(name); const searchUrl = `https://civitai.com/search?q=${encodeURIComponent(searchName)}`; window.open(searchUrl, '_blank'); } @@ -1154,7 +1154,7 @@ export const modelInfoMethods = { }, createInfoDialog(loraName, modelData) { - const loraDisplayName = loraName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, ''); + const loraDisplayName = this.stripModelExtension(loraName); const dialog = document.createElement('div'); dialog.className = 'mr-info-dialog-backdrop'; @@ -1917,7 +1917,7 @@ export const modelInfoMethods = { buildLocalInfoDialogData(loraName, modelData = {}) { const filename = loraName || modelData?.filename || modelData?.name || ''; - const modelName = String(filename || '').replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, ''); + const modelName = this.stripModelExtension(filename); const resolvedPath = this.getModelInfoResolvedPath(modelData); const categoryTypeMap = { checkpoints: 'checkpoint', diff --git a/web/resolver/views/tabs_loaded_methods.js b/web/resolver/views/tabs_loaded_methods.js index fade9f9..3a571cd 100644 --- a/web/resolver/views/tabs_loaded_methods.js +++ b/web/resolver/views/tabs_loaded_methods.js @@ -168,6 +168,8 @@ export const tabsLoadedMethods = { const activeCount = Object.values(byCategory).reduce((sum, cat) => sum + cat.active.length, 0); const inactiveCount = Object.values(byCategory).reduce((sum, cat) => sum + cat.inactive.length, 0); + const stripModelExtension = (value) => this.stripModelExtension(value || 'Unknown'); + const buildCategoryStrings = (filter) => { const result = {}; for (const [category, modelsObj] of Object.entries(byCategory)) { @@ -175,10 +177,7 @@ export const tabsLoadedMethods = { const models = filter === 'active' ? modelsObj.active : filter === 'inactive' ? modelsObj.inactive : [...modelsObj.active, ...modelsObj.inactive]; const parts = models.map(model => { const fullName = model.name || model.original_path?.split(/[\/\\]/).pop() || 'Unknown'; - let name = fullName; - if (fullName.match(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i)) { - name = fullName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, ''); - } + const name = stripModelExtension(fullName); const strength = model.strength !== null && model.strength !== undefined ? model.strength.toFixed(2) : '1.00'; @@ -194,7 +193,6 @@ export const tabsLoadedMethods = { const allString = buildCategoryStrings('all'); const copyIcon = getSvgIcon('copy', 'currentColor', 'mr-copy-btn-icon'); - const stripModelExtension = (value) => String(value || 'Unknown').replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, ''); const modelToken = (model, category) => { const fullName = model.name || model.original_path?.split(/[\/\\]/).pop() || 'Unknown'; const name = stripModelExtension(fullName); @@ -253,7 +251,7 @@ export const tabsLoadedMethods = { for (const model of modelsObj.active) { const fullName = model.name || model.original_path?.split(/[\/\\]/).pop() || 'Unknown'; - const name = fullName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, ''); + const name = this.stripModelExtension(fullName); const strength = model.strength !== null && model.strength !== undefined ? model.strength.toFixed(2) : null; html += `${this.escapeHtml(name)}${strength !== null ? `${this.escapeHtml(strength)}` : ''}`; } @@ -272,7 +270,7 @@ export const tabsLoadedMethods = { for (const model of modelsObj.inactive) { const fullName = model.name || model.original_path?.split(/[\/\\]/).pop() || 'Unknown'; - const name = fullName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, ''); + const name = this.stripModelExtension(fullName); const strength = model.strength !== null && model.strength !== undefined ? model.strength.toFixed(2) : null; html += `${this.escapeHtml(name)}${strength !== null ? `${this.escapeHtml(strength)}` : ''}`; }