Files
DemonGatanjieu-Anomalous_Mo…/web/modules/model_source.js
T
DemonGatanjieuandClaude Opus 5.5 e466e9dfcf feat(scan): list unmatched and unscanned models, show each scan's result, explain why
The scan page now says which models are unmatched or not scanned yet (type, folder,
why Civitai had nothing) and what each scan did, model by model; rows open the model,
scan it, look it up again or search Civitai. The progress box shows the counts and,
when floating, a way to the result. The activity log records a scan when it ends with
its result, and model edits (from -> to) and deletions (files moved) in detail.

- scraper.py reports one JSON line per changed model (--report-file); a failing model
  no longer ends the scan. Unmatched info keeps why (not_found / network / offline);
  online scans look network/offline ones up again. After two models in a row without
  an answer the rest of the scan infers from the files (--civitai-down for the next
  folders) instead of waiting out every timeout.
- civitai_client.py: the network side moved out of scraper.py; tells a 404 from
  Civitai being unreachable (CivitaiUnreachable).
- api/scan_report.py: ScanJob turns a scan's reports into one result, kept as the last
  scan (/anomalous/last_scan) and logged; /anomalous/scan_model opens a listed model.
- /anomalous/scan_all takes picked or listed models as `targets`, so one scan has one
  result; scan_summary lists unmatched and new models with their reason.
- Activity middleware takes details a handler attaches (request[ACTIVITY_DETAIL]).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:52:17 +08:00

64 lines
3.2 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') {
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)}`);
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') : '';
}