/* Model Insights — renders everything from /static/data/model_card.json */
const pretty = (s) => s.replace(/_/g, ' ');
const f1class = (v) => (v >= 0.8 ? 'good' : v >= 0.5 ? 'mid' : 'bad');
fetch('/static/data/model_card.json')
.then(r => r.json())
.then(render)
.catch(() => {
document.getElementById('kpis').innerHTML =
'
Could not load model_card.json
';
});
function render(card) {
const m = card.metrics, ds = card.dataset, mdl = card.model, hp = card.hyperparameters;
// ── KPIs ──
const kpis = [
{ v: (m.accuracy * 100).toFixed(1) + '%', l: 'Accuracy' },
{ v: m.macro_f1.toFixed(3), l: 'Macro F1' },
{ v: ds.classes, l: 'Classes' },
{ v: ds.total_images.toLocaleString(), l: 'Images' },
{ v: mdl.trainable_params_millions + 'M', l: 'Parameters' },
{ v: mdl.weights_size_mb + 'MB', l: 'Model size' },
];
document.getElementById('kpis').innerHTML = kpis.map(k =>
`${k.v}${k.l}
`).join('');
// ── Pipeline ──
document.getElementById('pipeline').innerHTML = card.pipeline.map(p =>
`${p.step}${p.detail}
`).join('');
// ── Dataset donut (train vs test) ──
const trainPct = Math.round(ds.train_images / ds.total_images * 100);
const accent = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#2EA866';
document.getElementById('donut').style.background =
`conic-gradient(${accent} 0 ${trainPct}%, var(--bg-tint) ${trainPct}% 100%)`;
document.getElementById('donutLegend').innerHTML = `
Train · ${ds.train_images.toLocaleString()}
Test · ${ds.test_images.toLocaleString()}
`;
document.getElementById('dsFacts').innerHTML = `
${ds.classes}dish classes
${ds.per_class}images / class
${ds.split}split
${ds.balanced ? 'Balanced' : 'Imbalanced'}distribution
`;
document.getElementById('augChips').innerHTML =
card.augmentations.map(a => `${a}`).join('');
// ── Model card + hyperparameters ──
const rows = (obj) => Object.entries(obj).map(([k, v]) =>
`| ${k} | ${v} |
`).join('');
document.getElementById('modelSpec').innerHTML = rows({
'Name': mdl.published_as,
'Architecture': mdl.architecture,
'Framework': mdl.framework,
'Parameters': mdl.trainable_params_millions + ' M',
'Weights size': mdl.weights_size_mb + ' MB',
'Input': mdl.input,
'Output': mdl.output,
});
document.getElementById('hpSpec').innerHTML = rows({
'Epochs': hp.epochs,
'Learning rate': hp.learning_rate,
'Train batch size': hp.train_batch_size,
'Eval batch size': hp.eval_batch_size,
'Weight decay': hp.weight_decay,
'Warmup steps': hp.warmup_steps,
'Optimizer steps': hp.optimizer_steps,
'Runtime': hp.runtime_min + ' min (GPU)',
});
// ── Train compare (baseline vs final) ──
document.getElementById('trainCompare').innerHTML = `
${(m.baseline_accuracy * 100).toFixed(1)}%Baseline accuracy
${(m.accuracy * 100).toFixed(1)}%After fine-tuning
${m.macro_f1.toFixed(3)}Macro F1
`;
document.getElementById('curveNote').textContent =
'Per-epoch loss/accuracy curves are not plotted: the training run only logged baseline and final metrics. Enable epoch logging in the notebook to capture full curves.';
// ── Per-class F1 bars ──
const sorted = [...card.per_class].sort((a, b) => b.f1 - a.f1);
renderBars('bestBars', sorted.slice(0, 10));
renderBars('worstBars', sorted.slice(-10).reverse());
renderBars('allBars', sorted);
// ── Limitations ──
document.getElementById('limits').innerHTML =
card.limitations.map(l => `${l}`).join('');
// animate bar widths after paint
requestAnimationFrame(() => requestAnimationFrame(() => {
document.querySelectorAll('.bar-fill').forEach(el => { el.style.width = el.dataset.w + '%'; });
}));
}
function renderBars(id, items) {
document.getElementById(id).innerHTML = items.map(c => `
${pretty(c.label)}
${c.f1.toFixed(2)}
`).join('');
}