const API = "https://kash-ai-labs--koshur-ocr-backend-api.modal.run"; const fileInput = document.querySelector("#image-file"); const dropZone = document.querySelector("#drop-zone"); const preview = document.querySelector("#preview"); const previewPlaceholder = document.querySelector("#preview-placeholder"); const recognizeButton = document.querySelector("#recognize"); const clearButton = document.querySelector("#clear"); const copyButton = document.querySelector("#copy"); const result = document.querySelector("#result"); const status = document.querySelector("#status"); const serviceDot = document.querySelector("#service-dot"); const serviceText = document.querySelector("#service-text"); let selectedFile = null; let previewUrl = null; function setStatus(message, kind = "neutral") { status.textContent = message; status.dataset.kind = kind; } function setFile(file) { if (!file || !file.type.startsWith("image/")) { setStatus("Choose a PNG, JPEG, or WebP image.", "error"); return; } if (file.size > 10 * 1024 * 1024) { setStatus("The image is larger than the 10 MB limit.", "error"); return; } selectedFile = file; if (previewUrl) URL.revokeObjectURL(previewUrl); previewUrl = URL.createObjectURL(file); preview.src = previewUrl; preview.hidden = false; previewPlaceholder.hidden = true; recognizeButton.disabled = false; result.value = ""; setStatus(`${file.name} · ${(file.size / 1024).toFixed(1)} KB`, "neutral"); } fileInput.addEventListener("change", () => setFile(fileInput.files[0])); dropZone.addEventListener("dragover", (event) => { event.preventDefault(); dropZone.classList.add("is-dragging"); }); dropZone.addEventListener("dragleave", () => dropZone.classList.remove("is-dragging")); dropZone.addEventListener("drop", (event) => { event.preventDefault(); dropZone.classList.remove("is-dragging"); setFile(event.dataTransfer.files[0]); }); recognizeButton.addEventListener("click", async () => { if (!selectedFile) return; recognizeButton.disabled = true; recognizeButton.classList.add("is-loading"); result.value = ""; setStatus("Recognizing… The first request may take up to a minute while the model wakes.", "working"); const form = new FormData(); form.append("image_file", selectedFile, selectedFile.name); try { const response = await fetch(`${API}/ocr`, { method: "POST", body: form }); let payload; try { payload = await response.json(); } catch { payload = {}; } if (!response.ok) throw new Error(payload.detail || `Request failed (${response.status})`); result.value = payload.text || ""; const d = payload.diagnostics; setStatus( payload.text ? `Done · ${d.input_width}×${d.input_height} px · ${d.ctc_frames} CTC frames` : "No text detected. Try a tighter, clearer single-line crop.", payload.text ? "success" : "neutral" ); } catch (error) { setStatus(`Could not run OCR: ${error.message}`, "error"); } finally { recognizeButton.disabled = false; recognizeButton.classList.remove("is-loading"); } }); clearButton.addEventListener("click", () => { selectedFile = null; fileInput.value = ""; result.value = ""; preview.hidden = true; preview.removeAttribute("src"); previewPlaceholder.hidden = false; recognizeButton.disabled = true; if (previewUrl) URL.revokeObjectURL(previewUrl); previewUrl = null; setStatus("Upload a tightly cropped single-line image to begin.", "neutral"); }); copyButton.addEventListener("click", async () => { if (!result.value) return; await navigator.clipboard.writeText(result.value); copyButton.textContent = "Copied"; setTimeout(() => { copyButton.textContent = "Copy text"; }, 1400); }); (async function checkService() { try { const response = await fetch(`${API}/health`); if (!response.ok) throw new Error(); serviceDot.dataset.state = "online"; serviceText.textContent = "Inference online"; } catch { serviceDot.dataset.state = "sleeping"; serviceText.textContent = "Inference waking"; } })();