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 = '';
}
}