Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>CircuitGlyph β Neural Network-Based Electronic Circuit Classifier v1.0.0</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet"/> | |
| <style> | |
| :root { | |
| --bg: #000000; | |
| --panel: #0e1018; | |
| --border: #3b415e; | |
| --accent: #033c42; | |
| --accent2: #c49a1b; | |
| --accent3: #00ff9d; | |
| --text: #c8d0e8; | |
| --text-dim: #8c6a0e; | |
| --danger: #b55667; | |
| --glow: 0 0 24px rgba(0,229,255,0.18); | |
| } | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| background: var(--bg); color: var(--text); | |
| font-family: 'Syne', sans-serif; min-height: 100vh; overflow-x: hidden; | |
| } | |
| body::before { | |
| content: ''; position: fixed; inset: 0; z-index: 0; | |
| background-image: | |
| linear-gradient(rgba(51, 109, 116, 0.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(0,229,255,0.03) 1px, transparent 1px); | |
| background-size: 40px 40px; pointer-events: none; | |
| } | |
| .orb { position: fixed; border-radius: 50%; pointer-events: none; z-index: 0; filter: blur(90px); opacity: 0.18; } | |
| .orb-1 { width: 500px; height: 500px; background: var(--accent); top: -120px; left: -100px; } | |
| .orb-2 { width: 400px; height: 400px; background: var(--accent); bottom: -80px; right: -60px; } | |
| .app { position: relative; z-index: 1; max-width: 960px; margin: 0 auto; padding: 48px 24px 80px; } | |
| header { display: flex; align-items: center; gap: 16px; margin-bottom: 56px; } | |
| .logo-mark { | |
| width: 140px; height: 110px; border: 3px solid var(--accent2); border-radius: 12px; | |
| display: grid; place-items: center; box-shadow: var(--glow); flex-shrink: 0; | |
| } | |
| .logo-text h1 { font-size: 3em; font-weight: 800; letter-spacing: -0.02em; color: var(--accent2); } | |
| .logo-text p { | |
| font-family: 'Space Mono', monospace; font-size: 0.68rem; | |
| color: var(--text-dim); letter-spacing: 0.1em; text-transform: uppercase; | |
| } | |
| .main-grid { display: grid; grid-template-columns: 2fr 2fr; gap: 20px; margin-bottom: 20px; } | |
| @media (max-width: 680px) { .main-grid { grid-template-columns: 2fr; } } | |
| .panel { background: var(--panel); border: 1px solid var(--border); border-radius: 14px; overflow: hidden; } | |
| .panel-header { | |
| padding: 12px 18px; border-bottom: 1px solid var(--border); | |
| display: flex; align-items: center; gap: 8px; | |
| } | |
| .panel-header .dot { width: 8px; height: 8px; border-radius: 50%; } | |
| .dot-cyan { background: var(--accent); } | |
| .dot-purple { background: var(--accent2); } | |
| .dot-green { background: var(--accent3); } | |
| .panel-title { | |
| font-family: 'Space Mono', monospace; font-size: 0.7rem; | |
| letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-dim); | |
| } | |
| /* Upload zone */ | |
| .upload-zone { | |
| padding: 28px 28px; display: flex; flex-direction: column; | |
| align-items: center; justify-content: center; gap: 14px; | |
| cursor: pointer; min-height: 260px; transition: background 0.2s; position: relative; | |
| } | |
| .upload-zone.drag-over { background: rgba(0,229,255,0.06); } | |
| .upload-zone input[type=file] { position: absolute; inset: 0; opacity: 0; cursor: pointer; } | |
| .upload-icon { | |
| width: 56px; height: 56px; border: 1.5px dashed rgba(0, 229, 255, 0.691); | |
| border-radius: 12px; display: grid; place-items: center; color: var(--accent); transition: all 0.2s; | |
| } | |
| .upload-zone:hover .upload-icon, .upload-zone.drag-over .upload-icon | |
| { border-color: var(--accent); box-shadow: var(--glow); } | |
| .upload-label { text-align: center; } | |
| .upload-label strong { display: block; font-size: 0.95rem; color: #fff; margin-bottom: 4px; } | |
| .upload-label span { font-family: 'Space Mono', monospace; font-size: 0.68rem; color: var(--accent3); } | |
| #preview-img { max-width: 100%; max-height: 210px; border-radius: 8px; object-fit: contain; display: none; } | |
| #file-name { | |
| font-family: 'Space Mono', monospace; font-size: 0.7rem; color: var(--accent); display: none; | |
| padding: 4px 10px; background: rgba(0,229,255,0.07); border-radius: 4px; | |
| word-break: break-all; text-align: center; | |
| } | |
| /* Analyze button */ | |
| .analyze-btn { | |
| width: 100%; padding: 14px 0; | |
| background: linear-gradient(55deg, var(--accent), var(--accent)); | |
| border: none; border-radius: 10px; color: #fff; | |
| font-family: 'Syne', sans-serif; font-size: 1rem; font-weight: 700; | |
| letter-spacing: 0.03em; cursor: pointer; transition: opacity 0.2s, transform 0.1s; | |
| display: flex; align-items: center; justify-content: center; gap: 10px; | |
| } | |
| .analyze-btn:hover:not(:disabled) { opacity: 0.9; transform: translateY(-1px); } | |
| .analyze-btn:disabled { opacity: 0.4; cursor: not-allowed; } | |
| /* Processed image panel */ | |
| .proc-zone { | |
| padding: 20px; min-height: 260px; display: flex; align-items: center; justify-content: center; | |
| } | |
| #proc-img { max-width: 100%; max-height: 310px; border-radius: 8px; object-fit: contain; display: none; } | |
| .proc-placeholder { | |
| font-family: 'Space Mono', monospace; font-size: 0.75rem; | |
| color: var(--text-dim); text-align: center; line-height: 1.8; | |
| } | |
| /* Results */ | |
| .result-body { padding: 24px 22px; min-height: 160px; } | |
| #typewriter-output { | |
| font-family: 'Space Mono', monospace; font-size: 0.82rem; | |
| line-height: 1.85; color: var(--text); white-space: pre-wrap; min-height: 40px; | |
| } | |
| .cursor { | |
| display: inline-block; width: 2px; height: 1em; background: var(--accent); | |
| vertical-align: middle; animation: blink 0.8s step-end infinite; margin-left: 2px; | |
| } | |
| @keyframes blink { 50% { opacity: 0; } } | |
| /* Confidence bars */ | |
| .conf-bars { margin-top: 20px; display: none; } | |
| .conf-bars.show { display: block; } | |
| .conf-title { | |
| font-family: 'Space Mono', monospace; font-size: 0.65rem; | |
| letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-dim); margin-bottom: 12px; | |
| } | |
| .bar-row { display: grid; grid-template-columns: 180px 1fr 60px; align-items: center; gap: 12px; margin-bottom: 8px; } | |
| .bar-label { font-family: 'Space Mono', monospace; font-size: 0.68rem; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | |
| .bar-track { background: rgba(255,255,255,0.06); border-radius: 4px; height: 6px; overflow: hidden; } | |
| .bar-fill { | |
| height: 100%; border-radius: 4px; | |
| background: linear-gradient(90deg, var(--accent2), var(--accent)); | |
| width: 0; transition: width 0.8s cubic-bezier(0.16,1,0.3,1); | |
| } | |
| .bar-pct { font-family: 'Space Mono', monospace; font-size: 0.68rem; color: var(--accent3); text-align: right; } | |
| /* Status */ | |
| .status-row { | |
| display: flex; align-items: center; gap: 10px; | |
| padding: 12px 18px; border-top: 1px solid var(--border); | |
| } | |
| .spinner { | |
| width: 14px; height: 14px; border: 2px solid rgba(0,229,255,0.15); | |
| border-top-color: var(--accent); border-radius: 50%; | |
| animation: spin 0.7s linear infinite; display: none; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| .spinner.active { display: block; } | |
| .status-text { font-family: 'Space Mono', monospace; font-size: 0.68rem; color: var(--text-dim); } | |
| .status-text.error { color: var(--danger); } | |
| .status-text.ok { color: var(--accent3); } | |
| .models-strip { display: flex; gap: 8px; flex-wrap: wrap; padding: 0 18px 14px; } | |
| .model-tag { | |
| font-family: 'Space Mono', monospace; font-size: 0.6rem; | |
| letter-spacing: 0.1em; text-transform: uppercase; padding: 3px 9px; border-radius: 4px; | |
| } | |
| .model-tag.eff { color: var(--accent3); background: rgba(123,92,255,0.1); border: 1px solid rgba(123,92,255,0.2); } | |
| .model-tag.vit { color: var(--accent3); background: rgba(0,229,255,0.07); border: 1px solid rgba(0,229,255,0.18); } | |
| footer { | |
| margin-top: 52px; text-align: center; | |
| font-family: 'Space Mono', monospace; font-size: 0.62rem; | |
| color: var(--text-dim); letter-spacing: 0.08em; line-height: 2; | |
| } | |
| footer span { color: var(--accent2); } | |
| @media (max-width: 480px) { | |
| .app { padding: 24px 14px 60px; } | |
| header { flex-direction: column; align-items: center; text-align: center; gap: 12px; margin-bottom: 32px; } | |
| .logo-mark { width: 90px; height: 72px; } | |
| .logo-mark img { width: 76px; height: 58px; } | |
| .logo-text h1 { font-size: 1.8em; } | |
| .logo-text p { font-size: 0.58rem; } | |
| .bar-row { grid-template-columns: 120px 1fr 48px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="orb orb-1"></div> | |
| <div class="orb orb-2"></div> | |
| <div class="app"> | |
| <header> | |
| <div class="logo-mark"> | |
| <img src="https://ssayed122-circuitglyph.hf.space/logo.png" alt="CircuitGlyph logo" width="120" height="90" /> | |
| </div> | |
| <div class="logo-text"> | |
| <h1>CircuitGlyph</h1> | |
| <p>Neural Network-Based Electronic Circuit Classification System Β· v1.0.0</p> | |
| </div> | |
| </header> | |
| <div class="main-grid"> | |
| <!-- Left: Upload --> | |
| <div> | |
| <div class="panel" style="margin-bottom:16px"> | |
| <div class="panel-header"> | |
| <div class="dot dot-cyan"></div> | |
| <span class="panel-title">Input Image</span> | |
| </div> | |
| <div class="upload-zone" id="drop-zone"> | |
| <input type="file" id="file-input" accept="image/*"/> | |
| <div class="upload-icon"> | |
| <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"> | |
| <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/> | |
| <polyline points="17 8 12 3 7 8"/> | |
| <line x1="12" y1="3" x2="12" y2="15"/> | |
| </svg> | |
| </div> | |
| <img id="preview-img" alt="Preview"/> | |
| <div class="upload-label"> | |
| <strong>Drop your circuit image here</strong> | |
| <span>or click to browse β PNG, JPG, BMP</span> | |
| </div> | |
| <div id="file-name"></div> | |
| </div> | |
| </div> | |
| <button class="analyze-btn" id="analyze-btn" disabled> | |
| <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"> | |
| <!-- input layer --> | |
| <circle cx="2" cy="3" r="2"/> | |
| <circle cx="2" cy="12" r="2"/> | |
| <circle cx="2" cy="21" r="2"/> | |
| <!-- hidden layer --> | |
| <circle cx="12" cy="3" r="2"/> | |
| <circle cx="12" cy="12" r="2"/> | |
| <circle cx="12" cy="21" r="2"/> | |
| <!-- output layer --> | |
| <circle cx="22" cy="10" r="2"/> | |
| <circle cx="22" cy="14" r="2"/> | |
| <!-- connections --> | |
| <line x1="3.5" y1="3" x2="10.5" y2="3"/> | |
| <line x1="3.5" y1="12" x2="10.5" y2="12"/> | |
| <line x1="3.5" y1="21" x2="10.5" y2="21"/> | |
| <line x1="13.5" y1="3" x2="19" y2="10"/> | |
| <line x1="13.5" y1="12" x2="19" y2="10"/> | |
| <line x1="13.5" y1="12" x2="19" y2="14"/> | |
| <line x1="13.5" y1="21" x2="19" y2="14"/> | |
| </svg> | |
| Predict Functionality | |
| </button> | |
| </div> | |
| <!-- Right: Processed image --> | |
| <div> | |
| <div class="panel" style="height:100%"> | |
| <div class="panel-header"> | |
| <div class="dot dot-purple"></div> | |
| <span class="panel-title">Preprocessed Circuit Image</span> | |
| </div> | |
| <div class="proc-zone"> | |
| <img id="proc-img" alt="Processed"/> | |
| <div class="proc-placeholder" id="proc-placeholder"> | |
| Cleaned, binarized, & square-padded<br>circuit image will appear here | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Results panel --> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <div class="dot dot-green"></div> | |
| <span class="panel-title">Prediction Report</span> | |
| </div> | |
| <div class="models-strip"> | |
| <span class="model-tag vit">ViT</span> | |
| <span class="model-tag eff">EfficientNet</span> | |
| </div> | |
| <div class="result-body"> | |
| <div id="typewriter-output">Awaiting image inputβ¦</div> | |
| <div class="conf-bars" id="conf-bars"> | |
| <div class="conf-title">Top-5 Predictions</div> | |
| <div id="bar-container"></div> | |
| </div> | |
| </div> | |
| <div class="status-row"> | |
| <div class="spinner" id="spinner"></div> | |
| <span class="status-text" id="status-text">Ready</span> | |
| </div> | |
| </div> | |
| </div> | |
| <footer>CircuitGlyph Β· <span>v1.0.0</span> Β· 51 circuit classes<br style="margin-bottom:6px"/>Developed by <span>Shehrin Sayed, Ph.D.</span> Β· CopyrightΒ© 2026. All rights reserved.</footer> | |
| <script> | |
| let selectedFile = null; | |
| const fileInput = document.getElementById('file-input'); | |
| const previewImg = document.getElementById('preview-img'); | |
| const fileNameEl = document.getElementById('file-name'); | |
| const uploadLabel = document.querySelector('.upload-label'); | |
| const uploadIcon = document.querySelector('.upload-icon'); | |
| const analyzeBtn = document.getElementById('analyze-btn'); | |
| const procImg = document.getElementById('proc-img'); | |
| const procPH = document.getElementById('proc-placeholder'); | |
| const twOutput = document.getElementById('typewriter-output'); | |
| const confBars = document.getElementById('conf-bars'); | |
| const barContainer = document.getElementById('bar-container'); | |
| const spinner = document.getElementById('spinner'); | |
| const statusText = document.getElementById('status-text'); | |
| const dropZone = document.getElementById('drop-zone'); | |
| // βββ File selection ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| fileInput.addEventListener('change', e => { | |
| const f = e.target.files[0]; | |
| if (f) handleFile(f); | |
| }); | |
| dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); }); | |
| dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over')); | |
| dropZone.addEventListener('drop', e => { | |
| e.preventDefault(); dropZone.classList.remove('drag-over'); | |
| const f = e.dataTransfer.files[0]; | |
| if (f && f.type.startsWith('image/')) handleFile(f); | |
| }); | |
| function handleFile(f) { | |
| selectedFile = f; | |
| previewImg.src = URL.createObjectURL(f); | |
| previewImg.style.display = 'block'; | |
| uploadLabel.style.display = 'none'; | |
| uploadIcon.style.display = 'none'; | |
| fileNameEl.textContent = f.name; | |
| fileNameEl.style.display = 'block'; | |
| analyzeBtn.disabled = false; | |
| setStatus('Selected image loaded β click Predict Functionality', ''); | |
| twOutput.innerHTML = 'Selected image loaded. Click <strong style="color:var(--accent)">Predict Functionality</strong> to classifyβ¦'; | |
| confBars.classList.remove('show'); | |
| } | |
| // βββ Analyze ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| analyzeBtn.addEventListener('click', () => { | |
| if (!selectedFile) return; | |
| runAnalysis(selectedFile); | |
| }); | |
| async function runAnalysis(file) { | |
| analyzeBtn.disabled = true; | |
| spinner.classList.add('active'); | |
| setStatus('Sending to Python backendβ¦', ''); | |
| twOutput.innerHTML = ''; | |
| confBars.classList.remove('show'); | |
| procImg.style.display = 'none'; | |
| procPH.style.display = 'block'; | |
| try { | |
| const formData = new FormData(); | |
| formData.append('image', file); | |
| setStatus('Preprocessing + running inferenceβ¦', ''); | |
| const response = await fetch('/analyze', { method: 'POST', body: formData }); | |
| const data = await response.json(); | |
| if (!response.ok || data.error) { | |
| throw new Error(data.error || `Server error ${response.status}`); | |
| } | |
| // Show processed image returned from backend | |
| if (data.processed_img) { | |
| procImg.src = 'data:image/png;base64,' + data.processed_img; | |
| procImg.style.display = 'block'; | |
| procPH.style.display = 'none'; | |
| } | |
| setStatus('Rendering resultsβ¦', ''); | |
| spinner.classList.remove('active'); | |
| // Typewrite the pred_string exactly as Python produces it | |
| typewrite(data.pred_string, 22, () => { | |
| renderBars(data.top5); | |
| confBars.classList.add('show'); | |
| setStatus('Analysis complete', 'ok'); | |
| }); | |
| } catch(err) { | |
| spinner.classList.remove('active'); | |
| setStatus('Error: ' + err.message, 'error'); | |
| twOutput.innerHTML = `<span style="color:var(--danger)">β ${err.message}</span>`; | |
| analyzeBtn.disabled = false; | |
| } | |
| } | |
| // βββ Typewriter ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function typewrite(text, speed, onDone) { | |
| twOutput.innerHTML = ''; | |
| const cursor = document.createElement('span'); | |
| cursor.className = 'cursor'; | |
| twOutput.appendChild(cursor); | |
| let i = 0; | |
| const id = setInterval(() => { | |
| if (i >= text.length) { | |
| clearInterval(id); | |
| cursor.remove(); | |
| if (onDone) onDone(); | |
| return; | |
| } | |
| cursor.insertAdjacentText('beforebegin', text[i++]); | |
| }, speed); | |
| } | |
| // βββ Confidence bars βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function renderBars(top5) { | |
| barContainer.innerHTML = ''; | |
| top5.forEach(item => { | |
| const pct = (item.conf * 100).toFixed(1); | |
| const row = document.createElement('div'); | |
| row.className = 'bar-row'; | |
| row.innerHTML = ` | |
| <span class="bar-label">${item.name}</span> | |
| <div class="bar-track"><div class="bar-fill" data-pct="${item.conf*100}"></div></div> | |
| <span class="bar-pct">${pct}%</span>`; | |
| barContainer.appendChild(row); | |
| }); | |
| requestAnimationFrame(() => { | |
| document.querySelectorAll('.bar-fill').forEach(el => { | |
| el.style.width = el.dataset.pct + '%'; | |
| }); | |
| }); | |
| } | |
| function setStatus(msg, cls) { | |
| statusText.textContent = msg; | |
| statusText.className = 'status-text' + (cls ? ' ' + cls : ''); | |
| } | |
| </script> | |
| </body> | |
| </html> | |