Files
DemonGatanjieu-Anomalous_Mo…/web/modules/model_source.js
T
DemonGatanjieuandClaude Opus 5.5 45353ffea1 Scans name damaged model files instead of calling them unknown
A model Civitai has no record of now has its safetensors header checked (model_identity.
safetensors_damage: too short, header longer than the file, not JSON, or a tensor ending past
the file's end; only the header is read). Such files get the unmatched reason "damaged": a red
badge on the card, a line in the details, a warning on the scan page (the summary counts them)
and first place in the unmatched list, with advice to download the model again.

Tests: header cases, a scan telling a damaged file from a whole one; the scan report fixtures
are whole safetensors files now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:35:23 +08:00

71 lines
3.7 KiB
JavaScript

/**
* Where a model's information came from, shown on cards and in the detail header.
* Matched on Civitai is the normal case and looks as before; only the exceptions are
* marked: base model inferred from the file (≈), and not scanned yet. The detail header
* also names the fields the user set (metadata.user_fields, from <model>.anomalous.json).
*/
import { translate as t } from './locales.js';
import { escapeHtml } from './safe_dom.js';
import { reasonText } from './scan_results.js';
const FIELD_KEYS = { custom_name: 'modelSourceFieldName', custom_notes: 'modelSourceFieldNotes', source_url: 'modelSourceFieldLink' };
function colourClass(baseModel) {
const lower = baseModel.toLowerCase();
if (lower.includes('flux')) return 'badge-flux';
if (lower.includes('pony') || lower.includes('illustrious') || lower.includes('anime')) return 'badge-rose';
if (lower.includes('sdxl') || lower.includes('xl')) return 'badge-gold';
return 'badge-amber';
}
/** Only .safetensors files are scanned, so only they can be "not scanned yet". */
function scannable(model) {
return /\.safetensors$/i.test(String(model?.filename || ''));
}
/** The card's top-left badge, or null. */
export function renderSourceBadge(model) {
const meta = model?.metadata || {};
const baseModel = String(meta.baseModel || '');
const badge = document.createElement('div');
badge.className = 'anomalous-card-badge';
if (meta.info_source === 'local' && meta.unmatched_reason === 'damaged') {
badge.classList.add('is-inferred', 'is-damaged');
badge.textContent = `⚠ ${t('modelSourceDamagedShort')}`;
badge.title = reasonText('damaged');
} else if (meta.info_source === 'local') {
badge.classList.add('is-inferred', ...(baseModel ? [colourClass(baseModel)] : []));
badge.textContent = `≈ ${baseModel || t('modelSourceLocalShort')}`;
badge.title = t('modelSourceLocalHint', { reason: reasonText(meta.unmatched_reason) });
} else if (!meta.info_source && scannable(model)) {
badge.classList.add('is-unscanned');
badge.textContent = t('modelSourceNotScanned');
badge.title = t('modelSourceNotScannedHint');
} else if (baseModel) {
badge.classList.add(colourClass(baseModel));
badge.textContent = baseModel;
} else {
return null;
}
return badge;
}
/** The detail header's line: size, base model, where the information came from, what the user set. */
export function renderSourceLine(span, model, meta) {
const parts = [`<strong>Size:</strong> ${escapeHtml(model.size_mb)} MB`];
if (meta.baseModel) parts.push(`<strong>Base:</strong> ${meta.info_source === 'local' ? '≈ ' : ''}${escapeHtml(meta.baseModel)}`);
const source = meta.info_source === 'civitai' ? t('modelSourceCivitai')
: meta.info_source === 'local' ? t('modelSourceLocal')
: scannable(model) ? t('modelSourceNotScanned') : '';
if (source) parts.push(`<strong>${escapeHtml(t('modelSourceLabel'))}:</strong> ${escapeHtml(source)}`);
if (meta.info_source === 'local' && meta.unmatched_reason === 'damaged') {
parts.push(`<span class="anomalous-source-damaged">⚠ ${escapeHtml(t('modelSourceDamaged'))}</span>`);
}
const edited = (meta.user_fields || []).map(field => t(FIELD_KEYS[field] || field));
if (edited.length) parts.push(escapeHtml(t('modelSourceEdited', { fields: edited.join(t('modelSourceListSep')) })));
span.innerHTML = parts.join('<span class="anomalous-source-sep"> · </span>');
span.title = meta.info_source === 'local' ? t('modelSourceLocalHint', { reason: reasonText(meta.unmatched_reason) })
: !meta.info_source && scannable(model) ? t('modelSourceNotScannedHint') : '';
}