JunhanCai's picture
Upload folder using huggingface_hub
8ccb998 verified
Raw
History Blame Contribute Delete
19.8 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GEMS Raman Webserver</title>
<style>
:root {
color-scheme: light;
--bg: #f4f0e8;
--panel: rgba(255, 255, 255, 0.88);
--panel-border: rgba(33, 37, 41, 0.12);
--text: #1f2937;
--muted: #5b6472;
--accent: #2563a8;
--accent-strong: #1e40af;
--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%),
radial-gradient(circle at top right, rgba(186, 92, 39, 0.12), transparent 24%),
linear-gradient(180deg, #faf7f1 0%, var(--bg) 100%);
}
.wrap { max-width: 1120px; margin: 0 auto; padding: 32px 20px 56px; }
.hero { margin-bottom: 22px; }
h1 { margin: 0 0 10px; font-size: 34px; letter-spacing: -0.02em; }
.subtitle { margin: 0; max-width: 880px; color: var(--muted); line-height: 1.6; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.card {
border: 1px solid var(--panel-border);
border-radius: 18px;
padding: 18px;
margin-top: 16px;
background: var(--panel);
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}
.card h2 { margin: 0 0 8px; font-size: 22px; }
.card p { margin-top: 0; }
label { display: block; font-size: 14px; margin-bottom: 6px; color: var(--muted); }
input, button { width: 100%; padding: 10px 12px; box-sizing: border-box; }
input { border: 1px solid #cdd5df; border-radius: 10px; background: #fff; }
button { background: linear-gradient(135deg, var(--accent), #3b82f6); color: #fff; border: none; border-radius: 10px; cursor: pointer; font-weight: 600; }
button:hover { background: linear-gradient(135deg, var(--accent-strong), #1d4ed8); }
.note { font-size: 13px; color: var(--muted); line-height: 1.6; }
.section-title { margin: 0 0 10px; font-size: 17px; }
.stack { display: grid; gap: 12px; }
.footer-note { margin-top: 18px; font-size: 13px; color: var(--muted); }
.live-status {
display: none;
margin-top: 22px;
}
.live-status.visible { display: block; }
.live-status iframe {
width: 100%;
min-height: 980px;
border: 0;
border-radius: 18px;
background: #fff;
box-shadow: var(--shadow);
}
.live-status .status-head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
margin-bottom: 10px;
padding: 0 2px;
}
.live-status .status-title {
font-size: 20px;
margin: 0;
}
.live-status .status-note {
color: var(--muted);
font-size: 13px;
}
.file-field { display: grid; gap: 8px; align-content: start; }
.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; }
.result-panel {
display: none;
margin-top: 22px;
}
.result-panel.visible { display: block; }
.result-panel .result-head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
margin-bottom: 10px;
padding: 0 2px;
}
.result-panel .result-title {
font-size: 20px;
margin: 0;
}
.result-panel .result-note {
color: var(--muted);
font-size: 13px;
}
.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;
}
.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; }
pre { white-space: pre-wrap; word-wrap: break-word; background: #f9fafb; padding: 14px; border-radius: 12px; overflow-x: auto; }
.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="hero">
<h1>GEMS Webserver for Raman spectroscopy</h1>
<p class="subtitle">Accelerate your spectral analysis with the GEMS foundation model. Please choose your desired workflow below:</p>
<ul class="workflow-instructions" style="list-style-type: none; padding-left: 0; margin-top: 20px;">
<li style="margin-bottom: 12px; line-height: 1.6;">
<strong style="color: #2563a8;">Fine-tune (Left Panel):</strong> Upload your raw spectral data and labels. The system will automatically execute a complete AutoML pipeline before fine-tuning the pretrained GEMS model and generating comprehensive evaluation reports.
</li>
<li style="line-height: 1.6;">
<strong style="color: #2563a8;">Predict (Right Panel):</strong> Upload a previously fine-tuned <code style="background: #E2E8F0; padding: 2px 6px; border-radius: 4px; font-family: monospace;">.pth</code> model weight file alongside your unknown spectral data for instant classification and confident Top-5 logit predictions.
</li>
</ul>
</div>
<div class="grid">
<form class="card stack" action="/start" method="post" enctype="multipart/form-data">
<div>
<h2>Fine-tune</h2>
<p class="note">Upload the raw training data and a pretrained model to generate a new classifier checkpoint and test results.</p>
</div>
<div class="file-field">
<label>True Label Mapping File (optional, .json/.txt)</label>
<div class="file-control">
<button type="button" data-file-target="train_label_mapping_file">Choose file</button>
<span class="file-name" data-file-name="train_label_mapping_file">No file selected</span>
<input type="file" id="train_label_mapping_file" name="label_mapping_file" accept=".json,.txt">
</div>
</div>
<div class="grid">
<div class="file-field">
<label>Spectral (.npy)</label>
<div class="file-control">
<button type="button" data-file-target="spectral_file">Choose file</button>
<span class="file-name" data-file-name="spectral_file">No file selected</span>
<input type="file" id="spectral_file" name="spectral_file" accept=".npy" required>
</div>
</div>
<div class="file-field">
<label>Labels (.npy)</label>
<div class="file-control">
<button type="button" data-file-target="labels_file">Choose file</button>
<span class="file-name" data-file-name="labels_file">No file selected</span>
<input type="file" id="labels_file" name="labels_file" accept=".npy" required>
</div>
</div>
<div class="file-field">
<label>Wavenumbers (.npy)</label>
<div class="file-control">
<button type="button" data-file-target="wavenumbers_file">Choose file</button>
<span class="file-name" data-file-name="wavenumbers_file">No file selected</span>
<input type="file" id="wavenumbers_file" name="wavenumbers_file" accept=".npy" required>
</div>
</div>
<div class="file-field">
<label>Pretrained Model (.pth) <span style="font-weight: normal; color: var(--muted);">- Optional</span></label>
<div class="file-control">
<button type="button" data-file-target="model_file">Choose file</button>
<span class="file-name" data-file-name="model_file">No file selected</span>
<input type="file" id="model_file" name="model_file" accept=".pth">
</div>
<div class="helper-note">
💡 Leave empty for the built-in <strong>GEMS Model</strong>. Upload to resume your checkpoint.
</div>
</div>
</div>
<div class="grid">
<div><label>Epochs</label><input type="number" name="epochs" value="60"></div>
<div><label>Batch Size</label><input type="number" name="batch_size" value="64"></div>
<div><label>Learning Rate</label><input type="number" step="0.000001" name="lr" value="0.0001"></div>
<div><label>Weight Decay</label><input type="number" step="0.0001" name="weight_decay" value="0.001"></div>
<div><label>Patience</label><input type="number" name="patience" value="12"></div>
<div><label>Label Smoothing</label><input type="number" step="0.01" name="label_smoothing" value="0.0"></div>
</div>
<div>
<button type="submit">Start Fine-Tuning Job</button>
</div>
</form>
<form class="card stack" action="/predict" method="post" enctype="multipart/form-data">
<div>
<h2>Predict</h2>
<p class="note">Upload the fine-tuned classifier checkpoint exported after training, such as final_model.pth. Then upload the spectrum file you want to classify. If the spectrum file already includes wavenumbers, you can leave the optional wavelength file empty.</p>
</div>
<div class="stack">
<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>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>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/.csv)</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>Low value</label>
<input type="number" step="0.0001" name="manual_low_cm" placeholder="e.g. 0">
</div>
<div>
<label>High value</label>
<input type="number" step="0.0001" name="manual_high_cm" placeholder="e.g. 3500">
</div>
</div>
</div>
</div>
<div>
<button type="submit">Run Prediction</button>
</div>
</form>
</div>
<div class="live-status" id="live-status">
<div class="status-head">
<h2 class="status-title">Live Training Status</h2>
<div class="status-note">The results panel will appear here after you start a job.</div>
</div>
<iframe id="status-frame" title="Training status"></iframe>
</div>
<div class="result-panel" id="predict-results">
<div class="result-head">
<h2 class="result-title">Prediction Results</h2>
<div class="result-note">The results will appear here after you run prediction.</div>
</div>
<div id="predict-results-body"></div>
</div>
<div class="footer-note">After training finishes, the status page will show t-SNE, the confusion matrix, the classification report, and download links.</div>
</div>
<script>
const liveStatus = document.getElementById('live-status');
const statusFrame = document.getElementById('status-frame');
const predictResults = document.getElementById('predict-results');
const predictResultsBody = document.getElementById('predict-results-body');
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();
document.querySelectorAll('form[action="/start"]').forEach((form) => {
form.addEventListener('submit', async (event) => {
if (!form.checkValidity()) {
form.reportValidity();
return;
}
event.preventDefault();
const submitButton = form.querySelector('button[type="submit"]');
const originalLabel = submitButton ? submitButton.textContent : '';
if (submitButton) {
submitButton.disabled = true;
submitButton.textContent = 'Uploading data & Initializing...';
}
try {
const response = await fetch(form.action, {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: new FormData(form),
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const payload = await response.json();
const targetUrl = payload.status_url;
if (statusFrame) {
statusFrame.src = targetUrl;
}
if (liveStatus) {
liveStatus.classList.add('visible');
liveStatus.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
} catch (error) {
alert(`Failed to start the training job: ${error}`);
} finally {
if (submitButton) {
submitButton.disabled = false;
submitButton.textContent = originalLabel;
}
}
});
});
document.querySelectorAll('form[action="/predict"]').forEach((form) => {
form.addEventListener('submit', async (event) => {
if (!form.checkValidity()) {
form.reportValidity();
return;
}
event.preventDefault();
const submitButton = form.querySelector('button[type="submit"]');
const originalLabel = submitButton ? submitButton.textContent : '';
if (submitButton) {
submitButton.disabled = true;
submitButton.textContent = 'Running...';
}
try {
const response = await fetch(form.action, {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: new FormData(form),
credentials: 'same-origin'
});
const payload = await response.json().catch(() => null);
if (!response.ok) {
const message = payload && payload.detail ? payload.detail : `HTTP ${response.status}`;
throw new Error(message);
}
if (predictResultsBody && payload && payload.results_html) {
predictResultsBody.innerHTML = payload.results_html;
}
if (predictResults) {
predictResults.classList.add('visible');
predictResults.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
} catch (error) {
alert(`Failed to run prediction: ${error.message || error}`);
} finally {
if (submitButton) {
submitButton.disabled = false;
submitButton.textContent = originalLabel;
}
}
});
});
</script>
</body>
</html>