Spaces:
Runtime error
Runtime error
Download static/app.js from urmilasaini/EcoSort_AI: direct link, hf CLI and curl.
- Browser
- Download file 14.2 kB
-
https://huggingface.co/spaces/urmilasaini/EcoSort_AI/resolve/main/static/app.js
- Command line
-
hf download hf://spaces/urmilasaini/EcoSort_AI/static/app.js
-
curl -L -o app.js https://huggingface.co/spaces/urmilasaini/EcoSort_AI/resolve/main/static/app.js
14.2 kB
| const video = document.getElementById("video"); | |
| const canvas = document.getElementById("canvas"); | |
| const captured = document.getElementById("captured"); | |
| const cameraError = document.getElementById("cameraError"); | |
| const fileInput = document.getElementById("fileInput"); | |
| const pickBtn = document.getElementById("pickBtn"); | |
| const scanBtn = document.getElementById("scanBtn"); | |
| const flipBtn = document.getElementById("flipBtn"); | |
| const flashBtn = document.getElementById("flashBtn"); | |
| const scanStatusText = document.getElementById("scanStatusText"); | |
| const camDetectBadge = document.getElementById("camDetectBadge"); | |
| const scannerModal = document.getElementById("scannerModal"); | |
| const openScannerBtn = document.getElementById("openScannerBtn"); | |
| const scanAnotherBtn = document.getElementById("scanAnotherBtn"); | |
| const closeScannerBtn = document.getElementById("closeScannerBtn"); | |
| const menuScanLink = document.getElementById("menuScanLink"); | |
| const menuBtn = document.getElementById("menuBtn"); | |
| const navMenu = document.getElementById("navMenu"); | |
| const resultOverlay = document.getElementById("resultOverlay"); | |
| const resultCard = document.getElementById("resultCard"); | |
| const resultIcon = document.getElementById("resultIcon"); | |
| const resultBinIcon = document.getElementById("resultBinIcon"); | |
| const resultBinName = document.getElementById("resultBinName"); | |
| const resultBinDesc = document.getElementById("resultBinDesc"); | |
| const resultLabel = document.getElementById("resultLabel"); | |
| const resultConfidence = document.getElementById("resultConfidence"); | |
| const resultConfidenceFill = document.getElementById("resultConfidenceFill"); | |
| const resultTopWrap = document.getElementById("resultTopWrap"); | |
| const resultTopList = document.getElementById("resultTopList"); | |
| const closeResultBtn = document.getElementById("closeResultBtn"); | |
| const cameraWrap = document.getElementById("cameraWrap"); | |
| const toast = document.getElementById("toast"); | |
| const analysisToggle = document.getElementById("analysisToggle"); | |
| const analysisPanel = document.getElementById("analysisPanel"); | |
| const analysisDist = document.getElementById("analysisDist"); | |
| const analysisModel = document.getElementById("analysisModel"); | |
| const mEntropy = document.getElementById("mEntropy"); | |
| const mEntropyNote = document.getElementById("mEntropyNote"); | |
| const mMargin = document.getElementById("mMargin"); | |
| const mLatency = document.getElementById("mLatency"); | |
| const mDevice = document.getElementById("mDevice"); | |
| const mLogit = document.getElementById("mLogit"); | |
| const explainBtn = document.getElementById("explainBtn"); | |
| const explainResult = document.getElementById("explainResult"); | |
| const explainImg = document.getElementById("explainImg"); | |
| const explainMeta = document.getElementById("explainMeta"); | |
| const CHECK_ICON = | |
| '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>'; | |
| const BIN_ICON = { | |
| green: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2C7 2 3 6 3 11c0 5 4 9 8 11 4-2 8-6 8-11 0-5-4-9-8-9z"/><line x1="12" y1="6" x2="12" y2="20"/></svg>', | |
| blue: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>', | |
| red: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>', | |
| }; | |
| let currentBlob = null; | |
| let lastScanBlob = null; | |
| let stream = null; | |
| let facingMode = "environment"; | |
| let torchOn = false; | |
| function showToast(message) { | |
| toast.textContent = message; | |
| toast.hidden = false; | |
| setTimeout(() => (toast.hidden = true), 3000); | |
| } | |
| function setStatus(text) { | |
| scanStatusText.textContent = text; | |
| } | |
| async function startCamera() { | |
| stopCamera(); | |
| try { | |
| stream = await navigator.mediaDevices.getUserMedia({ | |
| video: { facingMode }, | |
| audio: false, | |
| }); | |
| video.srcObject = stream; | |
| cameraError.hidden = true; | |
| torchOn = false; | |
| flashBtn.classList.remove("is-on"); | |
| const capabilities = stream.getVideoTracks()[0]?.getCapabilities?.() || {}; | |
| flashBtn.hidden = !capabilities.torch; | |
| } catch (err) { | |
| cameraError.hidden = false; | |
| flashBtn.hidden = true; | |
| console.warn("Camera unavailable:", err); | |
| } | |
| } | |
| function stopCamera() { | |
| if (stream) { | |
| stream.getTracks().forEach((t) => t.stop()); | |
| stream = null; | |
| } | |
| } | |
| function showCapturedFrame(blob) { | |
| currentBlob = blob; | |
| captured.src = URL.createObjectURL(blob); | |
| captured.hidden = false; | |
| video.hidden = true; | |
| } | |
| function resetToLive() { | |
| currentBlob = null; | |
| captured.hidden = true; | |
| video.hidden = false; | |
| fileInput.value = ""; | |
| setStatus("Point at a waste item"); | |
| } | |
| async function captureFrame() { | |
| canvas.width = video.videoWidth; | |
| canvas.height = video.videoHeight; | |
| canvas.getContext("2d").drawImage(video, 0, 0); | |
| return new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", 0.9)); | |
| } | |
| scanBtn.addEventListener("click", async () => { | |
| if (!currentBlob) { | |
| if (!video.srcObject) { | |
| showToast("Camera unavailable. Upload a photo instead."); | |
| return; | |
| } | |
| currentBlob = await captureFrame(); | |
| showCapturedFrame(currentBlob); | |
| } | |
| await runScan(currentBlob); | |
| }); | |
| pickBtn.addEventListener("click", () => fileInput.click()); | |
| fileInput.addEventListener("change", async () => { | |
| const file = fileInput.files[0]; | |
| if (!file) return; | |
| showCapturedFrame(file); | |
| await runScan(file); | |
| }); | |
| flipBtn.addEventListener("click", async () => { | |
| facingMode = facingMode === "environment" ? "user" : "environment"; | |
| resetToLive(); | |
| await startCamera(); | |
| }); | |
| flashBtn.addEventListener("click", async () => { | |
| const track = stream?.getVideoTracks()[0]; | |
| if (!track) return; | |
| torchOn = !torchOn; | |
| try { | |
| await track.applyConstraints({ advanced: [{ torch: torchOn }] }); | |
| flashBtn.classList.toggle("is-on", torchOn); | |
| } catch { | |
| torchOn = false; | |
| showToast("Flash isn't available on this camera."); | |
| } | |
| }); | |
| closeResultBtn.addEventListener("click", () => { | |
| resultOverlay.hidden = true; | |
| resetToLive(); | |
| }); | |
| function openScanner() { | |
| navMenu.hidden = true; | |
| menuBtn.setAttribute("aria-expanded", "false"); | |
| scannerModal.hidden = false; | |
| resetToLive(); | |
| startCamera(); | |
| } | |
| function closeScanner() { | |
| scannerModal.hidden = true; | |
| stopCamera(); | |
| resetToLive(); | |
| } | |
| openScannerBtn.addEventListener("click", openScanner); | |
| scanAnotherBtn.addEventListener("click", openScanner); | |
| closeScannerBtn.addEventListener("click", closeScanner); | |
| menuScanLink.addEventListener("click", (e) => { | |
| e.preventDefault(); | |
| openScanner(); | |
| }); | |
| menuBtn.addEventListener("click", () => { | |
| const isOpen = !navMenu.hidden; | |
| navMenu.hidden = isOpen; | |
| menuBtn.setAttribute("aria-expanded", String(!isOpen)); | |
| }); | |
| // The model answers in ~250ms, which is too fast to read. Hold the sweep long | |
| // enough for the scan to be legible, then pop the check before the result. | |
| const SWEEP_MS = 1800; | |
| const DETECT_MS = 700; | |
| const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); | |
| async function classify(blob) { | |
| const formData = new FormData(); | |
| formData.append("file", blob, "capture.jpg"); | |
| const res = await fetch("/api/classify", { method: "POST", body: formData }); | |
| if (!res.ok) { | |
| const err = await res.json().catch(() => ({})); | |
| throw new Error(err.detail || `Request failed (${res.status})`); | |
| } | |
| return res.json(); | |
| } | |
| async function runScan(blob) { | |
| scanBtn.disabled = true; | |
| lastScanBlob = blob; | |
| cameraWrap.classList.add("is-scanning"); | |
| setStatus("Scanning..."); | |
| // Request and sweep run together, so the wait is whichever takes longer. | |
| const [outcome] = await Promise.all([ | |
| classify(blob).then( | |
| (data) => ({ data }), | |
| (error) => ({ error }) | |
| ), | |
| sleep(SWEEP_MS), | |
| ]); | |
| cameraWrap.classList.remove("is-scanning"); | |
| scanBtn.disabled = false; | |
| if (outcome.error) { | |
| showToast(outcome.error.message || "Scan failed. Try again."); | |
| resetToLive(); | |
| return; | |
| } | |
| setStatus("Detecting..."); | |
| camDetectBadge.classList.add("is-visible"); | |
| await sleep(DETECT_MS); | |
| setStatus("Detected"); | |
| camDetectBadge.classList.remove("is-visible"); | |
| showResult(outcome.data); | |
| } | |
| function formatParams(n) { | |
| return n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n.toLocaleString(); | |
| } | |
| function certaintyNote(normalized) { | |
| if (normalized < 0.15) return "very certain"; | |
| if (normalized < 0.4) return "confident"; | |
| if (normalized < 0.7) return "ambiguous"; | |
| return "highly uncertain"; | |
| } | |
| function renderAnalysis(analysis) { | |
| analysisToggle.hidden = !analysis; | |
| if (!analysis) return; | |
| mEntropy.textContent = `${analysis.entropy_bits.toFixed(3)} bits`; | |
| mEntropyNote.textContent = certaintyNote(analysis.entropy_normalized); | |
| mMargin.textContent = `${(analysis.margin * 100).toFixed(1)}%`; | |
| mLatency.textContent = `${analysis.latency_ms} ms`; | |
| mDevice.textContent = analysis.device; | |
| mLogit.textContent = analysis.logit_top.toFixed(2); | |
| analysisDist.replaceChildren( | |
| ...analysis.distribution.map((item, i) => { | |
| const li = document.createElement("li"); | |
| if (i === 0) li.className = "is-top"; | |
| const name = document.createElement("span"); | |
| name.className = "dist-name"; | |
| name.textContent = item.label; | |
| const track = document.createElement("span"); | |
| track.className = "dist-track"; | |
| const fill = document.createElement("span"); | |
| fill.className = "dist-fill"; | |
| fill.style.width = `${Math.max(item.confidence * 100, 0.6)}%`; | |
| track.appendChild(fill); | |
| const pct = document.createElement("span"); | |
| pct.className = "dist-pct"; | |
| const p = item.confidence * 100; | |
| pct.textContent = p >= 0.1 ? `${p.toFixed(1)}%` : `${p.toExponential(1)}%`; | |
| li.append(name, track, pct); | |
| return li; | |
| }) | |
| ); | |
| const m = analysis.model; | |
| const rows = [ | |
| ["Model", m.id], | |
| ["Architecture", m.architecture], | |
| ["Parameters", formatParams(m.parameters)], | |
| ["Input", `${m.input_size[0]}脳${m.input_size[1]} 路 patch ${m.patch_size}`], | |
| ["Transformer", `${m.num_layers} layers 路 ${m.num_heads} heads 路 d=${m.hidden_size}`], | |
| ["Classes", m.num_classes], | |
| ["Source image", `${m.original_image_size[0]}脳${m.original_image_size[1]}`], | |
| ["Normalize", `mean ${m.normalize_mean.join(", ")} 路 std ${m.normalize_std.join(", ")}`], | |
| ]; | |
| analysisModel.replaceChildren( | |
| ...rows.flatMap(([k, v]) => { | |
| const dt = document.createElement("dt"); | |
| dt.textContent = k; | |
| const dd = document.createElement("dd"); | |
| dd.textContent = v; | |
| return [dt, dd]; | |
| }) | |
| ); | |
| } | |
| function collapseAnalysis() { | |
| analysisPanel.hidden = true; | |
| analysisToggle.setAttribute("aria-expanded", "false"); | |
| analysisToggle.classList.remove("is-open"); | |
| explainResult.hidden = true; | |
| explainImg.removeAttribute("src"); | |
| explainBtn.hidden = false; | |
| explainBtn.disabled = false; | |
| explainBtn.textContent = "Generate attention map"; | |
| } | |
| explainBtn.addEventListener("click", async () => { | |
| if (!lastScanBlob) { | |
| showToast("Scan an image first."); | |
| return; | |
| } | |
| explainBtn.disabled = true; | |
| explainBtn.textContent = "Computing attention..."; | |
| try { | |
| const formData = new FormData(); | |
| formData.append("file", lastScanBlob, "capture.jpg"); | |
| const res = await fetch("/api/explain", { method: "POST", body: formData }); | |
| if (!res.ok) { | |
| const err = await res.json().catch(() => ({})); | |
| throw new Error(err.detail || `Request failed (${res.status})`); | |
| } | |
| const data = await res.json(); | |
| explainImg.src = data.heatmap; | |
| explainMeta.textContent = `${data.grid[0]}脳${data.grid[1]} patch grid 路 ${data.patches} patches 路 ${data.layers} layers rolled out 路 ${data.latency_ms} ms`; | |
| explainResult.hidden = false; | |
| explainBtn.hidden = true; | |
| } catch (err) { | |
| showToast(err.message || "Could not generate the attention map."); | |
| explainBtn.disabled = false; | |
| explainBtn.textContent = "Generate attention map"; | |
| } | |
| }); | |
| analysisToggle.addEventListener("click", () => { | |
| const open = analysisPanel.hidden; | |
| analysisPanel.hidden = !open; | |
| analysisToggle.setAttribute("aria-expanded", String(open)); | |
| analysisToggle.classList.toggle("is-open", open); | |
| }); | |
| function showResult(data) { | |
| resultCard.classList.remove("green", "blue", "red"); | |
| resultCard.classList.add(data.bin_color); | |
| collapseAnalysis(); | |
| renderAnalysis(data.analysis); | |
| resultIcon.innerHTML = CHECK_ICON; | |
| resultBinIcon.innerHTML = BIN_ICON[data.bin_color] || ""; | |
| resultLabel.textContent = data.label; | |
| resultBinName.textContent = data.bin_name; | |
| resultBinDesc.textContent = data.bin_desc; | |
| const pct = data.confidence * 100; | |
| resultConfidence.textContent = `${pct.toFixed(1)}%`; | |
| const top = Array.isArray(data.top) ? data.top : []; | |
| resultTopWrap.hidden = top.length === 0; | |
| resultTopList.replaceChildren( | |
| ...top.map((item) => { | |
| const li = document.createElement("li"); | |
| const name = document.createElement("span"); | |
| name.className = "pred-name"; | |
| name.textContent = item.label; | |
| const track = document.createElement("span"); | |
| track.className = "pred-bar-track"; | |
| const fill = document.createElement("span"); | |
| fill.className = "pred-bar-fill"; | |
| fill.style.width = `${(item.confidence * 100).toFixed(1)}%`; | |
| track.appendChild(fill); | |
| const pctEl = document.createElement("span"); | |
| pctEl.className = "pred-pct"; | |
| pctEl.textContent = `${Math.round(item.confidence * 100)}%`; | |
| li.append(name, track, pctEl); | |
| return li; | |
| }) | |
| ); | |
| resultOverlay.hidden = false; | |
| // Let the overlay paint at 0 before growing the bar, so it animates. | |
| resultConfidenceFill.style.width = "0%"; | |
| requestAnimationFrame(() => { | |
| resultConfidenceFill.style.width = `${pct.toFixed(1)}%`; | |
| }); | |
| } | |