ocr / web /app.js
borreooo's picture
Add guide box for chassis camera
39a6f54
Raw
History Blame Contribute Delete
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(() => { });
}
});