From 093b6d33a7d96f5d03c80f49d1405f69daca9779 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Tue, 23 Jun 2026 19:47:49 +0200 Subject: [PATCH] feat: enrich model info dialog with location, size, tags, and trained words from metadata --- __init__.py | 17 +- core/sources/civitai.py | 177 +++++++++++++++-- web/css/resolver-main.css | 40 +++- web/resolver/views/model_info_methods.js | 238 +++++++++++++++++++++-- 4 files changed, 434 insertions(+), 38 deletions(-) diff --git a/__init__.py b/__init__.py index bf3580e..fc2a1e0 100644 --- a/__init__.py +++ b/__init__.py @@ -776,6 +776,7 @@ class ModelResolverExtension: # Get the file path to hash file_path = resolved_path if resolved_path else None + file_location = "" if not file_path and category: # Try to find the file in the model directories using folder_paths @@ -854,16 +855,26 @@ class ModelResolverExtension: # Search CivitAI for the model using hash if download_available and 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 += "/" try: from .core.sources.civitai import ( get_model_info_for_file, ) result = get_model_info_for_file(file_path) - if result and result.get("url"): + if result and ( + result.get("url") + or result.get("version_url") + or result.get("from_metadata") + or result.get("trained_words") + ): return web.json_response( { "filename": filename, + "location": result.get("location") + or file_location, "url": result.get("url"), "version_url": result.get("version_url"), "model_id": result.get("model_id"), @@ -874,6 +885,7 @@ class ModelResolverExtension: "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( @@ -904,8 +916,11 @@ class ModelResolverExtension: return web.json_response( { "url": result["url"], + "location": file_location, "model_name": result.get("name", clean_name), "version_id": result.get("version_id"), + "size": result.get("size"), + "tags": result.get("tags", []), } ) except Exception as e: diff --git a/core/sources/civitai.py b/core/sources/civitai.py index ef54121..1e26a09 100644 --- a/core/sources/civitai.py +++ b/core/sources/civitai.py @@ -1805,11 +1805,86 @@ def _read_model_metadata(metadata_path: str) -> Optional[Dict[str, Any]]: return None +def _as_metadata_dict(value: Any) -> Dict[str, Any]: + return value if isinstance(value, dict) else {} + + +def _as_metadata_list(value: Any) -> List[Any]: + if value is None: + return [] + if isinstance(value, list): + return value + if isinstance(value, tuple): + return list(value) + return [value] if value else [] + + +def _first_metadata_value(*values: Any) -> Any: + for value in values: + if value is None: + continue + if isinstance(value, str) and not value.strip(): + continue + if isinstance(value, (list, tuple, dict)) and not value: + continue + return value + return None + + +def _metadata_size_to_bytes(value: Any) -> Optional[int]: + if value is None or value == "": + return None + + try: + size = int(float(value)) + except (TypeError, ValueError): + return None + + return size if size >= 0 else None + + +def _format_model_location(file_path: str) -> str: + if not file_path: + return "" + + location = os.path.dirname(file_path).replace("\\", "/") + if location and not location.endswith("/"): + location += "/" + return location + + +def _normalize_metadata_trained_words(*values: Any) -> List[str]: + words: List[str] = [] + seen = set() + + for value in values: + for item in _as_metadata_list(value): + if isinstance(item, dict): + item = ( + item.get("word") + or item.get("name") + or item.get("text") + or item.get("value") + ) + text = str(item or "").strip() + if not text: + continue + key = text.lower() + if key in seen: + continue + seen.add(key) + words.append(text) + + return words + + def _metadata_to_model_info(metadata: Dict[str, Any]) -> Dict[str, Any]: """ Convert metadata file format to model info format used by our extension. """ - civitai_data = metadata.get("civitai") or {} + civitai_data = _as_metadata_dict(metadata.get("civitai")) + selected_version = _as_metadata_dict(metadata.get("selected_version")) + path_metadata = _as_metadata_dict(metadata.get("path_metadata")) # Extract images with metadata images = [] @@ -1840,14 +1915,21 @@ def _metadata_to_model_info(metadata: Dict[str, Any]) -> Dict[str, Any]: } ) - # Get trained words from CivitAI data - trained_words = civitai_data.get("trainedWords") or [] - if isinstance(trained_words, str): - trained_words = [trained_words] if trained_words else [] + # Get trained words from common sidecar shapes, including LoRA Manager metadata. + trained_words = _normalize_metadata_trained_words( + metadata.get("trained_words"), + metadata.get("trainedWords"), + civitai_data.get("trainedWords"), + selected_version.get("trained_words"), + selected_version.get("trainedWords"), + path_metadata.get("trained_words"), + path_metadata.get("trainedWords"), + ) # Get model info - model_info = civitai_data.get("model") or {} - version_info = civitai_data + model_info = _as_metadata_dict(civitai_data.get("model")) + file_infos = _as_metadata_list(civitai_data.get("files")) + file_info = _as_metadata_dict(file_infos[0]) if file_infos else {} # Build model_id from CivitAI data model_id = civitai_data.get("modelId") or civitai_data.get("id") @@ -1855,11 +1937,38 @@ def _metadata_to_model_info(metadata: Dict[str, Any]) -> Dict[str, Any]: return { "source": "metadata", "model_id": model_id, - "model_name": metadata.get("model_name") or metadata.get("file_name", ""), + "model_name": _first_metadata_value( + metadata.get("model_name"), + metadata.get("modelName"), + model_info.get("name"), + metadata.get("file_name"), + ) + or "", "model_type": model_info.get("type", "") or civitai_data.get("type", ""), "version_id": civitai_data.get("id"), - "version_name": civitai_data.get("name", ""), + "version_name": _first_metadata_value( + metadata.get("version_name"), + metadata.get("versionName"), + selected_version.get("name"), + civitai_data.get("name"), + ) + or "", "sha256": metadata.get("sha256", ""), + "size": _metadata_size_to_bytes( + _first_metadata_value( + metadata.get("size"), + metadata.get("file_size"), + metadata.get("fileSize"), + metadata.get("sizeBytes"), + path_metadata.get("size"), + path_metadata.get("file_size"), + path_metadata.get("fileSize"), + path_metadata.get("sizeBytes"), + (file_info.get("sizeKB") * 1024) + if file_info.get("sizeKB") is not None + else None, + ) + ), "url": f"https://civitai.com/models/{civitai_data.get('modelId')}" if civitai_data.get("modelId") else None, @@ -1867,14 +1976,32 @@ def _metadata_to_model_info(metadata: Dict[str, Any]) -> Dict[str, Any]: if model_id else None, "download_url": civitai_data.get("downloadUrl"), - "base_model": (metadata.get("base_model") or civitai_data.get("baseModel", "")), - "tags": metadata.get("tags") or [], + "base_model": _first_metadata_value( + metadata.get("base_model"), + metadata.get("baseModel"), + selected_version.get("base_model"), + selected_version.get("baseModel"), + civitai_data.get("baseModel"), + ) + or "", + "tags": _as_metadata_list(metadata.get("tags") or model_info.get("tags")), "trained_words": trained_words, "images": images, "clip_skip": civitai_data.get("clipSkip"), - "description": metadata.get("model_description", "") - or civitai_data.get("description", ""), - "model_description": metadata.get("model_description", ""), + "description": _first_metadata_value( + metadata.get("modelDescription"), + metadata.get("model_description"), + metadata.get("description"), + model_info.get("description"), + civitai_data.get("description"), + ) + or "", + "model_description": _first_metadata_value( + metadata.get("modelDescription"), + metadata.get("model_description"), + model_info.get("description"), + ) + or "", "from_metadata": True, } @@ -1903,7 +2030,15 @@ def get_model_info_for_file( metadata = _read_model_metadata(metadata_path) if metadata: log_info(f"Using metadata file for {file_path}") - return _metadata_to_model_info(metadata) + result = _metadata_to_model_info(metadata) + result["file_path"] = file_path + result["location"] = _format_model_location(file_path) + if not result.get("size"): + try: + result["size"] = os.path.getsize(file_path) + except Exception: + pass + return result else: # Debug: list all files in the same directory directory = os.path.dirname(file_path) @@ -1919,4 +2054,14 @@ def get_model_info_for_file( if not file_hash: return None - return get_model_info_by_hash(file_hash, api_key) + result = get_model_info_by_hash(file_hash, api_key) + if result: + result["file_path"] = file_path + result["location"] = _format_model_location(file_path) + if not result.get("size"): + try: + result["size"] = os.path.getsize(file_path) + except Exception: + pass + + return result diff --git a/web/css/resolver-main.css b/web/css/resolver-main.css index ab6d7f1..9d2fc43 100644 --- a/web/css/resolver-main.css +++ b/web/css/resolver-main.css @@ -3365,8 +3365,9 @@ /* Info Area (type/base model tags) */ .mr-info-area { display: flex; + flex-wrap: wrap; gap: 8px; - margin-bottom: 16px; + margin-bottom: 10px; } .mr-info-tag { display: inline-flex; @@ -3386,6 +3387,37 @@ .mr-info-tag.-basemodel-sdxl-turbo { background: rgba(33,150,243,0.2); color: #64B5F6; } .mr-info-tag.-basemodel-flux\.1 { background: rgba(255,152,0,0.2); color: #FFB74D; } .mr-info-tag.-basemodel-flux\.1-d { background: rgba(255,152,0,0.2); color: #FFB74D; } +.mr-info-tags-row { + display: flex; + align-items: flex-start; + gap: 10px; + margin: 0 0 16px; +} +.mr-info-tags-label { + flex: 0 0 auto; + padding-top: 5px; + color: var(--mr-text-muted, #888); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} +.mr-info-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + min-width: 0; +} +.mr-info-model-tag { + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border: 1px solid rgba(255,255,255,0.08); + background: rgba(255,255,255,0.06); + color: var(--mr-text-muted, #c8c8c8); + font-size: 10px; +} /* Info Table */ .mr-info-table { @@ -3413,6 +3445,12 @@ color: var(--input-text, #e0e0e0); word-break: break-word; } +.mr-info-location { + font-family: "Cascadia Mono", "Consolas", monospace; + font-size: 12px; + line-height: 1.45; + word-break: break-all; +} .mr-info-trained-words-label { display: flex; align-items: center; diff --git a/web/resolver/views/model_info_methods.js b/web/resolver/views/model_info_methods.js index d8d2ff6..cde49e7 100644 --- a/web/resolver/views/model_info_methods.js +++ b/web/resolver/views/model_info_methods.js @@ -300,12 +300,20 @@ export const modelInfoMethods = { +
+ Tags +
+
+ + + + @@ -322,6 +330,10 @@ export const modelInfoMethods = { + + + +
File ?
Location ?
Hash (sha256) ? Base Model ?
Size ?
@@ -489,9 +501,162 @@ export const modelInfoMethods = { )]; } + if (words && typeof words === 'object') { + return this.normalizeTrainedWords( + words.trained_words + || words.trainedWords + || words.words + || words.values + || [] + ); + } + return []; }, + formatInfoDialogLocation(data = {}) { + let location = data.location + || data.folder_path + || data.folderPath + || data.download_directory + || data.downloadDirectory + || data.directory + || ''; + + if (!location) { + const fullPath = data.resolved_path || data.resolvedPath || data.full_path || data.fullPath || data.path || ''; + const normalizedFullPath = String(fullPath || '').replace(/\\/g, '/'); + const lastSlash = normalizedFullPath.lastIndexOf('/'); + if (lastSlash > 0) { + location = normalizedFullPath.slice(0, lastSlash + 1); + } + } + + location = String(location || '').trim().replace(/\\/g, '/'); + if (location && !location.endsWith('/')) { + location += '/'; + } + + return location; + }, + + normalizeInfoTags(tags) { + if (Array.isArray(tags)) { + const seen = new Set(); + const result = []; + for (let tag of tags) { + if (tag && typeof tag === 'object') { + tag = tag.name || tag.tag || tag.text || tag.value || ''; + } + const text = String(tag || '').trim(); + if (!text) continue; + const key = text.toLowerCase(); + if (seen.has(key)) continue; + seen.add(key); + result.push(text); + } + return result; + } + + if (typeof tags === 'string') { + return this.normalizeInfoTags( + tags + .split(/[\n,|;]/) + .map(tag => tag.trim()) + .filter(Boolean) + ); + } + + if (tags && typeof tags === 'object') { + return this.normalizeInfoTags( + tags.tags + || tags.model_tags + || tags.modelTags + || tags.values + || [] + ); + } + + return []; + }, + + getInfoDialogTags(data = {}) { + const sources = [ + data.tags, + data.model_tags, + data.modelTags, + data.civitai?.tags, + data.civitai?.model?.tags, + data.model?.tags, + data.metadata?.tags, + data.path_metadata?.tags + ]; + + return this.normalizeInfoTags( + sources.flatMap(source => this.normalizeInfoTags(source)) + ); + }, + + formatInfoDialogSize(data = {}) { + const rawSize = data.size + ?? data.file_size + ?? data.fileSize + ?? data.size_bytes + ?? data.sizeBytes + ?? data.file?.size + ?? data.file?.sizeBytes + ?? null; + + if (rawSize === null || rawSize === undefined || rawSize === '') return ''; + if (rawSize === 0 || rawSize === '0') return '0 B'; + + const numericSize = Number(rawSize); + if (Number.isFinite(numericSize)) { + return typeof this.formatBytes === 'function' + ? this.formatBytes(numericSize) + : `${numericSize} B`; + } + + return String(rawSize); + }, + + getInfoDialogTrainedWords(data = {}) { + const sources = [ + data.trained_words, + data.trainedWords, + data.civitai?.trainedWords, + data.civitai?.trained_words, + data.selected_version?.trainedWords, + data.selected_version?.trained_words, + data.modelVersion?.trainedWords, + data.modelVersion?.trained_words, + data.model_version?.trainedWords, + data.model_version?.trained_words, + data.version?.trainedWords, + data.version?.trained_words, + data.path_metadata?.trainedWords, + data.path_metadata?.trained_words, + data.metadata?.trainedWords, + data.metadata?.trained_words + ]; + + return this.normalizeTrainedWords( + sources.flatMap(source => this.normalizeTrainedWords(source)) + ); + }, + + setInfoTableRowVisible(row, visible) { + if (!row) return; + row.classList.toggle('mr-hidden-initial', !visible); + row.style.display = visible ? '' : 'none'; + }, + + setInfoElementVisible(element, visible) { + if (!element) return; + element.classList.toggle('mr-hidden-initial', !visible); + element.style.display = visible ? '' : 'none'; + }, + updateSelectedTrainedWordsSummary(dialog) { if (!dialog) return; @@ -639,13 +804,19 @@ export const modelInfoMethods = { */ async fetchModelInfoForDialog(loraName, modelData, dialog) { try { + const resolvedPath = modelData?.resolved_path + || modelData?.path + || modelData?.full_path + || modelData?.folder_path + || modelData?.download_directory + || ''; const response = await api.fetchApi('/model_resolver/civitai-search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: loraName, category: modelData?.category || '', - resolved_path: modelData?.resolved_path || '' + resolved_path: resolvedPath }) }); @@ -706,12 +877,39 @@ export const modelInfoMethods = { } } + // Update top tags + const tagsRowEl = dialog.querySelector('.mr-info-tags-row'); + const tagsEl = dialog.querySelector('.mr-info-tags'); + if (tagsRowEl && tagsEl) { + const tags = this.getInfoDialogTags(data); + if (tags.length > 0) { + tagsEl.innerHTML = tags.map(tag => ` + + ${this.escapeHtml(tag)} + + `).join(''); + this.setInfoElementVisible(tagsRowEl, true); + this.bindTooltips(tagsEl); + } else { + tagsEl.innerHTML = ''; + this.setInfoElementVisible(tagsRowEl, false); + } + } + // Update file const fileEl = dialog.querySelector('.mr-info-file'); if (fileEl && data.filename) { fileEl.textContent = data.filename; } + // Update location + const locationEl = dialog.querySelector('.mr-info-location'); + if (locationEl) { + const location = this.formatInfoDialogLocation(data); + locationEl.textContent = location; + this.setInfoTableRowVisible(locationEl.closest('tr'), Boolean(location)); + } + // Update hash const hashEl = dialog.querySelector('.mr-info-hash'); if (hashEl) { @@ -752,17 +950,22 @@ export const modelInfoMethods = { const baseModel = data.base_model || data.baseModel || ''; baseModelRowEl.textContent = baseModel; const row = baseModelRowEl.closest('tr'); - if (row && baseModel) { - row.style.display = ''; - } else if (row) { - row.style.display = 'none'; - } + this.setInfoTableRowVisible(row, Boolean(baseModel)); + } + + // Update file size row + const sizeEl = dialog.querySelector('.mr-info-size'); + if (sizeEl) { + const sizeLabel = this.formatInfoDialogSize(data); + sizeEl.textContent = sizeLabel; + this.setInfoTableRowVisible(sizeEl.closest('tr'), Boolean(sizeLabel)); } // Update trained words const trainedWordsEl = dialog.querySelector('.mr-info-trained-words'); if (trainedWordsEl) { - const words = this.normalizeTrainedWords(data.trained_words || data.trainedWords || []); + const words = this.getInfoDialogTrainedWords(data); + const row = trainedWordsEl.closest('tr'); if (words.length > 0) { dialog._selectedTrainedWords = new Set(); trainedWordsEl.innerHTML = `
${words.map(word => ` @@ -776,13 +979,11 @@ export const modelInfoMethods = { ${this.escapeHtml(word)} `).join('')}
`; - const row = trainedWordsEl.closest('tr'); - if (row) row.style.display = ''; + this.setInfoTableRowVisible(row, true); this.bindTooltips(trainedWordsEl); this.updateSelectedTrainedWordsSummary(dialog); } else { - const row = trainedWordsEl.closest('tr'); - if (row) row.style.display = 'none'; + this.setInfoTableRowVisible(row, false); } } @@ -790,13 +991,12 @@ export const modelInfoMethods = { const clipSkipEl = dialog.querySelector('.mr-info-clip-skip'); if (clipSkipEl) { const clipSkip = data.clip_skip || data.clipSkip; + const row = clipSkipEl.closest('tr'); if (clipSkip && clipSkip !== 'None') { clipSkipEl.textContent = clipSkip; - const row = clipSkipEl.closest('tr'); - if (row) row.style.display = ''; + this.setInfoTableRowVisible(row, true); } else { - const row = clipSkipEl.closest('tr'); - if (row) row.style.display = 'none'; + this.setInfoTableRowVisible(row, false); } } @@ -823,9 +1023,7 @@ export const modelInfoMethods = { descEl.classList.remove('is-expanded'); const shouldCollapse = textOnly.length > 520 || finalHtml.length > 900; - if (actionsEl) { - actionsEl.style.display = shouldCollapse ? '' : 'none'; - } + this.setInfoElementVisible(actionsEl, shouldCollapse); if (toggleBtn) { toggleBtn.textContent = 'Show more'; } @@ -834,10 +1032,10 @@ export const modelInfoMethods = { } const row = descEl.closest('tr'); - if (row) row.style.display = ''; + this.setInfoTableRowVisible(row, true); } else { const row = descEl.closest('tr'); - if (row) row.style.display = 'none'; + this.setInfoTableRowVisible(row, false); } }