Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Intel Scene Classifier</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;800&display=swap" rel="stylesheet" /> | |
| <style> | |
| /* Reset & CSS vars */ | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #0a0a0f; | |
| --surface: #111118; | |
| --border: rgba(255,255,255,0.08); | |
| --accent: #7c6aff; | |
| --accent2: #ff6ab0; | |
| --text: #e8e8f0; | |
| --muted: #6b6b80; | |
| --success: #4dffb0; | |
| --radius: 16px; | |
| --mono: "Space Mono", monospace; | |
| --sans: "Syne", sans-serif; | |
| } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: var(--sans); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 40px 20px 80px; | |
| } | |
| /* Animated grid background */ | |
| body::before { | |
| content: ""; | |
| position: fixed; | |
| inset: 0; | |
| background-image: | |
| linear-gradient(rgba(124,106,255,0.04) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(124,106,255,0.04) 1px, transparent 1px); | |
| background-size: 48px 48px; | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| /* Header */ | |
| header { | |
| text-align: center; | |
| margin-bottom: 48px; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .logo-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| letter-spacing: 0.15em; | |
| color: var(--accent); | |
| background: rgba(124,106,255,0.1); | |
| border: 1px solid rgba(124,106,255,0.3); | |
| padding: 6px 16px; | |
| border-radius: 100px; | |
| margin-bottom: 20px; | |
| } | |
| .logo-badge::before { | |
| content: ""; | |
| width: 6px; height: 6px; | |
| background: var(--accent); | |
| border-radius: 50%; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; transform: scale(1); } | |
| 50% { opacity: 0.4; transform: scale(0.8); } | |
| } | |
| h1 { | |
| font-size: clamp(2rem, 5vw, 3.4rem); | |
| font-weight: 800; | |
| line-height: 1.1; | |
| background: linear-gradient(135deg, #fff 0%, var(--accent) 60%, var(--accent2) 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .subtitle { | |
| color: var(--muted); | |
| font-family: var(--mono); | |
| font-size: 13px; | |
| margin-top: 12px; | |
| letter-spacing: 0.05em; | |
| } | |
| /* Main card */ | |
| .card { | |
| width: 100%; | |
| max-width: 640px; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 36px; | |
| position: relative; | |
| z-index: 1; | |
| backdrop-filter: blur(12px); | |
| } | |
| .card + .card { margin-top: 24px; } | |
| /* Form */ | |
| .field-label { | |
| font-size: 11px; | |
| letter-spacing: 0.12em; | |
| color: var(--muted); | |
| font-family: var(--mono); | |
| text-transform: uppercase; | |
| margin-bottom: 10px; | |
| display: block; | |
| } | |
| /* Model selector */ | |
| .model-selector { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 12px; | |
| margin-bottom: 28px; | |
| } | |
| .model-option { display: none; } | |
| .model-btn { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 18px 12px; | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| background: transparent; | |
| user-select: none; | |
| } | |
| .model-btn:hover { | |
| border-color: var(--accent); | |
| background: rgba(124,106,255,0.06); | |
| } | |
| .model-option:checked + .model-btn { | |
| border-color: var(--accent); | |
| background: rgba(124,106,255,0.12); | |
| box-shadow: 0 0 0 1px var(--accent); | |
| } | |
| .model-btn .icon { | |
| font-size: 28px; | |
| } | |
| .model-btn .name { | |
| font-weight: 600; | |
| font-size: 14px; | |
| } | |
| .model-btn .tag { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--muted); | |
| } | |
| /* File upload zone */ | |
| .upload-zone { | |
| border: 2px dashed var(--border); | |
| border-radius: 12px; | |
| padding: 40px 20px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| position: relative; | |
| margin-bottom: 24px; | |
| } | |
| .upload-zone:hover, .upload-zone.drag-over { | |
| border-color: var(--accent); | |
| background: rgba(124,106,255,0.05); | |
| } | |
| .upload-zone input[type="file"] { | |
| position: absolute; | |
| inset: 0; | |
| opacity: 0; | |
| cursor: pointer; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .upload-icon { font-size: 40px; margin-bottom: 12px; } | |
| .upload-text { | |
| font-size: 15px; | |
| font-weight: 600; | |
| } | |
| .upload-hint { | |
| font-size: 12px; | |
| font-family: var(--mono); | |
| color: var(--muted); | |
| margin-top: 6px; | |
| } | |
| #file-name { | |
| margin-top: 10px; | |
| font-size: 12px; | |
| font-family: var(--mono); | |
| color: var(--accent); | |
| word-break: break-all; | |
| } | |
| /* Preview image */ | |
| #preview-wrap { | |
| display: none; | |
| margin-bottom: 24px; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| border: 1px solid var(--border); | |
| } | |
| #preview-wrap img { | |
| width: 100%; | |
| max-height: 260px; | |
| object-fit: cover; | |
| display: block; | |
| } | |
| /* Submit button */ | |
| .btn-predict { | |
| width: 100%; | |
| padding: 16px; | |
| background: linear-gradient(135deg, var(--accent), var(--accent2)); | |
| color: #fff; | |
| font-family: var(--sans); | |
| font-size: 16px; | |
| font-weight: 700; | |
| border: none; | |
| border-radius: 12px; | |
| cursor: pointer; | |
| transition: opacity 0.2s, transform 0.15s; | |
| letter-spacing: 0.03em; | |
| } | |
| .btn-predict:hover { opacity: 0.9; transform: translateY(-1px); } | |
| .btn-predict:active { transform: translateY(0); } | |
| /* Loading state */ | |
| .btn-predict.loading { | |
| pointer-events: none; | |
| opacity: 0.7; | |
| } | |
| /* Error banner */ | |
| .error-banner { | |
| background: rgba(255, 80, 80, 0.1); | |
| border: 1px solid rgba(255, 80, 80, 0.3); | |
| color: #ff7070; | |
| padding: 14px 18px; | |
| border-radius: 10px; | |
| font-family: var(--mono); | |
| font-size: 13px; | |
| margin-bottom: 24px; | |
| } | |
| /* Result section */ | |
| .result-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| margin-bottom: 28px; | |
| } | |
| .result-thumb { | |
| width: 80px; | |
| height: 80px; | |
| border-radius: 10px; | |
| object-fit: cover; | |
| border: 2px solid var(--accent); | |
| flex-shrink: 0; | |
| } | |
| .result-meta { flex: 1; } | |
| .result-label { | |
| font-size: 11px; | |
| font-family: var(--mono); | |
| color: var(--muted); | |
| letter-spacing: 0.1em; | |
| text-transform: uppercase; | |
| margin-bottom: 4px; | |
| } | |
| .result-class { | |
| font-size: 28px; | |
| font-weight: 800; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .result-conf { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| margin-top: 6px; | |
| font-family: var(--mono); | |
| font-size: 13px; | |
| color: var(--success); | |
| } | |
| .model-pill { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| letter-spacing: 0.1em; | |
| padding: 3px 10px; | |
| border-radius: 100px; | |
| background: rgba(124,106,255,0.15); | |
| border: 1px solid rgba(124,106,255,0.3); | |
| color: var(--accent); | |
| text-transform: uppercase; | |
| margin-top: 8px; | |
| } | |
| /* Probability bars */ | |
| .prob-list { display: flex; flex-direction: column; gap: 10px; } | |
| .prob-row { | |
| display: grid; | |
| grid-template-columns: 90px 1fr 54px; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .prob-name { | |
| font-size: 13px; | |
| font-family: var(--mono); | |
| color: var(--muted); | |
| white-space: nowrap; | |
| } | |
| .prob-bar-bg { | |
| height: 8px; | |
| background: rgba(255,255,255,0.05); | |
| border-radius: 100px; | |
| overflow: hidden; | |
| } | |
| .prob-bar-fill { | |
| height: 100%; | |
| border-radius: 100px; | |
| background: linear-gradient(90deg, var(--accent), var(--accent2)); | |
| transition: width 0.6s cubic-bezier(0.4,0,0.2,1); | |
| } | |
| .prob-val { | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| color: var(--text); | |
| text-align: right; | |
| } | |
| .prob-row.top .prob-name { color: var(--text); font-weight: bold; } | |
| .prob-row.top .prob-val { color: var(--success); } | |
| /* Try again button */ | |
| .btn-reset { | |
| margin-top: 24px; | |
| display: block; | |
| text-align: center; | |
| padding: 12px; | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| color: var(--muted); | |
| font-size: 13px; | |
| font-family: var(--mono); | |
| text-decoration: none; | |
| transition: all 0.2s; | |
| } | |
| .btn-reset:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| } | |
| /* Footer */ | |
| footer { | |
| margin-top: 48px; | |
| font-size: 11px; | |
| font-family: var(--mono); | |
| color: var(--muted); | |
| text-align: center; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| footer span { color: var(--accent); } | |
| /* Responsive */ | |
| @media (max-width: 480px) { | |
| .card { padding: 24px 18px; } | |
| .model-btn .icon { font-size: 22px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo-badge">NEURAL SCENE CLASSIFIER</div> | |
| <h1><br/>Image Classification</h1> | |
| <p class="subtitle">· PyTorch & TensorFlow ·</p> | |
| </header> | |
| {% if error %} | |
| <div class="card" style="max-width:640px;width:100%;position:relative;z-index:1;"> | |
| <div class="error-banner">⚠ {{ error }}</div> | |
| </div> | |
| {% endif %} | |
| {% if result %} | |
| <div class="card"> | |
| <div class="result-header"> | |
| <img class="result-thumb" | |
| src="data:image/jpeg;base64,{{ img_b64 }}" | |
| alt="uploaded image" /> | |
| <div class="result-meta"> | |
| <div class="result-label">Predicted Scene</div> | |
| <div class="result-class"> | |
| {{ class_icons.get(result.predicted_class, '🔍') }} | |
| {{ result.predicted_class | capitalize }} | |
| </div> | |
| <div class="result-conf"> | |
| ✓ {{ result.confidence }}% confidence | |
| </div> | |
| <div class="model-pill"> | |
| {% if model_used == 'pytorch' %}🔥 PyTorch{% else %}⚡ TensorFlow{% endif %} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Probability bars --> | |
| <span class="field-label">All class probabilities</span> | |
| <div class="prob-list"> | |
| {% for cls, prob in sorted_probs %} | |
| <div class="prob-row {% if cls == result.predicted_class %}top{% endif %}"> | |
| <span class="prob-name"> | |
| {{ class_icons.get(cls, '') }} {{ cls }} | |
| </span> | |
| <div class="prob-bar-bg"> | |
| <div class="prob-bar-fill" :style="{ width: prob + '%' }"></div> | |
| </div> | |
| <span class="prob-val">{{ prob }}%</span> | |
| </div> | |
| {% endfor %} | |
| </div> | |
| <a href="/" class="btn-reset"> Classify another image</a> | |
| </div> | |
| <!-- UPLOAD FORM --> | |
| {% else %} | |
| <div class="card"> | |
| <form method="POST" action="/predict" enctype="multipart/form-data" id="upload-form"> | |
| <!-- Model selector --> | |
| <span class="field-label">Select model backend</span> | |
| <div class="model-selector"> | |
| <input class="model-option" type="radio" name="model" | |
| id="opt-pytorch" value="pytorch" checked /> | |
| <label class="model-btn" for="opt-pytorch"> | |
| <span class="icon">🔥</span> | |
| <span class="name">PyTorch</span> | |
| <span class="tag">sara_model.pth</span> | |
| </label> | |
| <input class="model-option" type="radio" name="model" | |
| id="opt-tensorflow" value="tensorflow" /> | |
| <label class="model-btn" for="opt-tensorflow"> | |
| <span class="icon">⚡</span> | |
| <span class="name">TensorFlow</span> | |
| <span class="tag">sara_model.keras</span> | |
| </label> | |
| </div> | |
| <!-- File upload --> | |
| <span class="field-label">Upload image</span> | |
| <div class="upload-zone" id="drop-zone"> | |
| <input type="file" name="image" id="file-input" | |
| accept=".jpg,.jpeg,.png,.webp,.bmp" required /> | |
| <div class="upload-icon">📸</div> | |
| <div class="upload-text">Drop your image here</div> | |
| <div class="upload-hint">Click to browse · JPG PNG WEBP · max 5 MB</div> | |
| <div id="file-name"></div> | |
| </div> | |
| <!-- Preview --> | |
| <div id="preview-wrap"> | |
| <img id="preview-img" src="" alt="preview" /> | |
| </div> | |
| <!-- Submit --> | |
| <button type="submit" class="btn-predict" id="submit-btn"> | |
| Classify Image | |
| </button> | |
| </form> | |
| </div> | |
| {% endif %} | |
| <footer> | |
| Classification made with Pytorch and TensorFlow · | |
| </footer> | |
| <!-- JS: drag-drop, preview, loading state --> | |
| <script> | |
| const dropZone = document.getElementById('drop-zone'); | |
| const fileInput = document.getElementById('file-input'); | |
| const fileName = document.getElementById('file-name'); | |
| const preview = document.getElementById('preview-img'); | |
| const previewW = document.getElementById('preview-wrap'); | |
| const submitBtn = document.getElementById('submit-btn'); | |
| const form = document.getElementById('upload-form'); | |
| if (!fileInput) { /* result page — skip */ } | |
| else { | |
| // Show file name + preview on selection | |
| fileInput.addEventListener('change', () => { | |
| const file = fileInput.files[0]; | |
| if (!file) return; | |
| fileName.textContent = '✓ ' + file.name; | |
| const reader = new FileReader(); | |
| reader.onload = e => { | |
| preview.src = e.target.result; | |
| previewW.style.display = 'block'; | |
| }; | |
| reader.readAsDataURL(file); | |
| }); | |
| // Drag-and-drop visual feedback | |
| ['dragenter','dragover'].forEach(ev => | |
| dropZone.addEventListener(ev, e => { | |
| e.preventDefault(); | |
| dropZone.classList.add('drag-over'); | |
| }) | |
| ); | |
| ['dragleave','drop'].forEach(ev => | |
| dropZone.addEventListener(ev, e => { | |
| e.preventDefault(); | |
| dropZone.classList.remove('drag-over'); | |
| }) | |
| ); | |
| dropZone.addEventListener('drop', e => { | |
| const dt = e.dataTransfer; | |
| if (dt.files[0]) { | |
| // Assign dropped file to input | |
| const dtTransfer = new DataTransfer(); | |
| dtTransfer.items.add(dt.files[0]); | |
| fileInput.files = dtTransfer.files; | |
| fileInput.dispatchEvent(new Event('change')); | |
| } | |
| }); | |
| // Loading state on submit | |
| form.addEventListener('submit', () => { | |
| submitBtn.textContent = 'Classifying…'; | |
| submitBtn.classList.add('loading'); | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> | |