// ── State ────────────────────────────────────────────────────────────────── let currentFile = null; let lastJSON = null; // ── File Input ───────────────────────────────────────────────────────────── const fileInput = document.getElementById('file-input'); const dropZone = document.getElementById('drop-zone'); fileInput.addEventListener('change', e => handleFile(e.target.files[0])); 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) handleFile(f); }); dropZone.addEventListener('click', (e) => { if (e.target !== fileInput) { fileInput.click(); } }); function handleFile(file) { if (!file || !file.type.startsWith('image/')) { showToast('⚠️ Vui lòng chọn file ảnh!', true); return; } currentFile = file; // Preview const reader = new FileReader(); reader.onload = e => { document.getElementById('preview-img').src = e.target.result; document.getElementById('preview-name').textContent = `${file.name} · ${(file.size / 1024).toFixed(1)} KB`; document.getElementById('preview-wrap').style.display = 'block'; }; reader.readAsDataURL(file); document.getElementById('btn-predict').disabled = false; } // ── Predict ──────────────────────────────────────────────────────────────── async function runPredict() { if (!currentFile) return; setLoading(true); const formData = new FormData(); formData.append('image', currentFile); formData.append('conf', document.getElementById('conf-slider').value); formData.append('iou', document.getElementById('iou-slider').value); try { const res = await fetch('/predict', { method: 'POST', body: formData }); const data = await res.json(); if (!res.ok || data.error) { throw new Error(data.error || 'Lỗi server'); } // ① Update annotated image const resImg = document.getElementById('result-img'); resImg.src = `data:image/jpeg;base64,${data.image_b64}`; resImg.style.display = 'block'; document.getElementById('placeholder').style.display = 'none'; // ② Stats bar document.getElementById('stat-time').textContent = `${data.inference_time_ms} ms`; document.getElementById('stat-count').textContent = `${data.total_detections} vật thể`; document.getElementById('stats-bar').style.display = 'flex'; // ③ JSON lastJSON = data; updateOCRPanel(data.detections); const displayDetections = data.detections.map(d => { const copy = { ...d }; delete copy.crop_b64; return copy; }); const finalJSON = { image: data.filename || "unknown.jpg", objects: displayDetections }; document.getElementById('json-output').innerHTML = syntaxHighlight(JSON.stringify(finalJSON, null, 2)); showToast(`✅ Phát hiện ${data.total_detections} vật thể · ${data.inference_time_ms}ms`); } catch (err) { showToast('❌ ' + err.message, true); document.getElementById('json-output').textContent = `// Lỗi: ${err.message}`; } finally { setLoading(false); } } // ── Helpers ──────────────────────────────────────────────────────────────── function setLoading(on) { document.getElementById('spinner').style.display = on ? 'block' : 'none'; document.getElementById('btn-text').textContent = on ? 'Đang xử lý...' : '⚡ Nhận diện'; document.getElementById('btn-predict').disabled = on; } function syntaxHighlight(json) { return json.replace( /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, match => { let cls = 'j-num'; if (/^"/.test(match)) { cls = /:$/.test(match) ? 'j-key' : 'j-str'; } else if (/true|false/.test(match)) { cls = 'j-bool'; } else if (/null/.test(match)) { cls = 'j-null'; } return `${match}`; } ); } function updateOCRPanel(detections) { const ocrOutput = document.getElementById('ocr-output'); const ocrItems = detections.map((item, index) => ({ id: index + 1, className: item.class, text: item.ocr_content ? item.ocr_content.trim() : "", crop: item.crop_image, crop_b64: item.crop_b64 })); if (ocrItems.length === 0) { ocrOutput.innerHTML = '

Không tìm thấy đối tượng nào.

'; return; } ocrOutput.innerHTML = ocrItems.map(item => `
${item.className} #${item.id} ${item.crop_b64 ? `⬇ Tải Ảnh Cắt` : ''}
${item.text ? `
${escapeHtml(item.text)}
` : ''}
`).join(''); } function downloadCrop(id) { if (!lastJSON || !lastJSON.detections) return; const item = lastJSON.detections.find(d => d.id === id); if (!item || !item.crop_b64) return; try { const byteCharacters = atob(item.crop_b64); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray], {type: 'image/jpeg'}); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = item.crop_image || `crop_${id}.jpg`; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); showToast('⬇ Đã tải ảnh cắt!'); } catch (err) { showToast('❌ Lỗi tải ảnh!', true); console.error(err); } } function escapeHtml(value) { return value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function copyJSON() { if (!lastJSON) { showToast('Chưa có kết quả!', true); return; } const displayDetections = lastJSON.detections.map(d => { const copy = { ...d }; delete copy.crop_b64; return copy; }); const finalJSON = { image: lastJSON.filename || "unknown.jpg", objects: displayDetections }; navigator.clipboard.writeText(JSON.stringify(finalJSON, null, 2)); showToast('📋 Đã copy JSON!'); } function downloadJSON() { if (!lastJSON) { showToast('Chưa có kết quả!', true); return; } const displayDetections = lastJSON.detections.map(d => { const copy = { ...d }; delete copy.crop_b64; return copy; }); const finalJSON = { image: lastJSON.filename || "unknown.jpg", objects: displayDetections }; const blob = new Blob([JSON.stringify(finalJSON, null, 2)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `rtdetr_result_${Date.now()}.json`; a.click(); showToast('⬇ Đã tải JSON!'); } function clearAll() { currentFile = null; lastJSON = null; document.getElementById('preview-wrap').style.display = 'none'; document.getElementById('result-img').style.display = 'none'; document.getElementById('placeholder').style.display = 'flex'; document.getElementById('stats-bar').style.display = 'none'; document.getElementById('json-output').textContent = '// Chạy nhận diện để xem kết quả JSON ở đây...'; document.getElementById('ocr-output').innerHTML = '

Các đối tượng được cắt (Crops) và nội dung OCR sẽ hiển thị ở đây.

'; document.getElementById('btn-predict').disabled = true; fileInput.value = ''; } function showToast(msg, isError = false) { const t = document.getElementById('toast'); t.textContent = msg; t.className = isError ? 'error' : ''; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 3000); }