diff --git a/__init__.py b/__init__.py index 35d57ce..bf17526 100644 --- a/__init__.py +++ b/__init__.py @@ -1112,6 +1112,19 @@ class ModelResolverExtension: filename = data.get("filename", "") category = data.get("category", "") resolved_path = data.get("resolved_path", "") + local_only = to_bool(data.get("local_only"), False) + provided_hash = ( + data.get("sha256") + or data.get("hash") + or data.get("file_hash") + or "" + ) + provided_hash = str(provided_hash or "").strip().lower() + if not ( + len(provided_hash) == 64 + and all(ch in "0123456789abcdef" for ch in provided_hash) + ): + provided_hash = "" if not filename: return web.json_response( @@ -1155,17 +1168,125 @@ class ModelResolverExtension: except Exception: pass - # Search CivitAI for the model using hash - if download_available and file_path and _os.path.exists(file_path): + if file_path and _os.path.exists(file_path): file_location = _os.path.dirname(file_path).replace("\\", "/") if file_location and not file_location.endswith("/"): file_location += "/" + + def infer_model_type_from_category(value): + if not str(value or "").strip(): + return "" + normalized = normalize_download_category(value or "") + type_map = { + "checkpoints": "checkpoint", + "loras": "lora", + "vae": "vae", + "text_encoders": "text_encoder", + "clip": "clip", + "clip_vision": "clip_vision", + "controlnet": "controlnet", + "upscale_models": "upscale", + "diffusion_models": "diffusion_model", + } + return type_map.get(normalized, normalized.rstrip("s")) + + def build_info_response( + result=None, + *, + metadata_path="", + metadata_saved=False, + civitai_checked=False, + local_payload=False, + ): + result = result or {} + size_value = result.get("size") + if not size_value and file_path and _os.path.exists(file_path): + try: + size_value = _os.path.getsize(file_path) + except Exception: + size_value = None + + model_type = ( + result.get("model_type") + or result.get("type") + or infer_model_type_from_category(category) + ) + + return { + "filename": result.get("filename") or filename, + "category": category, + "file_path": result.get("file_path") or file_path or "", + "resolved_path": result.get("resolved_path") or file_path or "", + "metadata_path": metadata_path + or result.get("metadata_path") + or "", + "metadata_saved": bool(metadata_saved), + "location": result.get("location") or file_location, + "url": result.get("url"), + "version_url": result.get("version_url"), + "model_id": result.get("model_id"), + "model_name": result.get("model_name") or clean_name, + "model_type": model_type, + "version_id": result.get("version_id"), + "version_name": result.get("version_name", ""), + "sha256": result.get("sha256") or provided_hash, + "size": size_value, + "base_model": result.get("base_model"), + "tags": result.get("tags", []), + "trained_words": result.get("trained_words", []), + "images": result.get("images", []), + "clip_skip": result.get("clip_skip"), + "description": result.get("description", ""), + "model_description": result.get("model_description", ""), + "from_metadata": bool(result.get("from_metadata")), + "local_only": bool(local_payload), + "civitai_checked": bool(civitai_checked), + } + + if local_only: + result = None + if download_available and file_path and _os.path.exists(file_path): + try: + from .core.sources.civitai import ( + get_model_info_for_file, + ) + + result = get_model_info_for_file( + file_path, + local_only=True, + ) + except Exception as e: + self.logger.warning(f"Local model info error: {e}") + return web.json_response( + build_info_response( + result, + local_payload=True, + civitai_checked=False, + ) + ) + + # Search CivitAI for the model using hash + if download_available and file_path and _os.path.exists(file_path): try: from .core.sources.civitai import ( + get_model_info_by_hash, get_model_info_for_file, ) - result = get_model_info_for_file(file_path) + if provided_hash: + result = get_model_info_by_hash(provided_hash) + if result: + result["file_path"] = file_path + result["resolved_path"] = file_path + result["location"] = file_location + if not result.get("size"): + try: + result["size"] = _os.path.getsize(file_path) + except Exception: + pass + else: + result = get_model_info_for_file(file_path) + if result and ( result.get("url") or result.get("version_url") @@ -1177,7 +1298,7 @@ class ModelResolverExtension: if not result.get("from_metadata"): try: sidecar_path = get_metadata_sidecar_path(file_path) - if sidecar_path and not _os.path.exists(sidecar_path): + if sidecar_path: metadata_payload = { "source": "civitai", "details_source": "civitai", @@ -1190,7 +1311,7 @@ class ModelResolverExtension: "model_id": result.get("model_id"), "version_id": result.get("version_id"), "version_name": result.get("version_name", ""), - "sha256": result.get("sha256"), + "sha256": result.get("sha256") or provided_hash, "size": result.get("size"), "base_model": result.get("base_model"), "tags": result.get("tags", []), @@ -1219,45 +1340,18 @@ class ModelResolverExtension: or "", ) or "" metadata_saved = bool(metadata_path) - elif sidecar_path: - metadata_path = sidecar_path except Exception as metadata_error: self.logger.warning( f"CivitAI metadata sidecar save failed: {metadata_error}" ) return web.json_response( - { - "filename": filename, - "file_path": file_path, - "resolved_path": file_path, - "metadata_path": metadata_path, - "metadata_saved": metadata_saved, - "location": result.get("location") - or file_location, - "url": result.get("url"), - "version_url": result.get("version_url"), - "model_id": result.get("model_id"), - "model_name": result.get( - "model_name", clean_name - ), - "model_type": result.get("model_type", ""), - "version_id": result.get("version_id"), - "version_name": result.get("version_name", ""), - "sha256": result.get("sha256"), - "size": result.get("size"), - "base_model": result.get("base_model"), - "tags": result.get("tags", []), - "trained_words": result.get( - "trained_words", [] - ), - "images": result.get("images", []), - "clip_skip": result.get("clip_skip"), - "description": result.get("description", ""), - "model_description": result.get( - "model_description", "" - ), - } + build_info_response( + result, + metadata_path=metadata_path, + metadata_saved=metadata_saved, + civitai_checked=True, + ) ) except Exception as e: self.logger.warning(f"CivitAI search error: {e}") @@ -1275,6 +1369,7 @@ class ModelResolverExtension: if result and result.get("url"): return web.json_response( { + "filename": filename, "url": result["url"], "file_path": file_path, "resolved_path": file_path, @@ -1283,13 +1378,16 @@ class ModelResolverExtension: "version_id": result.get("version_id"), "size": result.get("size"), "tags": result.get("tags", []), + "civitai_checked": True, } ) except Exception as e: self.logger.warning(f"CivitAI fallback search error: {e}") # No result found - return web.json_response({"url": None}) + response = build_info_response(None, civitai_checked=True) + response["url"] = None + return web.json_response(response) @routes.post("/model_resolver/model-details") @json_api_endpoint("model-details") diff --git a/core/sources/civitai.py b/core/sources/civitai.py index b6bed69..630089c 100644 --- a/core/sources/civitai.py +++ b/core/sources/civitai.py @@ -1783,7 +1783,7 @@ def _metadata_to_model_info(metadata: Dict[str, Any]) -> Dict[str, Any]: def get_model_info_for_file( - file_path: str, api_key: Optional[str] = None + file_path: str, api_key: Optional[str] = None, local_only: bool = False ) -> Optional[Dict[str, Any]]: """ Get model info from CivitAI by computing file hash and looking up by hash. @@ -1792,6 +1792,8 @@ def get_model_info_for_file( Args: file_path: Full path to the model file api_key: Optional CivitAI API key + local_only: If true, never calculate a hash or query CivitAI when + metadata is missing. Return only local file information. Returns: Dict with model info from CivitAI, or None if not found @@ -1826,6 +1828,26 @@ def get_model_info_for_file( ] log_info(f"Files in {directory}: {metadata_files}") + if local_only: + filename = os.path.basename(file_path) + stem = os.path.splitext(filename)[0] + result = { + "source": "local", + "filename": filename, + "file_path": file_path, + "resolved_path": file_path, + "model_name": stem, + "model_type": "", + "location": _format_model_location(file_path), + "from_metadata": False, + "local_only": True, + } + try: + result["size"] = os.path.getsize(file_path) + except Exception: + pass + return result + # If no metadata file, compute hash and look up on CivitAI file_hash = _get_sha256_hash(file_path) if not file_hash: diff --git a/web/css/resolver-main.css b/web/css/resolver-main.css index 3b97b51..6cebeac 100644 --- a/web/css/resolver-main.css +++ b/web/css/resolver-main.css @@ -3834,6 +3834,7 @@ font-weight: 700; white-space: nowrap; box-shadow: 0 1px 0 rgba(255, 255, 255, 0.08) inset; + cursor: pointer; } .mr-info-link:hover { @@ -3842,6 +3843,15 @@ color: #ffffff; } +button.mr-info-link { + font: inherit; +} + +button.mr-info-link:disabled { + cursor: wait; + opacity: 0.72; +} + .mr-info-external-link-icon { width: 14px; height: 14px; diff --git a/web/resolver/views/model_info_methods.js b/web/resolver/views/model_info_methods.js index 8914f0d..07d5c15 100644 --- a/web/resolver/views/model_info_methods.js +++ b/web/resolver/views/model_info_methods.js @@ -264,8 +264,8 @@ export const modelInfoMethods = { }); } - // Fetch CivitAI info - this.fetchModelInfoForDialog(loraName, modelData, dialog); + dialog._infoDialogLookup = { loraName, modelData }; + this.fetchLocalModelInfoForDialog(loraName, modelData, dialog); }, /** @@ -793,14 +793,14 @@ export const modelInfoMethods = { async calculateInfoDialogHash(dialog, button) { if (button?.dataset.hashCalculating === 'true') { await this.cancelInfoDialogHashCalculation(dialog, button); - return; + return null; } const data = dialog?._infoDialogData || {}; const filePath = this.getInfoDialogModelFilePath(data); if (!filePath) { this.showNotification?.('No local file path available', 'error'); - return; + return null; } dialog._hashCalculation = { progressId: '', cancelRequested: false }; @@ -842,7 +842,7 @@ export const modelInfoMethods = { await new Promise(resolve => setTimeout(resolve, 250)); if (!dialog.isConnected) { await this.cancelInfoDialogHashCalculation(dialog, button); - return; + return null; } const progress = await this.fetchJson( `/model_resolver/calculate-file-hash/progress/${encodeURIComponent(progressId)}`, @@ -869,7 +869,7 @@ export const modelInfoMethods = { } dialog._hashCalculation = null; this.showNotification?.('Hash calculation stopped.', 'info'); - return; + return null; } if (progress?.status === 'error') { throw new Error(progress.error || progress.message || 'Hash calculation failed'); @@ -895,6 +895,7 @@ export const modelInfoMethods = { result.metadata_updated ? 'Hash calculated and saved to metadata.' : 'Hash calculated.', 'success' ); + return updatedData; } catch (error) { dialog._hashCalculation = null; if (button) { @@ -905,6 +906,7 @@ export const modelInfoMethods = { } this.hideInfoDialogHashProgress(dialog); this.showNotification?.(`Hash calculation failed: ${error?.message || error}`, 'error'); + return null; } }, @@ -913,6 +915,12 @@ export const modelInfoMethods = { dialog.dataset.mlInfoBound = 'true'; dialog.addEventListener('click', async (event) => { + const fetchCivitaiButton = event.target.closest('.mr-info-fetch-civitai'); + if (fetchCivitaiButton && dialog.contains(fetchCivitaiButton)) { + await this.fetchCivitaiModelInfoForDialog(dialog, fetchCivitaiButton); + return; + } + const hashButton = event.target.closest('.mr-info-hash-calculate'); if (hashButton && dialog.contains(hashButton)) { await this.calculateInfoDialogHash(dialog, hashButton); @@ -1047,28 +1055,167 @@ export const modelInfoMethods = { dialog._infoDialogResizeSaveTimer = resizeSaveTimer; }, + getModelInfoResolvedPath(modelData = {}) { + return modelData?.resolved_path + || modelData?.path + || modelData?.full_path + || modelData?.folder_path + || modelData?.download_directory + || ''; + }, + + 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 resolvedPath = this.getModelInfoResolvedPath(modelData); + const categoryTypeMap = { + checkpoints: 'checkpoint', + loras: 'lora', + vae: 'vae', + text_encoders: 'text_encoder', + clip: 'clip', + clip_vision: 'clip_vision', + controlnet: 'controlnet', + upscale_models: 'upscale', + diffusion_models: 'diffusion_model' + }; + const rawType = modelData?.model_type || modelData?.modelType || modelData?.type || modelData?.category || ''; + const normalizedType = String(rawType || '').trim().toLowerCase().replace(/[\s-]+/g, '_'); + return { + filename, + category: modelData?.category || '', + file_path: resolvedPath, + resolved_path: resolvedPath, + location: resolvedPath ? this.formatInfoDialogLocation({ file_path: resolvedPath }) : '', + model_name: modelData?.model_name || modelData?.modelName || modelName, + model_type: categoryTypeMap[normalizedType] || rawType, + sha256: modelData?.sha256 || modelData?.hash || modelData?.hashes?.SHA256 || '', + size: modelData?.size || modelData?.file_size || modelData?.fileSize || '', + base_model: modelData?.base_model || modelData?.baseModel || '', + tags: modelData?.tags || [], + trained_words: modelData?.trained_words || modelData?.trainedWords || [], + local_only: true, + civitai_checked: false + }; + }, + /** - * Fetch model info and update the dialog + * Fetch local model info and update the dialog without querying CivitAI. */ - async fetchModelInfoForDialog(loraName, modelData, dialog) { + async fetchLocalModelInfoForDialog(loraName, modelData, dialog) { + const requestId = `${Date.now()}_${Math.random()}`; + dialog._localInfoRequestId = requestId; + const fallbackData = this.buildLocalInfoDialogData(loraName, modelData); + this.updateInfoDialogWithData(dialog, fallbackData); + try { - const resolvedPath = modelData?.resolved_path - || modelData?.path - || modelData?.full_path - || modelData?.folder_path - || modelData?.download_directory - || ''; const data = await this.fetchJson('/model_resolver/civitai-search', { method: 'POST', body: JSON.stringify({ filename: loraName, category: modelData?.category || '', - resolved_path: resolvedPath + resolved_path: this.getModelInfoResolvedPath(modelData), + local_only: true + }) + }, 'Fetch local model info'); + if ( + dialog._localInfoRequestId !== requestId + || dialog._infoDialogCivitaiFetchStarted + || dialog._infoDialogData?.civitai_checked + || dialog._infoDialogData?.version_url + || dialog._infoDialogData?.url + ) { + return; + } + this.updateInfoDialogWithData(dialog, { ...fallbackData, ...data, civitai_checked: false }); + } catch (e) { + if (dialog._localInfoRequestId !== requestId || dialog._infoDialogCivitaiFetchStarted) { + return; + } + this.updateInfoDialogWithData(dialog, fallbackData); + } + }, + + async fetchCivitaiModelInfoForDialog(dialog, button = null) { + if (!dialog) return; + if (button?.dataset.fetchingCivitai === 'true') return; + + const lookup = dialog._infoDialogLookup || {}; + let data = dialog._infoDialogData || {}; + const filename = data.filename || lookup.loraName || ''; + const category = data.category || lookup.modelData?.category || ''; + const resolvedPath = this.getInfoDialogModelFilePath(data) + || this.getModelInfoResolvedPath(lookup.modelData || {}); + + if (!filename) { + this.showNotification?.('No model filename available', 'error'); + return; + } + + dialog._infoDialogCivitaiFetchStarted = true; + const setFetchingState = (isFetching, label = 'Fetch with CivitAI') => { + if (!button) return; + button.dataset.fetchingCivitai = isFetching ? 'true' : 'false'; + button.disabled = isFetching; + button.innerHTML = isFetching + ? `${getSvgIcon('civitai', 'currentColor', 'mr-info-external-link-icon')} Fetching...` + : `${getSvgIcon('civitai', 'currentColor', 'mr-info-external-link-icon')} ${this.escapeHtml(label)}`; + }; + + setFetchingState(true); + + try { + let hash = this.getInfoDialogHash(data); + if (!hash) { + const hashButton = dialog.querySelector('.mr-info-hash-calculate'); + const hashResult = await this.calculateInfoDialogHash(dialog, hashButton); + if (!hashResult) { + setFetchingState(false); + return; + } + data = dialog._infoDialogData || data; + hash = this.getInfoDialogHash(data); + } + + if (!hash) { + throw new Error('No SHA256 hash available'); + } + + const result = await this.fetchJson('/model_resolver/civitai-search', { + method: 'POST', + body: JSON.stringify({ + filename, + category, + resolved_path: resolvedPath, + sha256: hash }) }, 'Fetch CivitAI model info'); - this.updateInfoDialogWithData(dialog, data); - } catch (e) { - this.updateInfoDialogError(dialog, 'Error fetching info from CivitAI'); + + const merged = { + ...data, + ...result, + filename, + category, + file_path: result.file_path || data.file_path || resolvedPath, + resolved_path: result.resolved_path || data.resolved_path || resolvedPath, + sha256: result.sha256 || hash, + civitai_checked: true + }; + dialog._infoDialogData = merged; + this.updateInfoDialogWithData(dialog, merged); + if (merged.url || merged.version_url) { + this.showNotification?.('CivitAI info loaded.', 'success'); + } else { + this.showNotification?.('No exact CivitAI match found.', 'info'); + } + } catch (error) { + this.showNotification?.(`CivitAI fetch failed: ${error?.message || error}`, 'error'); + this.updateInfoDialogWithData(dialog, { + ...(dialog._infoDialogData || data), + civitai_checked: true + }); + } finally { + setFetchingState(false); } }, @@ -1092,7 +1239,7 @@ export const modelInfoMethods = { // Update title const titleEl = dialog.querySelector('.mr-info-dialog-title'); if (titleEl) { - const modelName = data.model_name || data.modelName || 'Unknown Model'; + const modelName = data.model_name || data.modelName || data.name || data.filename || 'Unknown Model'; const versionName = data.version_name || data.versionName || ''; titleEl.innerHTML = this.renderVersionedModelNameHtml(modelName, versionName) || this.escapeHtml(modelName); } @@ -1178,7 +1325,7 @@ export const modelInfoMethods = { ${getSvgIcon('externalLink', 'currentColor', 'mr-info-external-link-icon')} `; - } else { + } else if (data.civitai_checked || data.civitaiChecked) { const searchName = data.model_name || data.modelName || 'Unknown'; civitaiLinkEl.innerHTML = ` Model not found @@ -1187,6 +1334,15 @@ export const modelInfoMethods = { ${getSvgIcon('externalLink', 'currentColor', 'mr-info-external-link-icon')} `; + } else if (this.getInfoDialogModelFilePath(data)) { + civitaiLinkEl.innerHTML = ` + + `; + } else { + civitaiLinkEl.innerHTML = ''; } }