Spaces:
Running
Running
| 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"; | |
| } | |
| })(); | |