/* 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(''); }