JunhanCai's picture
Initial commit with GEMS model and Dockerfile
6918c6b
Raw
History Blame Contribute Delete
11.2 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Prediction Results</title>
<style>
:root {
color-scheme: light;
--bg: #f4f0e8;
--panel: rgba(255, 255, 255, 0.9);
--panel-border: rgba(33, 37, 41, 0.12);
--text: #1f2937;
--muted: #5b6472;
--accent: #2563a8;
--shadow: 0 18px 50px rgba(32, 41, 48, 0.12);
}
body {
margin: 0;
font-family: "Segoe UI", Tahoma, sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(37, 99, 168, 0.14), transparent 28%),
linear-gradient(180deg, #faf7f1 0%, var(--bg) 100%);
}
.wrap { max-width: 1180px; margin: 0 auto; padding: 32px 20px 56px; }
.card { border: 1px solid var(--panel-border); border-radius: 18px; padding: 18px; background: var(--panel); box-shadow: var(--shadow); backdrop-filter: blur(10px); margin-top: 16px; }
.topline { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; align-items: center; margin-bottom: 16px; }
.pill { display: inline-flex; align-items: center; gap: 8px; border-radius: 999px; padding: 8px 12px; background: rgba(37, 99, 168, 0.1); color: var(--accent); font-weight: 700; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.stat { padding: 12px 14px; border-radius: 14px; background: rgba(37, 99, 168, 0.06); border: 1px solid rgba(37, 99, 168, 0.12); }
.stat .k { font-size: 12px; color: var(--muted); margin-bottom: 6px; }
.stat .v { font-size: 18px; font-weight: 700; }
.meta { color: var(--muted); font-size: 14px; line-height: 1.6; }
table { width: 100%; border-collapse: collapse; overflow: hidden; }
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid rgba(0,0,0,0.08); font-size: 14px; }
th { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
pre { white-space: pre-wrap; word-wrap: break-word; background: #f9fafb; padding: 14px; border-radius: 12px; overflow-x: auto; }
.file-field { display: grid; gap: 8px; }
.file-control {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
min-width: 0;
padding: 10px 12px;
border: 1px solid #cdd5df;
border-radius: 10px;
background: #fff;
box-sizing: border-box;
overflow: hidden;
}
.file-control button {
width: 110px;
flex: 0 0 110px;
white-space: nowrap;
padding: 8px 12px;
border-radius: 8px;
}
.file-name {
color: var(--muted);
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
flex: 1 1 auto;
}
.file-control input[type="file"] { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.preview-img {
width: 100%;
height: auto;
border-radius: 12px;
border: 1px solid rgba(0,0,0,0.08);
background: white;
}
.top5-grid {
display: grid;
gap: 8px;
}
.top5-item {
padding: 10px 12px;
border-radius: 12px;
background: rgba(37, 99, 168, 0.06);
border: 1px solid rgba(37, 99, 168, 0.12);
font-size: 13px;
line-height: 1.6;
}
.helper-note {
color: var(--muted);
font-size: 13px;
line-height: 1.6;
margin-top: 8px;
}
</style>
</head>
<body>
<div class="wrap">
<div class="topline">
<div>
<h2>Prediction Results: {{ prediction_id }}</h2>
<div class="meta">The model checkpoint has been loaded and predictions have been generated.</div>
</div>
<div class="pill">DONE</div>
</div>
<div class="card">
<form action="/predict" method="post" enctype="multipart/form-data" class="grid" style="margin-bottom: 16px;">
<div class="file-field">
<label>Saved Model (.pth)</label>
<div class="file-control">
<button type="button" data-file-target="predict_model_file">Choose file</button>
<span class="file-name" data-file-name="predict_model_file">No file selected</span>
<input type="file" id="predict_model_file" name="model_file" accept=".pth" required>
</div>
</div>
<div class="file-field">
<label>True Label Mapping File (optional, .json/.txt)</label>
<div class="file-control">
<button type="button" data-file-target="predict_label_mapping_file">Choose file</button>
<span class="file-name" data-file-name="predict_label_mapping_file">No file selected</span>
<input type="file" id="predict_label_mapping_file" name="label_mapping_file" accept=".json,.txt">
</div>
</div>
<div class="file-field">
<label>Spectral (.npy/.txt/.csv)</label>
<div class="file-control">
<button type="button" data-file-target="predict_spectral_file">Choose file</button>
<span class="file-name" data-file-name="predict_spectral_file">No file selected</span>
<input type="file" id="predict_spectral_file" name="spectral_file" accept=".npy,.txt,.csv" required>
</div>
</div>
<div class="file-field">
<label>Wavelengths / Wavenumbers (optional, .npy/.txt)</label>
<div class="file-control">
<button type="button" data-file-target="predict_wavenumbers_file">Choose file</button>
<span class="file-name" data-file-name="predict_wavenumbers_file">No file selected</span>
<input type="file" id="predict_wavenumbers_file" name="wavenumbers_file" accept=".npy,.txt,.csv">
</div>
</div>
<div class="file-field">
<label>Manual wavelength range fallback (optional)</label>
<div class="grid" style="grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;">
<div>
<label style="margin-bottom: 4px;">Low value</label>
<input type="number" step="0.0001" name="manual_low_cm" placeholder="e.g. 0">
</div>
<div>
<label style="margin-bottom: 4px;">High value</label>
<input type="number" step="0.0001" name="manual_high_cm" placeholder="e.g. 3500">
</div>
</div>
</div>
<div style="grid-column: 1 / -1;">
<button type="submit" style="width: 100%;">Run Prediction</button>
</div>
</form>
<div class="helper-note">
If your spectrum file already contains two columns (wavelength and intensity), you can leave the wavelength file empty.
If the spectrum file contains only intensity values, upload a wavelength file or fill in the manual range.
</div>
<div class="stats">
<div class="stat"><div class="k">Samples</div><div class="v">{{ summary.num_samples }}</div></div>
<div class="stat"><div class="k">Classes</div><div class="v">{{ summary.class_names|length }}</div></div>
<div class="stat"><div class="k">Download CSV</div><div class="v"><a href="{{ download_csv }}">CSV</a></div></div>
</div>
<div class="meta" style="margin-top: 12px;">If you need the same checkpoint for another dataset, keep the exported pth file and upload it here again.</div>
</div>
<div class="grid">
<div class="card">
<h3>Per-sample Predictions</h3>
<table>
<thead>
<tr>
<th>Sample</th>
<th>Predicted class</th>
<th>Confidence</th>
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
<td>{{ row.sample_index }}</td>
<td>{{ row.pred_label }}</td>
<td>{{ "%.4f"|format(row.confidence) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card">
<h3>Top-5 Logits</h3>
<div class="top5-grid">
{% for row in top5_rows %}
<div class="top5-item">
<strong>Sample {{ row.sample_index }}</strong>
<div>
{% for item in row.top5 %}
<div>Top{{ item.rank }}: {{ item.class_name }} - {{ "%.6f"|format(item.logit) }}</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="grid">
<div class="card">
<h3>Input Spectra Preview</h3>
<img class="preview-img" src="{{ preview_image }}" alt="Input spectra preview">
</div>
</div>
<div class="card">
<h3>Model Notes</h3>
<div class="meta">The model checkpoint was loaded with its saved architecture and preprocessing config, so the same pth file can be reused later for inference on new spectra.</div>
{% if summary.spectrum_source or summary.wavenumber_source %}
<div class="helper-note">Spectrum source: {{ summary.spectrum_source }} | Wavelength source: {{ summary.wavenumber_source }}</div>
{% endif %}
{% if summary.label_mapping_source %}
<div class="helper-note">Label mapping source: {{ summary.label_mapping_source }}</div>
{% endif %}
</div>
<p style="margin-top: 12px;"><a href="/">Back</a></p>
</div>
<script>
document.querySelectorAll('[data-file-target]').forEach((button) => {
button.addEventListener('click', () => {
const targetId = button.getAttribute('data-file-target');
const input = document.getElementById(targetId);
if (input) {
input.click();
}
});
});
document.querySelectorAll('input[type="file"]').forEach((input) => {
input.addEventListener('change', () => {
const nameNode = document.querySelector(`[data-file-name="${input.id}"]`);
if (nameNode) {
const fileName = input.files.length ? input.files[0].name : 'No file selected';
nameNode.textContent = fileName;
nameNode.title = fileName;
}
});
});
const syncHoverText = () => {
document.querySelectorAll('input:not([type="file"]):not([type="hidden"])').forEach((input) => {
const hoverText = input.value || input.placeholder || input.getAttribute('aria-label') || input.name || '';
input.title = hoverText;
});
document.querySelectorAll('.file-name').forEach((node) => {
node.title = node.textContent.trim();
});
};
document.querySelectorAll('input:not([type="file"]):not([type="hidden"])').forEach((input) => {
input.addEventListener('input', syncHoverText);
input.addEventListener('change', syncHoverText);
});
syncHoverText();
</script>
</body>
</html>