feat: enrich model info dialog with location, size, tags, and trained words from metadata

This commit is contained in:
Dariusz L
2026-06-23 19:47:49 +02:00
parent ee7d59546c
commit 093b6d33a7
4 changed files with 434 additions and 38 deletions
+16 -1
View File
@@ -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:
+161 -16
View File
@@ -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
+39 -1
View File
@@ -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;
+218 -20
View File
@@ -300,12 +300,20 @@ export const modelInfoMethods = {
<span class="mr-info-tag mr-info-type"></span>
<span class="mr-info-tag mr-info-basemodel"></span>
</div>
<div class="mr-info-tags-row mr-hidden-initial">
<span class="mr-info-tags-label">Tags</span>
<div class="mr-info-tags"></div>
</div>
<table class="mr-info-table">
<tbody>
<tr class="mr-info-file-row">
<td><span>File <span class="mr-tooltip-badge" data-tooltip="The model file name found locally or returned by CivitAI.">?</span></span></td>
<td><span class="mr-info-file"></span></td>
</tr>
<tr class="mr-info-location-row mr-hidden-initial">
<td><span>Location <span class="mr-tooltip-badge" data-tooltip="Folder where this local model file is stored.">?</span></span></td>
<td><span class="mr-info-location"></span></td>
</tr>
<tr class="mr-info-hash-row">
<td><span>Hash (sha256) <span class="mr-tooltip-badge" data-tooltip="Unique fingerprint of the local file. Model Resolver uses it to confirm the exact CivitAI version.">?</span></span></td>
<td><span class="mr-info-hash"></span></td>
@@ -322,6 +330,10 @@ export const modelInfoMethods = {
<td><span>Base Model <span class="mr-tooltip-badge" data-tooltip="Base model this resource was made for, for example SD1.5, SDXL or Flux.">?</span></span></td>
<td><span class="mr-info-base-model"></span></td>
</tr>
<tr class="mr-info-size-row mr-hidden-initial">
<td><span>Size <span class="mr-tooltip-badge" data-tooltip="The local model file size, read from metadata or from the file on disk.">?</span></span></td>
<td><span class="mr-info-size"></span></td>
</tr>
<tr class="mr-info-trainedwords-row mr-hidden-initial">
<td>
<div class="mr-info-trained-words-label">
@@ -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 => `
<span class="mr-info-tag mr-info-model-tag" data-tooltip="${this.escapeHtml(tag)}">
${this.escapeHtml(tag)}
</span>
`).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 = `<div class="mr-info-trained-words-list">${words.map(word => `
@@ -776,13 +979,11 @@ export const modelInfoMethods = {
${this.escapeHtml(word)}
</button>
`).join('')}</div>`;
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);
}
}