import { useState, useEffect, useRef } from "react"; import * as pdfjsLib from 'pdfjs-dist'; import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url'; pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorkerSrc; import "./App.css"; function App() { // Worker reference const workerRef = useRef(null); // Application State const [modelStatus, setModelStatus] = useState("idle"); const [progressItems, setProgressItems] = useState({}); const [totalProgress, setTotalProgress] = useState(0); const [errorMsg, setErrorMsg] = useState(""); const [loadTime, setLoadTime] = useState(null); // Performance metrics const [loadedDevice, setLoadedDevice] = useState("Detecting..."); const [deviceConfig, setDeviceConfig] = useState("wasm"); const [generationTime, setGenerationTime] = useState(null); // VLM State const [imageFile, setImageFile] = useState(null); const [imagePreviewUrl, setImagePreviewUrl] = useState(""); const [extractedMetadata, setExtractedMetadata] = useState(null); const [isGenerating, setIsGenerating] = useState(false); // Initialize Web Worker useEffect(() => { workerRef.current = new Worker(new URL("./ai/worker.js", import.meta.url), { type: "module", }); const onMessageReceived = (e) => { const { type, data, error } = e.data; switch (type) { case "progress": setProgressItems((prev) => { const newItems = { ...prev, [data.file]: data }; const itemsArray = Object.values(newItems); const loaded = itemsArray.reduce((acc, item) => acc + (item.loaded || 0), 0); const total = itemsArray.reduce((acc, item) => acc + (item.total || 0), 0); if (total > 0) { setTotalProgress((loaded / total) * 100); } return newItems; }); break; case "ready": setModelStatus("ready"); setLoadedDevice(data.device === "webgpu" ? "WebGPU" : "WASM CPU"); setLoadTime((performance.now() - loadTime) / 1000); break; case "completed": setExtractedMetadata(data.metadata); setGenerationTime(data.durationSec); setIsGenerating(false); break; case "error": console.error("Worker Error:", error); setErrorMsg(error); setModelStatus("error"); setIsGenerating(false); break; default: break; } }; workerRef.current.onmessage = onMessageReceived; // Auto-load model on mount (try WebGPU first) setModelStatus("loading"); setProgressItems({}); setTotalProgress(0); setErrorMsg(""); setLoadTime(performance.now()); workerRef.current.postMessage({ type: "load", data: { device: "wasm" } }); // Clean up worker on unmount return () => { if (workerRef.current) { workerRef.current.terminate(); } }; }, []); function handleDeviceChange(newDevice) { if (isGenerating || modelStatus === "loading") return; setDeviceConfig(newDevice); setModelStatus("loading"); setProgressItems({}); setTotalProgress(0); setErrorMsg(""); setLoadTime(performance.now()); if (workerRef.current) { workerRef.current.postMessage({ type: "load", data: { device: newDevice } }); } } async function handleClearCache() { if (window.confirm("Are you sure you want to delete the cached model files? This will require downloading the VLM weights again.")) { try { // Clear the custom IndexedDB cache await new Promise((resolve, reject) => { const req = indexedDB.deleteDatabase('transformers-cache'); req.onsuccess = resolve; req.onerror = reject; req.onblocked = resolve; }); // Clear fallback Cache API just in case const cacheKeys = await caches.keys(); for (const key of cacheKeys) { if (key.includes("onnx") || key.includes("transformers") || key.includes("SmolVLM")) { await caches.delete(key); } } alert("Successfully cleared cached files from IndexedDB and Cache Storage!"); window.location.reload(); } catch (err) { alert("Failed to clear cache: " + err.message); } } } async function handleImageUpload(e) { const file = e.target.files[0]; if (!file) return; setImageFile(file); setExtractedMetadata(null); setGenerationTime(null); if (file.type === "application/pdf") { try { const arrayBuffer = await file.arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; const page = await pdf.getPage(1); const viewport = page.getViewport({ scale: 2.0 }); const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); canvas.height = viewport.height; canvas.width = viewport.width; await page.render({ canvasContext: context, viewport }).promise; const dataUrl = canvas.toDataURL("image/png"); setImagePreviewUrl(dataUrl); } catch (err) { console.error("PDF Parsing error:", err); alert("Failed to read PDF."); } } else { const reader = new FileReader(); reader.onload = (ev) => { setImagePreviewUrl(ev.target.result); }; reader.readAsDataURL(file); } } function handleExtractName() { if (!imagePreviewUrl || isGenerating || modelStatus !== "ready") return; setIsGenerating(true); setExtractedMetadata(null); setGenerationTime(null); const img = new Image(); img.onload = () => { // Crop 1: Top Quarter (25%) const canvasQuarter = document.createElement("canvas"); const ctxQuarter = canvasQuarter.getContext("2d"); canvasQuarter.width = img.width; canvasQuarter.height = img.height * 0.25; ctxQuarter.drawImage(img, 0, 0, img.width, img.height * 0.25, 0, 0, canvasQuarter.width, canvasQuarter.height); // Crop: Top Half (50%) — enough context for both header info and subject deduction const canvasHalf = document.createElement("canvas"); const ctxHalf = canvasHalf.getContext("2d"); canvasHalf.width = img.width; canvasHalf.height = img.height * 0.50; ctxHalf.drawImage(img, 0, 0, img.width, img.height * 0.50, 0, 0, canvasHalf.width, canvasHalf.height); const croppedHalf = canvasHalf.toDataURL("image/png"); if (workerRef.current) { workerRef.current.postMessage({ type: "generate", data: { imageFileOrUrlTopHalf: croppedHalf, requestId: "vlm-extract" } }); } }; img.src = imagePreviewUrl; } function handleDownloadRenamed() { if (!imageFile || !extractedMetadata) return; // Clean name: remove all spaces, punctuation, leaving only alphanumeric const cleanName = (extractedMetadata.name || "Unknown") .replace(/[^a-zA-Z0-9]/g, ""); // Clean roll number: remove any non-alphanumeric characters (like trailing periods) const cleanRoll = (extractedMetadata.rollNo || "") .replace(/[^a-zA-Z0-9]/g, ""); // Map subjects to their clean abbreviations (Maths, Bio, Phy, Chem, Eco) const rawSubject = (extractedMetadata.subject || "").trim().toLowerCase(); let subAbbr = "Sub"; if (rawSubject.includes("math")) subAbbr = "Maths"; else if (rawSubject.includes("bio")) subAbbr = "Bio"; else if (rawSubject.includes("phys")) subAbbr = "Phy"; else if (rawSubject.includes("chem")) subAbbr = "Chem"; else if (rawSubject.includes("econ")) subAbbr = "Eco"; else { // Fallback: clean the subject string and capitalize it const cleaned = rawSubject.replace(/[^a-z0-9]/gi, ''); subAbbr = cleaned ? (cleaned.charAt(0).toUpperCase() + cleaned.slice(1)) : "Sub"; } // Retain original extension (default to .pdf if missing) const extension = imageFile.name.includes('.') ? imageFile.name.split('.').pop() : 'pdf'; // Format: StudentNameROLL_sub (e.g. AadarshPatel21_Maths.pdf) const newFilename = `${cleanName}${cleanRoll}_${subAbbr}.${extension}`; // Create download link const url = URL.createObjectURL(imageFile); const a = document.createElement('a'); a.href = url; a.download = newFilename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } return (
Powered by SmolVLM-500M-Instruct running {modelStatus === "ready" && loadedDevice === "WebGPU" ? "on GPU via WebGPU" : "on CPU via WebAssembly"}
Downloading and compiling the SmolVLM-500M-Instruct model.
This will take a few minutes on the first run as it caches the ~800MB weights.
Upload an image of a document (PNG, JPG, WebP) containing a student's name.