Spaces:
Runtime error
Runtime error
| <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> | |