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 = { +
| File ? | |||
| Location ? | ++ | ||
| Hash (sha256) ? | @@ -322,6 +330,10 @@ export const modelInfoMethods = { | 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);
}
}
|