Download web/app.js from borreooo/ocr: direct link, hf CLI and curl.
- Browser
- Download file 16.8 kB
-
https://huggingface.co/spaces/borreooo/ocr/resolve/main/web/app.js
- Command line
-
hf download hf://spaces/borreooo/ocr/web/app.js
-
curl -L -o app.js https://huggingface.co/spaces/borreooo/ocr/resolve/main/web/app.js
16.8 kB
| document.addEventListener('DOMContentLoaded', () => { | |
| const API = window.location.origin; | |
| // ββ Toast ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function toast(msg, type = 'info') { | |
| const el = document.createElement('div'); | |
| el.className = `toast ${type}`; | |
| el.textContent = msg; | |
| document.getElementById('toasts').appendChild(el); | |
| setTimeout(() => el.remove(), 3500); | |
| } | |
| // ββ Backend status βββββββββββββββββββββββββββββββββββββββ | |
| const dot = document.getElementById('connection-badge'); | |
| function checkStatus() { | |
| fetch(`${API}/api/status`) | |
| .then(() => { dot.className = 'status-dot online'; }) | |
| .catch(() => { dot.className = 'status-dot offline'; }); | |
| } | |
| checkStatus(); | |
| setInterval(checkStatus, 20000); | |
| // ββ PWA install ββββββββββββββββββββββββββββββββββββββββββ | |
| let installPrompt; | |
| const installBtn = document.getElementById('pwa-install-btn'); | |
| window.addEventListener('beforeinstallprompt', e => { | |
| e.preventDefault(); | |
| installPrompt = e; | |
| installBtn.classList.remove('hidden'); | |
| }); | |
| installBtn.addEventListener('click', async () => { | |
| if (!installPrompt) return; | |
| installPrompt.prompt(); | |
| const { outcome } = await installPrompt.userChoice; | |
| if (outcome === 'accepted') toast('App installed!', 'success'); | |
| installPrompt = null; | |
| installBtn.classList.add('hidden'); | |
| }); | |
| // ββ Mode switching βββββββββββββββββββββββββββββββββββββββ | |
| let currentMode = 'dataset'; | |
| let chassisBlob = null; | |
| let barcodeBlob = null; | |
| document.querySelectorAll('.seg-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('.seg-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| currentMode = btn.dataset.mode; | |
| ['dataset', 'upload', 'camera'].forEach(m => | |
| document.getElementById(`mode-${m}`).classList.add('hidden') | |
| ); | |
| document.getElementById(`mode-${currentMode}`).classList.remove('hidden'); | |
| stopAllCameras(); | |
| validate(); | |
| }); | |
| }); | |
| // ββ Dataset pairs ββββββββββββββββββββββββββββββββββββββββ | |
| const pairSelect = document.getElementById('dataset-pair-select'); | |
| const barcodeValInput = document.getElementById('barcode-val'); | |
| fetch(`${API}/api/test-pairs`) | |
| .then(r => r.json()) | |
| .then(pairs => { | |
| pairSelect.innerHTML = '<option value="">β Choose a pair β</option>'; | |
| pairs.forEach(p => { | |
| const o = document.createElement('option'); | |
| o.value = o.textContent = p; | |
| pairSelect.appendChild(o); | |
| }); | |
| }) | |
| .catch(() => toast('Could not load dataset pairs', 'error')); | |
| pairSelect.addEventListener('change', () => { | |
| const key = pairSelect.value; | |
| if (!key) { barcodeValInput.value = ''; validate(); return; } | |
| barcodeValInput.value = 'Decodingβ¦'; | |
| barcodeValInput.disabled = true; | |
| fetch(`${API}/api/scan-barcode/${encodeURIComponent(key)}`) | |
| .then(r => r.json()) | |
| .then(d => { | |
| barcodeValInput.value = d.success ? d.barcode : ''; | |
| if (!d.success) toast('Barcode not decoded. Enter manually.', 'info'); | |
| }) | |
| .catch(() => { barcodeValInput.value = ''; toast('Barcode scan failed', 'error'); }) | |
| .finally(() => { barcodeValInput.disabled = false; validate(); }); | |
| }); | |
| // ββ Upload dropzones βββββββββββββββββββββββββββββββββββββ | |
| function setupDropzone(dzId, inputId, nameId, isBarcode) { | |
| const dz = document.getElementById(dzId); | |
| const inp = document.getElementById(inputId); | |
| const nm = document.getElementById(nameId); | |
| dz.addEventListener('click', () => inp.click()); | |
| dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('active'); }); | |
| dz.addEventListener('dragleave', () => dz.classList.remove('active')); | |
| dz.addEventListener('drop', e => { | |
| e.preventDefault(); | |
| dz.classList.remove('active'); | |
| if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0], nm, isBarcode); | |
| }); | |
| inp.addEventListener('change', () => { | |
| if (inp.files[0]) handleFile(inp.files[0], nm, isBarcode); | |
| }); | |
| } | |
| function handleFile(file, nameEl, isBarcode) { | |
| nameEl.textContent = file.name; | |
| if (isBarcode) { | |
| barcodeBlob = file; | |
| barcodeValInput.value = 'Decodingβ¦'; | |
| barcodeValInput.disabled = true; | |
| const fd = new FormData(); | |
| fd.append('file', file); | |
| fetch(`${API}/api/scan-barcode`, { method: 'POST', body: fd }) | |
| .then(r => r.json()) | |
| .then(d => { | |
| barcodeValInput.value = d.success ? d.barcode : ''; | |
| if (d.success) toast('Barcode decoded', 'success'); | |
| else toast('Could not decode barcode. Enter manually.', 'info'); | |
| }) | |
| .catch(() => { barcodeValInput.value = ''; toast('Barcode scan error', 'error'); }) | |
| .finally(() => { barcodeValInput.disabled = false; validate(); }); | |
| } else { | |
| chassisBlob = file; | |
| validate(); | |
| } | |
| } | |
| setupDropzone('barcode-dropzone', 'barcode-file', 'barcode-file-name', true); | |
| setupDropzone('chassis-dropzone', 'chassis-file', 'chassis-file-name', false); | |
| // ββ Camera β barcode βββββββββββββββββββββββββββββββββββββ | |
| let qr = null; | |
| const startBarcodeCam = document.getElementById('start-barcode-cam'); | |
| const camStep2 = document.getElementById('cam-step-2'); | |
| startBarcodeCam.addEventListener('click', () => { | |
| if (qr) { | |
| qr.stop().finally(() => { qr = null; startBarcodeCam.textContent = 'Start Camera'; }); | |
| return; | |
| } | |
| startBarcodeCam.textContent = 'Stop'; | |
| qr = new Html5Qrcode('barcode-reader'); | |
| qr.start( | |
| { facingMode: 'environment' }, | |
| { fps: 10, qrbox: { width: 240, height: 100 } }, | |
| decoded => { | |
| barcodeValInput.value = decoded; | |
| toast(`Barcode: ${decoded}`, 'success'); | |
| qr.stop().finally(() => { | |
| qr = null; | |
| startBarcodeCam.textContent = 'Start Camera'; | |
| camStep2.classList.remove('disabled'); | |
| document.getElementById('start-chassis-cam').disabled = false; | |
| validate(); | |
| }); | |
| }, | |
| () => { } | |
| ).catch(e => { toast(`Camera error: ${e}`, 'error'); startBarcodeCam.textContent = 'Start Camera'; qr = null; }); | |
| }); | |
| // ββ Camera β chassis βββββββββββββββββββββββββββββββββββββ | |
| let chassisStream = null; | |
| const chassisVideo = document.getElementById('chassis-video'); | |
| const chassisCanvas = document.getElementById('chassis-canvas'); | |
| const snapContainer = document.getElementById('chassis-snap-container'); | |
| const snapImg = document.getElementById('chassis-snap-img'); | |
| const startChassisCam = document.getElementById('start-chassis-cam'); | |
| const captureBtn = document.getElementById('capture-chassis'); | |
| const retakeBtn = document.getElementById('reset-chassis-snap'); | |
| const guideBox = document.getElementById('chassis-guide-box'); | |
| // Guide box region as fractions of the video frame (matches CSS: 70% wide, 22% tall, centered) | |
| const GUIDE_BOX_FRACTION = { widthFrac: 0.70, heightFrac: 0.22, xFrac: 0.15, yFrac: 0.39 }; | |
| startChassisCam.addEventListener('click', async () => { | |
| if (chassisStream) { stopChassisCamera(); return; } | |
| try { | |
| chassisStream = await navigator.mediaDevices.getUserMedia({ | |
| video: { facingMode: 'environment', width: { ideal: 1280 } }, audio: false | |
| }); | |
| chassisVideo.srcObject = chassisStream; | |
| chassisVideo.classList.remove('hidden'); | |
| guideBox.classList.remove('hidden'); | |
| captureBtn.classList.remove('hidden'); | |
| snapContainer.classList.add('hidden'); | |
| retakeBtn.classList.add('hidden'); | |
| startChassisCam.textContent = 'Stop'; | |
| } catch (e) { toast(`Camera error: ${e.message}`, 'error'); } | |
| }); | |
| captureBtn.addEventListener('click', () => { | |
| const vw = chassisVideo.videoWidth; | |
| const vh = chassisVideo.videoHeight; | |
| // Crop to just the guide box region so OCR only sees the framed chassis strip | |
| const cropX = vw * GUIDE_BOX_FRACTION.xFrac; | |
| const cropY = vh * GUIDE_BOX_FRACTION.yFrac; | |
| const cropW = vw * GUIDE_BOX_FRACTION.widthFrac; | |
| const cropH = vh * GUIDE_BOX_FRACTION.heightFrac; | |
| chassisCanvas.width = cropW; | |
| chassisCanvas.height = cropH; | |
| chassisCanvas.getContext('2d').drawImage( | |
| chassisVideo, | |
| cropX, cropY, cropW, cropH, // source rect (the guide box area) | |
| 0, 0, cropW, cropH // destination rect (full canvas) | |
| ); | |
| chassisCanvas.toBlob(blob => { | |
| chassisBlob = blob; | |
| snapImg.src = URL.createObjectURL(blob); | |
| chassisVideo.classList.add('hidden'); | |
| guideBox.classList.add('hidden'); | |
| captureBtn.classList.add('hidden'); | |
| snapContainer.classList.remove('hidden'); | |
| retakeBtn.classList.remove('hidden'); | |
| stopChassisCamera(); | |
| validate(); | |
| }, 'image/jpeg'); | |
| }); | |
| retakeBtn.addEventListener('click', () => { | |
| chassisBlob = null; | |
| snapContainer.classList.add('hidden'); | |
| retakeBtn.classList.add('hidden'); | |
| startChassisCam.click(); | |
| validate(); | |
| }); | |
| function stopChassisCamera() { | |
| if (chassisStream) { chassisStream.getTracks().forEach(t => t.stop()); chassisStream = null; } | |
| chassisVideo.srcObject = null; | |
| chassisVideo.classList.add('hidden'); | |
| guideBox.classList.add('hidden'); | |
| captureBtn.classList.add('hidden'); | |
| startChassisCam.textContent = 'Start Camera'; | |
| } | |
| function stopAllCameras() { | |
| if (qr) { qr.stop().catch(() => { }); qr = null; startBarcodeCam.textContent = 'Start Camera'; } | |
| stopChassisCamera(); | |
| } | |
| // ββ Validation βββββββββββββββββββββββββββββββββββββββββββ | |
| const runBtn = document.getElementById('run-ocr-btn'); | |
| barcodeValInput.addEventListener('input', validate); | |
| function validate() { | |
| const hasBarcode = barcodeValInput.value.trim() !== '' && barcodeValInput.value !== 'Decodingβ¦'; | |
| let hasChassis = false; | |
| if (currentMode === 'dataset') hasChassis = pairSelect.value !== ''; | |
| else hasChassis = chassisBlob !== null; | |
| runBtn.disabled = !(hasBarcode && hasChassis); | |
| } | |
| // ββ Run OCR ββββββββββββββββββββββββββββββββββββββββββββββ | |
| let lastResult = null; | |
| runBtn.addEventListener('click', () => { | |
| runBtn.disabled = true; | |
| runBtn.querySelector('.spinner').classList.remove('hidden'); | |
| const fd = new FormData(); | |
| fd.append('barcode_val', barcodeValInput.value.trim().toUpperCase()); | |
| if (currentMode === 'dataset') { | |
| fd.append('chassis_key', pairSelect.value); | |
| } else { | |
| fd.append('chassis_file', chassisBlob, currentMode === 'camera' ? 'chassis.jpg' : chassisBlob.name); | |
| } | |
| fetch(`${API}/api/match`, { method: 'POST', body: fd }) | |
| .then(r => { | |
| if (!r.ok) return r.json().then(e => { throw new Error(e.detail); }); | |
| return r.json(); | |
| }) | |
| .then(data => { lastResult = data; renderResult(data); toast('Scan complete', 'success'); }) | |
| .catch(e => toast(e.message || 'OCR failed', 'error')) | |
| .finally(() => { | |
| runBtn.disabled = false; | |
| runBtn.querySelector('.spinner').classList.add('hidden'); | |
| }); | |
| }); | |
| function renderResult(d) { | |
| document.getElementById('result-empty').classList.add('hidden'); | |
| document.getElementById('result-content').classList.remove('hidden'); | |
| // Banner | |
| const banner = document.getElementById('result-banner'); | |
| banner.className = `result-banner ${d.status}`; | |
| const icons = { | |
| EXACT: 'β', | |
| CORRECTED: '~', | |
| FAILED: 'β' | |
| }; | |
| const subs = { | |
| EXACT: 'Raw OCR matched the barcode exactly.', | |
| CORRECTED: 'Match achieved after error-correction.', | |
| FAILED: 'OCR output could not be matched.' | |
| }; | |
| document.getElementById('banner-icon').textContent = icons[d.status]; | |
| document.getElementById('banner-title').textContent = d.status === 'EXACT' ? 'Exact Match' : d.status === 'CORRECTED' ? 'Corrected Match' : 'No Match'; | |
| document.getElementById('banner-sub').textContent = subs[d.status]; | |
| // Table | |
| document.getElementById('res-expected').textContent = d.barcode_val; | |
| document.getElementById('res-raw').textContent = d.raw_ocr || 'β'; | |
| document.getElementById('res-corrected').textContent = d.corrected_ocr || 'β'; | |
| document.getElementById('res-conf').textContent = `${Math.round(d.confidence * 100)}%`; | |
| document.getElementById('res-filter').textContent = d.winning_label || 'β'; | |
| // Preprocessing carousel | |
| const tabs = document.querySelectorAll('.pre-tab'); | |
| tabs.forEach(t => t.replaceWith(t.cloneNode(true))); | |
| const newTabs = document.querySelectorAll('.pre-tab'); | |
| function setPreview(type) { | |
| const img = document.getElementById('pre-img'); | |
| const lbl = document.getElementById('pre-ocr-text'); | |
| if (type === 'original') { | |
| img.src = `data:image/jpeg;base64,${d.original}`; | |
| lbl.textContent = d.raw_ocr || 'β'; | |
| } else { | |
| const v = d.variations.find(x => x.label.toLowerCase() === type); | |
| const det = d.variation_details.find(x => x.label.toLowerCase() === type); | |
| if (v) img.src = `data:image/jpeg;base64,${v.base64}`; | |
| lbl.textContent = det ? (det.text || 'β') : 'β'; | |
| } | |
| } | |
| newTabs.forEach(tab => { | |
| tab.addEventListener('click', () => { | |
| newTabs.forEach(t => t.classList.remove('active')); | |
| tab.classList.add('active'); | |
| setPreview(tab.dataset.pre); | |
| }); | |
| }); | |
| // Set initial to original | |
| document.querySelector('.pre-tab[data-pre="original"]').classList.add('active'); | |
| setPreview('original'); | |
| } | |
| // ββ Modal ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const modal = document.getElementById('modal'); | |
| const modalImg = document.getElementById('modal-img'); | |
| const modalTitle = document.getElementById('modal-title'); | |
| function openModal(url, title) { | |
| modalTitle.textContent = title; | |
| modalImg.src = `${url}?t=${Date.now()}`; | |
| modal.classList.remove('hidden'); | |
| } | |
| document.getElementById('modal-close').addEventListener('click', () => modal.classList.add('hidden')); | |
| modal.addEventListener('click', e => { if (e.target === modal) modal.classList.add('hidden'); }); | |
| // ββ Service Worker βββββββββββββββββββββββββββββββββββββββ | |
| if ('serviceWorker' in navigator) { | |
| navigator.serviceWorker.register('/sw.js').catch(() => { }); | |
| } | |
| }); |