Download app.js from vimdhayak/Garbage-Classification: direct link, hf CLI and curl.
- Browser
- Download file 17.8 kB
-
https://huggingface.co/spaces/vimdhayak/Garbage-Classification/resolve/main/app.js
- Command line
-
hf download hf://spaces/vimdhayak/Garbage-Classification/app.js
-
curl -L -o app.js https://huggingface.co/spaces/vimdhayak/Garbage-Classification/resolve/main/app.js
17.8 kB
| ; | |
| const APP = Object.freeze({ | |
| modelUrl: './ms_se_efficientnet_b0.onnx?v=ac619ed4', | |
| ortVersion: '1.27.0', | |
| imageSize: 224, | |
| mean: [0.485, 0.456, 0.406], | |
| std: [0.229, 0.224, 0.225], | |
| labels: [ | |
| 'battery', 'biological', 'cardboard', 'clothes', 'glass', | |
| 'metal', 'paper', 'plastic', 'shoes', 'trash', | |
| ], | |
| metadata: { | |
| input: { width: 224, height: 224, channels: 3 }, | |
| complexity: { parameters: 4234159 }, | |
| }, | |
| }); | |
| const state = { | |
| session: null, | |
| labels: APP.labels, | |
| metadata: APP.metadata, | |
| bitmap: null, | |
| objectUrl: null, | |
| file: null, | |
| modelReady: false, | |
| predicting: false, | |
| }; | |
| const els = { | |
| runtimePill: document.querySelector('#runtimePill'), | |
| runtimeText: document.querySelector('#runtimeText'), | |
| statusDot: document.querySelector('#statusDot'), | |
| statusCard: document.querySelector('.status-card'), | |
| progressBar: document.querySelector('#progressBar'), | |
| loadPercent: document.querySelector('#loadPercent'), | |
| statusDetail: document.querySelector('#statusDetail'), | |
| fileInput: document.querySelector('#fileInput'), | |
| dropZone: document.querySelector('#dropZone'), | |
| clearButton: document.querySelector('#clearButton'), | |
| previewWrap: document.querySelector('#previewWrap'), | |
| imagePreview: document.querySelector('#imagePreview'), | |
| fileName: document.querySelector('#fileName'), | |
| fileDetails: document.querySelector('#fileDetails'), | |
| predictButton: document.querySelector('#predictButton'), | |
| predictButtonText: document.querySelector('#predictButtonText'), | |
| buttonSpinner: document.querySelector('#buttonSpinner'), | |
| inputMessage: document.querySelector('#inputMessage'), | |
| emptyResult: document.querySelector('#emptyResult'), | |
| results: document.querySelector('#results'), | |
| predictedClass: document.querySelector('#predictedClass'), | |
| confidenceRing: document.querySelector('#confidenceRing'), | |
| confidenceValue: document.querySelector('#confidenceValue'), | |
| topThree: document.querySelector('#topThree'), | |
| probabilityList: document.querySelector('#probabilityList'), | |
| preprocessTime: document.querySelector('#preprocessTime'), | |
| inferenceTime: document.querySelector('#inferenceTime'), | |
| totalTime: document.querySelector('#totalTime'), | |
| providerBadge: document.querySelector('#providerBadge'), | |
| classChips: document.querySelector('#classChips'), | |
| modelFacts: document.querySelector('#modelFacts'), | |
| canvas: document.querySelector('#preprocessCanvas'), | |
| }; | |
| function setProgress(percent, detail) { | |
| const safePercent = Math.max(0, Math.min(100, Math.round(percent))); | |
| els.progressBar.style.width = `${safePercent}%`; | |
| els.loadPercent.textContent = `${safePercent}%`; | |
| if (detail) els.statusDetail.textContent = detail; | |
| } | |
| function setRuntimeState(kind, text) { | |
| els.runtimePill.classList.remove('ready', 'error'); | |
| if (kind) els.runtimePill.classList.add(kind); | |
| els.runtimeText.textContent = text; | |
| } | |
| function setError(message, error) { | |
| console.error(message, error || ''); | |
| state.modelReady = false; | |
| els.statusCard.classList.add('error'); | |
| setRuntimeState('error', 'Runtime unavailable'); | |
| setProgress(100, message); | |
| els.inputMessage.textContent = 'The model could not be initialized. Refresh the page or try another modern browser.'; | |
| els.inputMessage.classList.add('error'); | |
| updatePredictButton(); | |
| } | |
| function titleCase(value) { | |
| return String(value) | |
| .replace(/[_-]+/g, ' ') | |
| .replace(/\b\w/g, character => character.toUpperCase()); | |
| } | |
| function formatBytes(bytes) { | |
| if (!Number.isFinite(bytes) || bytes <= 0) return ''; | |
| const units = ['B', 'KB', 'MB', 'GB']; | |
| const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); | |
| return `${(bytes / (1024 ** index)).toFixed(index >= 2 ? 1 : 0)} ${units[index]}`; | |
| } | |
| function formatMilliseconds(value) { | |
| if (!Number.isFinite(value)) return '—'; | |
| if (value < 10) return `${value.toFixed(2)} ms`; | |
| if (value < 100) return `${value.toFixed(1)} ms`; | |
| return `${Math.round(value)} ms`; | |
| } | |
| function updatePredictButton() { | |
| const enabled = state.modelReady && Boolean(state.bitmap) && !state.predicting; | |
| els.predictButton.disabled = !enabled; | |
| els.clearButton.disabled = !state.bitmap && !state.file; | |
| } | |
| async function fetchBinaryWithProgress(url, onProgress) { | |
| const response = await fetch(url, { cache: 'force-cache' }); | |
| if (!response.ok) throw new Error(`Model download failed: HTTP ${response.status}`); | |
| const total = Number(response.headers.get('content-length')) || 0; | |
| if (!response.body || !total) { | |
| const buffer = await response.arrayBuffer(); | |
| onProgress(1, buffer.byteLength); | |
| return new Uint8Array(buffer); | |
| } | |
| const reader = response.body.getReader(); | |
| const chunks = []; | |
| let received = 0; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| chunks.push(value); | |
| received += value.length; | |
| onProgress(received / total, total); | |
| } | |
| const bytes = new Uint8Array(received); | |
| let offset = 0; | |
| for (const chunk of chunks) { | |
| bytes.set(chunk, offset); | |
| offset += chunk.length; | |
| } | |
| return bytes; | |
| } | |
| function renderClassChips() { | |
| els.classChips.replaceChildren(); | |
| for (const label of state.labels) { | |
| const chip = document.createElement('span'); | |
| chip.className = 'class-chip'; | |
| chip.textContent = label; | |
| els.classChips.appendChild(chip); | |
| } | |
| } | |
| function renderModelFacts() { | |
| if (!state.metadata) return; | |
| const facts = [ | |
| ['Input', `${state.metadata.input.width} × ${state.metadata.input.height} RGB`], | |
| ['Classes', String(state.labels.length)], | |
| ['Parameters', `${(state.metadata.complexity.parameters / 1e6).toFixed(3)}M`], | |
| ['Runtime', 'WebAssembly'], | |
| ]; | |
| els.modelFacts.replaceChildren(); | |
| for (const [term, value] of facts) { | |
| const wrapper = document.createElement('div'); | |
| const dt = document.createElement('dt'); | |
| const dd = document.createElement('dd'); | |
| dt.textContent = term; | |
| dd.textContent = value; | |
| wrapper.append(dt, dd); | |
| els.modelFacts.appendChild(wrapper); | |
| } | |
| } | |
| async function initializeModel() { | |
| try { | |
| if (!window.ort) throw new Error('ONNX Runtime Web did not load from the pinned CDN.'); | |
| setRuntimeState('', 'Preparing model'); | |
| setProgress(3, 'Preparing class labels and model information…'); | |
| renderClassChips(); | |
| renderModelFacts(); | |
| ort.env.logLevel = 'error'; | |
| ort.env.wasm.numThreads = 1; | |
| ort.env.wasm.proxy = false; | |
| ort.env.wasm.wasmPaths = `https://cdn.jsdelivr.net/npm/onnxruntime-web@${APP.ortVersion}/dist/`; | |
| setRuntimeState('', 'Downloading model'); | |
| const modelBytes = await fetchBinaryWithProgress(APP.modelUrl, (fraction, totalBytes) => { | |
| const percent = 5 + (fraction * 66); | |
| setProgress(percent, `Downloading ${formatBytes(totalBytes) || 'ONNX model'}…`); | |
| }); | |
| setProgress(75, 'Creating optimized WebAssembly inference session…'); | |
| setRuntimeState('', 'Creating session'); | |
| state.session = await ort.InferenceSession.create(modelBytes, { | |
| executionProviders: ['wasm'], | |
| graphOptimizationLevel: 'all', | |
| }); | |
| const inputNames = Array.isArray(state.session.inputNames) ? state.session.inputNames : []; | |
| const outputNames = Array.isArray(state.session.outputNames) ? state.session.outputNames : []; | |
| if (!inputNames.includes('input') || !outputNames.includes('logits')) { | |
| throw new Error(`Unexpected ONNX interface: inputs ${inputNames.join(', ') || 'none'}, outputs ${outputNames.join(', ') || 'none'}`); | |
| } | |
| // Run one deterministic smoke test before enabling the interface. This catches | |
| // invalid model files, unsupported operators, and output-shape mismatches. | |
| setProgress(92, 'Verifying model execution and output shape…'); | |
| const testData = new Float32Array(3 * APP.imageSize * APP.imageSize); | |
| const testTensor = new ort.Tensor('float32', testData, [1, 3, APP.imageSize, APP.imageSize]); | |
| const testOutputs = await state.session.run({ input: testTensor }); | |
| const testLogits = testOutputs.logits && testOutputs.logits.data; | |
| if (!testLogits || testLogits.length !== state.labels.length) { | |
| const actualLength = testLogits ? testLogits.length : 0; | |
| throw new Error(`ONNX smoke test failed: expected ${state.labels.length} logits, received ${actualLength}.`); | |
| } | |
| if (!Array.from(testLogits).every(Number.isFinite)) { | |
| throw new Error('ONNX smoke test failed: output contains non-finite values.'); | |
| } | |
| state.modelReady = true; | |
| els.statusCard.classList.remove('error'); | |
| setProgress(100, 'Ready. The ONNX model has loaded successfully.'); | |
| setRuntimeState('ready', 'Model ready'); | |
| els.providerBadge.textContent = 'WASM'; | |
| updatePredictButton(); | |
| } catch (error) { | |
| setError(error.message || 'Model initialization failed.', error); | |
| } | |
| } | |
| async function decodeImage(file) { | |
| if ('createImageBitmap' in window) { | |
| try { | |
| return await createImageBitmap(file, { imageOrientation: 'from-image' }); | |
| } catch (_) { | |
| return createImageBitmap(file); | |
| } | |
| } | |
| return new Promise((resolve, reject) => { | |
| const url = URL.createObjectURL(file); | |
| const image = new Image(); | |
| image.onload = () => { | |
| URL.revokeObjectURL(url); | |
| resolve(image); | |
| }; | |
| image.onerror = () => { | |
| URL.revokeObjectURL(url); | |
| reject(new Error('The selected file could not be decoded as an image.')); | |
| }; | |
| image.src = url; | |
| }); | |
| } | |
| function resetResults() { | |
| els.results.classList.add('hidden'); | |
| els.emptyResult.classList.remove('hidden'); | |
| els.topThree.replaceChildren(); | |
| els.probabilityList.replaceChildren(); | |
| } | |
| function clearImage() { | |
| if (state.bitmap && typeof state.bitmap.close === 'function') state.bitmap.close(); | |
| if (state.objectUrl) URL.revokeObjectURL(state.objectUrl); | |
| state.bitmap = null; | |
| state.objectUrl = null; | |
| state.file = null; | |
| els.fileInput.value = ''; | |
| els.imagePreview.removeAttribute('src'); | |
| els.previewWrap.classList.add('hidden'); | |
| els.dropZone.classList.remove('hidden'); | |
| els.fileName.textContent = ''; | |
| els.fileDetails.textContent = ''; | |
| els.inputMessage.textContent = ''; | |
| els.inputMessage.classList.remove('error'); | |
| resetResults(); | |
| updatePredictButton(); | |
| } | |
| async function handleFile(file) { | |
| if (!file) return; | |
| if (!file.type.startsWith('image/')) { | |
| els.inputMessage.textContent = 'Select a valid image file.'; | |
| els.inputMessage.classList.add('error'); | |
| return; | |
| } | |
| try { | |
| els.inputMessage.textContent = 'Decoding image…'; | |
| els.inputMessage.classList.remove('error'); | |
| if (state.bitmap && typeof state.bitmap.close === 'function') state.bitmap.close(); | |
| if (state.objectUrl) URL.revokeObjectURL(state.objectUrl); | |
| state.bitmap = await decodeImage(file); | |
| state.file = file; | |
| state.objectUrl = URL.createObjectURL(file); | |
| els.imagePreview.src = state.objectUrl; | |
| els.fileName.textContent = file.name; | |
| els.fileDetails.textContent = `${state.bitmap.width} × ${state.bitmap.height} · ${formatBytes(file.size)}`; | |
| els.dropZone.classList.add('hidden'); | |
| els.previewWrap.classList.remove('hidden'); | |
| els.inputMessage.textContent = state.modelReady ? 'Image ready for classification.' : 'Image ready. Waiting for the model to finish loading.'; | |
| resetResults(); | |
| updatePredictButton(); | |
| } catch (error) { | |
| els.inputMessage.textContent = error.message || 'Could not read this image.'; | |
| els.inputMessage.classList.add('error'); | |
| clearImage(); | |
| } | |
| } | |
| function preprocessImage(bitmap) { | |
| const canvas = els.canvas; | |
| const context = canvas.getContext('2d', { willReadFrequently: true }); | |
| canvas.width = APP.imageSize; | |
| canvas.height = APP.imageSize; | |
| context.clearRect(0, 0, APP.imageSize, APP.imageSize); | |
| context.imageSmoothingEnabled = true; | |
| context.imageSmoothingQuality = 'high'; | |
| context.drawImage(bitmap, 0, 0, APP.imageSize, APP.imageSize); | |
| const rgba = context.getImageData(0, 0, APP.imageSize, APP.imageSize).data; | |
| const planeSize = APP.imageSize * APP.imageSize; | |
| const nchw = new Float32Array(3 * planeSize); | |
| for (let pixel = 0; pixel < planeSize; pixel += 1) { | |
| const source = pixel * 4; | |
| nchw[pixel] = ((rgba[source] / 255) - APP.mean[0]) / APP.std[0]; | |
| nchw[planeSize + pixel] = ((rgba[source + 1] / 255) - APP.mean[1]) / APP.std[1]; | |
| nchw[(2 * planeSize) + pixel] = ((rgba[source + 2] / 255) - APP.mean[2]) / APP.std[2]; | |
| } | |
| return new ort.Tensor('float32', nchw, [1, 3, APP.imageSize, APP.imageSize]); | |
| } | |
| function softmax(logits) { | |
| const maximum = Math.max(...logits); | |
| const exponentials = logits.map(value => Math.exp(value - maximum)); | |
| const denominator = exponentials.reduce((sum, value) => sum + value, 0); | |
| return exponentials.map(value => value / denominator); | |
| } | |
| function renderResults(probabilities, timings) { | |
| const ranked = probabilities | |
| .map((probability, index) => ({ label: state.labels[index], probability })) | |
| .sort((a, b) => b.probability - a.probability); | |
| const winner = ranked[0]; | |
| const confidencePercent = winner.probability * 100; | |
| els.predictedClass.textContent = titleCase(winner.label); | |
| els.confidenceValue.textContent = `${confidencePercent.toFixed(1)}%`; | |
| els.confidenceRing.style.setProperty('--confidence', `${winner.probability * 360}deg`); | |
| els.confidenceRing.setAttribute('aria-label', `${confidencePercent.toFixed(1)} percent confidence`); | |
| els.topThree.replaceChildren(); | |
| for (const item of ranked.slice(0, 3)) { | |
| const row = document.createElement('li'); | |
| const name = document.createElement('span'); | |
| const value = document.createElement('span'); | |
| name.className = 'class-name'; | |
| value.className = 'class-prob'; | |
| name.textContent = item.label; | |
| value.textContent = `${(item.probability * 100).toFixed(2)}%`; | |
| row.append(name, value); | |
| els.topThree.appendChild(row); | |
| } | |
| els.probabilityList.replaceChildren(); | |
| for (const item of ranked) { | |
| const row = document.createElement('div'); | |
| row.className = 'probability-row'; | |
| const name = document.createElement('span'); | |
| name.className = 'name'; | |
| name.textContent = item.label; | |
| const track = document.createElement('div'); | |
| track.className = 'bar-track'; | |
| const fill = document.createElement('div'); | |
| fill.className = 'bar-fill'; | |
| fill.style.width = `${Math.max(item.probability * 100, 0.3)}%`; | |
| track.appendChild(fill); | |
| const value = document.createElement('span'); | |
| value.className = 'value'; | |
| value.textContent = `${(item.probability * 100).toFixed(2)}%`; | |
| row.append(name, track, value); | |
| els.probabilityList.appendChild(row); | |
| } | |
| els.preprocessTime.textContent = formatMilliseconds(timings.preprocess); | |
| els.inferenceTime.textContent = formatMilliseconds(timings.inference); | |
| els.totalTime.textContent = formatMilliseconds(timings.total); | |
| els.emptyResult.classList.add('hidden'); | |
| els.results.classList.remove('hidden'); | |
| } | |
| async function predict() { | |
| if (!state.modelReady || !state.bitmap || state.predicting) return; | |
| state.predicting = true; | |
| updatePredictButton(); | |
| els.predictButtonText.textContent = 'Classifying…'; | |
| els.buttonSpinner.classList.remove('hidden'); | |
| els.inputMessage.textContent = 'Running browser-side inference…'; | |
| els.inputMessage.classList.remove('error'); | |
| try { | |
| const totalStart = performance.now(); | |
| const preprocessStart = performance.now(); | |
| const inputTensor = preprocessImage(state.bitmap); | |
| const preprocessEnd = performance.now(); | |
| const inferenceStart = performance.now(); | |
| const outputs = await state.session.run({ input: inputTensor }); | |
| const inferenceEnd = performance.now(); | |
| const outputTensor = outputs.logits; | |
| if (!outputTensor || !outputTensor.data) { | |
| throw new Error('The ONNX runtime did not return the expected logits output.'); | |
| } | |
| const logits = Array.from(outputTensor.data); | |
| if (logits.length !== state.labels.length) { | |
| throw new Error(`Expected ${state.labels.length} logits but received ${logits.length}.`); | |
| } | |
| const probabilities = softmax(logits); | |
| const totalEnd = performance.now(); | |
| renderResults(probabilities, { | |
| preprocess: preprocessEnd - preprocessStart, | |
| inference: inferenceEnd - inferenceStart, | |
| total: totalEnd - totalStart, | |
| }); | |
| els.inputMessage.textContent = 'Prediction completed locally in this browser.'; | |
| } catch (error) { | |
| console.error(error); | |
| els.inputMessage.textContent = error.message || 'Inference failed.'; | |
| els.inputMessage.classList.add('error'); | |
| } finally { | |
| state.predicting = false; | |
| els.predictButtonText.textContent = 'Classify image'; | |
| els.buttonSpinner.classList.add('hidden'); | |
| updatePredictButton(); | |
| } | |
| } | |
| function preventDefaults(event) { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| } | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| els.dropZone.addEventListener(eventName, event => { | |
| preventDefaults(event); | |
| els.dropZone.classList.add('dragging'); | |
| }); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| els.dropZone.addEventListener(eventName, event => { | |
| preventDefaults(event); | |
| els.dropZone.classList.remove('dragging'); | |
| }); | |
| }); | |
| els.dropZone.addEventListener('drop', event => handleFile(event.dataTransfer.files[0])); | |
| els.dropZone.addEventListener('keydown', event => { | |
| if (event.key === 'Enter' || event.key === ' ') { | |
| event.preventDefault(); | |
| els.fileInput.click(); | |
| } | |
| }); | |
| els.fileInput.addEventListener('change', event => handleFile(event.target.files[0])); | |
| els.clearButton.addEventListener('click', clearImage); | |
| els.predictButton.addEventListener('click', predict); | |
| window.addEventListener('beforeunload', () => { | |
| if (state.bitmap && typeof state.bitmap.close === 'function') state.bitmap.close(); | |
| if (state.objectUrl) URL.revokeObjectURL(state.objectUrl); | |
| }); | |
| initializeModel(); | |