Download webserver/templates/predict.html from JunhanCai/GEMS-Generalized-Raman-Classifier: direct link, hf CLI and curl.
- Browser
- Download file 11.2 kB
-
https://huggingface.co/spaces/JunhanCai/GEMS-Generalized-Raman-Classifier/resolve/main/webserver/templates/predict.html
- Command line
-
hf download hf://spaces/JunhanCai/GEMS-Generalized-Raman-Classifier/webserver/templates/predict.html
-
curl -L -o predict.html https://huggingface.co/spaces/JunhanCai/GEMS-Generalized-Raman-Classifier/resolve/main/webserver/templates/predict.html
11.2 kB
| <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> | |