Koshur-OCR / app.js
KashlabsAI's picture
Deploy static Koshur OCR interface with Modal backend
e699840 verified
Raw
History Blame Contribute Delete
4.09 kB
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";
}
})();