Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Hybrid Multimodal Document Engine</title> | |
| <style> | |
| :root { --bg-main: #f8fafc; --panel-bg: #ffffff; --border-color: #e2e8f0; --text-dark: #1e293b; --accent-blue: #2563eb; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: var(--bg-main); color: var(--text-dark); margin: 0; padding: 20px; } | |
| .container { max-width: 1200px; margin: 0 auto; } | |
| .grid { display: grid; grid-template-columns: 1fr; gap: 20px; margin-top: 20px; } | |
| .panel { background: var(--panel-bg); border: 1px solid var(--border-color); border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); } | |
| h2 { margin-top: 0; font-size: 1.25rem; } | |
| .form-group { margin-bottom: 15px; } | |
| label { display: block; margin-bottom: 5px; font-weight: 600; } | |
| input[type="file"], select, input[type="text"], input[type="password"] { width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 4px; box-sizing: border-box; } | |
| button { background-color: var(--accent-blue); color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 600; } | |
| button:hover { opacity: 0.9; } | |
| button:disabled { background-color: #94a3b8; cursor: not-allowed; } | |
| .btn-secondary { background-color: #64748b; padding: 8px 15px; font-size: 0.85rem; margin-top: 5px; } | |
| .btn-warning { background-color: #f59e0b; color: white; } | |
| table { width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 0.95rem; } | |
| th, td { text-align: left; padding: 10px; border-bottom: 1px solid var(--border-color); } | |
| th { background-color: #f1f5f9; } | |
| .progress-container { margin-top: 20px; padding: 15px; border: 1px solid #cbd5e1; border-radius: 6px; background-color: #f8fafc; display: none; } | |
| .progress-bar-bg { width: 100%; height: 12px; background-color: #e2e8f0; border-radius: 6px; overflow: hidden; margin: 10px 0; } | |
| .progress-bar-fill { height: 100%; background-color: #22c55e; width: 0%; transition: width 0.5s linear; } | |
| .status-text { font-weight: bold; color: #334155; } | |
| .time-remaining { font-size: 0.9rem; color: #64748b; font-style: italic; } | |
| .extracted-value-highlight { color: #16a34a; font-weight: 700; } | |
| .error-highlight { color: #ef4444; font-weight: 700; } | |
| .chat-box { height: 300px; overflow-y: auto; border: 1px solid var(--border-color); padding: 10px; border-radius: 4px; margin-bottom: 10px; background: #fafafa; font-size: 0.95rem; line-height: 1.5; } | |
| .chat-input-area { display: flex; gap: 10px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Multilingual Document Translator & Data Extractor</h1> | |
| <div class="form-group"> | |
| <label> (Powered by Gemini for cloud-based AI processing. Document data is securely transmitted for analysis.)<span style="font-weight:normal; color:#64748b;"></span></label> | |
| </div> | |
| <div class="grid"> | |
| <div class="panel"> | |
| <h2>Upload Your Documents</h2> | |
| <form id="uploadForm"> | |
| <div class="form-group"> | |
| <label>Document to Process (PDF)</label> | |
| <input type="file" id="docFile" accept=".pdf" required> | |
| </div> | |
| <div class="form-group"> | |
| <label>Excel Formatting Sheet (.xlsx)</label> | |
| <input type="file" id="excelFile" accept=".xlsx" required> | |
| </div> | |
| <div class="form-group"> | |
| <label>Google Gemini API Key <span style="font-weight:normal; color:#64748b;"></span></label> | |
| <input type="password" id="apiKey" placeholder="AIzaSy..." autocomplete="off"> | |
| <button type="button" id="syncModelsBtn" class="btn-secondary">🔄 Sync Available Models</button> | |
| </div> | |
| <div class="form-group"> | |
| <label>Document Language (for OCR Scanning)</label> | |
| <select id="ocrLang"> | |
| <option value="eng+spa">English & Spanish (Default)</option> | |
| <option value="eng">English Only (Fastest)</option> | |
| <option value="eng+fra">English & French</option> | |
| <option value="eng+deu">English & German</option> | |
| <option value="eng+ita">English & Italian</option> | |
| <option value="eng+por">English & Portuguese</option> | |
| <option value="eng+ara">English & Arabic</option> | |
| <option value="eng+chi_sim">English & Simplified Chinese</option> | |
| <option value="eng+msa">English & Malay</option> | |
| <option value="eng+hin">English & Hindi</option> | |
| <option value="eng+rus">English & Russian</option> | |
| <option value="eng+jpn">English & Japanese</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label>Processing Mode</label> | |
| <select id="modelSelection"> | |
| <!-- <option value="gemini-2.5-flash">Cloud: Gemini 2.5 Flash (Uses Vision RAG)</option> | |
| <option value="qwen2.5:14b">Local: Qwen 2.5 14B (Strict Text Privacy)</option> --> | |
| </select> | |
| </div> | |
| <button type="submit" id="submitBtn">Start Processing</button> | |
| <button type="button" id="retryBtn" class="btn-warning" style="display:none; margin-top:10px;">⚠️ Rate Limit / Errors Detected. Switch Models and Retry Failed Fields</button> | |
| </form> | |
| <div class="progress-container" id="progressArea"> | |
| <div class="status-text" id="statusText">Initializing System...</div> | |
| <div class="progress-bar-bg"> | |
| <div class="progress-bar-fill" id="progressBar"></div> | |
| </div> | |
| <div class="time-remaining" id="timeRemaining">Calculating estimated completion time...</div> | |
| </div> | |
| <div id="downloadArea" style="margin-top: 15px; display: none;"> | |
| <button id="downloadBtn" style="background-color: #16a34a;">Download Clean Formatted Excel Sheet</button> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <h2>Processing Progress</h2> | |
| <div style="overflow-x: auto;"> | |
| <table id="logsTable"> | |
| <thead> | |
| <tr> | |
| <th>Spreadsheet</th> | |
| <th>Field</th> | |
| <th>Extracted Information (English)</th> | |
| <th>Found on Page(s)</th> | |
| <th>Processing Time</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr><td colspan="5" style="text-align: center; color: #64748b;">Upload your files and click "Start Processing".</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <h2>Ask About the Document</h2> | |
| <div class="chat-box" id="chatBox"> | |
| <p style="color: #64748b; font-style: italic;">Ask questions about the document or extracted information...</p> | |
| </div> | |
| <div class="chat-input-area"> | |
| <input type="text" id="chatInput" placeholder="Example: Summarize the key information in this document."> | |
| <button id="sendChatBtn">Ask AI</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let generatedFilePath = ""; | |
| let currentVisualProgress = 0; | |
| let targetVisualProgress = 0; | |
| let progressInterval = null; | |
| function startContinuousProgress() { | |
| if(progressInterval) clearInterval(progressInterval); | |
| progressInterval = setInterval(() => { | |
| if(currentVisualProgress < targetVisualProgress - 0.5) { | |
| currentVisualProgress += 0.16; | |
| document.getElementById('progressBar').style.width = currentVisualProgress + '%'; | |
| } | |
| }, 1000); | |
| } | |
| document.getElementById('syncModelsBtn').addEventListener('click', async () => { | |
| const apiKey = document.getElementById('apiKey').value.trim(); | |
| const selectEl = document.getElementById('modelSelection'); | |
| const btn = document.getElementById('syncModelsBtn'); | |
| btn.innerText = "⏳ Syncing..."; btn.disabled = true; | |
| selectEl.innerHTML = '<option>Fetching available models...</option>'; | |
| try { | |
| const response = await fetch(`/api/models?api_key=${encodeURIComponent(apiKey)}`); | |
| const data = await response.json(); | |
| selectEl.innerHTML = ''; | |
| data.models.forEach(m => { | |
| const opt = document.createElement('option'); | |
| opt.value = m.value; opt.textContent = m.label; | |
| selectEl.appendChild(opt); | |
| }); | |
| const flashOption = Array.from(selectEl.options).find(opt => opt.value === 'gemini-1.5-flash'); | |
| if (flashOption) flashOption.selected = true; | |
| } catch (err) { | |
| selectEl.innerHTML = '<option value="qwen2.5:14b">Local: Qwen 2.5 14B</option>'; | |
| } finally { | |
| btn.innerText = "🔄 Sync Available Models"; btn.disabled = false; | |
| } | |
| }); | |
| async function extractTextLocally(file, progressCallback) { | |
| const arrayBuffer = await file.arrayBuffer(); | |
| const pdf = await pdfjsLib.getDocument(arrayBuffer).promise; | |
| const numPages = pdf.numPages; | |
| let extractedChunks = []; | |
| // 1. Quick check on Page 1 to see if this is a scanned image document | |
| const firstPage = await pdf.getPage(1); | |
| const firstContent = await firstPage.getTextContent(); | |
| const isScanned = firstContent.items.map(item => item.str).join(' ').trim().length < 50; | |
| let scheduler = null; | |
| if (isScanned) { | |
| // Spin up to 4 parallel AI threads based on the user's processor cores! | |
| const selectedLang = document.getElementById('ocrLang')?.value || 'eng+spa'; | |
| const numWorkers = Math.min(4, (navigator.hardwareConcurrency || 4)); | |
| progressCallback(`Phase 1 (Local CPU): Booting ${numWorkers}-Core Parallel AI Pool...`, 2); | |
| scheduler = Tesseract.createScheduler(); | |
| for (let w = 0; w < numWorkers; w++) { | |
| const worker = await Tesseract.createWorker(selectedLang); | |
| scheduler.addWorker(worker); | |
| } | |
| } | |
| let completedPages = 0; | |
| const pageTasks = []; | |
| // 2. Queue up all pages to run across the parallel worker pool | |
| for (let i = 1; i <= numPages; i++) { | |
| pageTasks.push((async () => { | |
| const page = await pdf.getPage(i); | |
| const textContent = await page.getTextContent(); | |
| let pageText = textContent.items.map(item => item.str).join(' '); | |
| // If text is missing (< 50 chars) and scheduler is running, feed image to multi-core pool | |
| if (pageText.trim().length < 50 && scheduler) { | |
| const viewport = page.getViewport({ scale: 1.2 }); // Optimized for speed & accuracy | |
| const canvas = document.createElement('canvas'); | |
| const context = canvas.getContext('2d'); | |
| canvas.height = viewport.height; | |
| canvas.width = viewport.width; | |
| await page.render({ | |
| canvasContext: context, | |
| viewport: viewport | |
| }).promise; | |
| // Send canvas to whomever is free in the worker pool! | |
| const { | |
| data: { text } | |
| } = await scheduler.addJob('recognize', canvas); | |
| pageText = text; | |
| // Immediately free browser memory! | |
| canvas.width = 0; | |
| canvas.height = 0; | |
| } | |
| completedPages++; | |
| const prog = Math.round(2 + ((completedPages / numPages) * 18)); | |
| progressCallback( | |
| `Phase 1 (Local CPU): Scanned Page ${completedPages} of ${numPages}...`, | |
| prog | |
| ); | |
| // Layout-aware chunking matching your exact existing formatting | |
| let cleanText = pageText.replace(/\n{3,}/g, '\n\n'); | |
| let blocks = cleanText.split('\n\n'); | |
| let currentChunk = ""; | |
| let pageChunks = []; | |
| for (let block of blocks) { | |
| if (currentChunk.length + block.length < 800) { | |
| currentChunk += block + "\n\n"; | |
| } else { | |
| if (currentChunk.trim()) { | |
| pageChunks.push({ | |
| page: i, | |
| text: currentChunk.trim() | |
| }); | |
| } | |
| currentChunk = currentChunk.slice(-150) + block + "\n\n"; | |
| } | |
| } | |
| if (currentChunk.trim()) { | |
| pageChunks.push({ | |
| page: i, | |
| text: currentChunk.trim() | |
| }); | |
| } | |
| return pageChunks; | |
| })()); | |
| } | |
| // 3. Run all threads simultaneously and wait for completion | |
| const results = await Promise.all(pageTasks); | |
| results.forEach(chunks => extractedChunks.push(...chunks)); | |
| if (scheduler) { | |
| await scheduler.terminate(); | |
| } | |
| return JSON.stringify(extractedChunks); | |
| } | |
| async function executePipeline(retryFailed = false) { | |
| const submitBtn = document.getElementById('submitBtn'); | |
| const retryBtn = document.getElementById('retryBtn'); | |
| const statusText = document.getElementById('statusText'); | |
| const timeRemainingEl = document.getElementById('timeRemaining'); | |
| submitBtn.disabled = true; | |
| retryBtn.style.display = 'none'; | |
| document.getElementById('progressArea').style.display = 'block'; | |
| document.getElementById('downloadArea').style.display = 'none'; | |
| if(!retryFailed) { | |
| currentVisualProgress = 0; targetVisualProgress = 0; | |
| document.getElementById('logsTable').querySelector('tbody').innerHTML = ""; | |
| } | |
| let localExtractedJson = null; | |
| if (!retryFailed) { | |
| try { | |
| const docFile = document.getElementById('docFile').files[0]; | |
| localExtractedJson = await extractTextLocally(docFile, (msg, prog) => { | |
| statusText.innerText = msg; | |
| currentVisualProgress = prog; | |
| document.getElementById('progressBar').style.width = prog + '%'; | |
| }); | |
| } catch (err) { | |
| console.warn("Browser OCR skipped or failed, server will handle it:", err); | |
| } | |
| } | |
| startContinuousProgress(); | |
| const formData = new FormData(); | |
| formData.append('doc_file', document.getElementById('docFile').files[0]); | |
| formData.append('excel_file', document.getElementById('excelFile').files[0]); | |
| formData.append('model_selection', document.getElementById('modelSelection').value); | |
| formData.append('api_key', document.getElementById('apiKey').value); | |
| formData.append('retry_failed_only', retryFailed ? "true" : "false"); | |
| // Send the locally extracted text to the backend! | |
| if (localExtractedJson) { | |
| formData.append('local_extracted_text', localExtractedJson); | |
| } | |
| let latencyHistory = []; | |
| try { | |
| const response = await fetch('/api/process', { method: 'POST', body: formData }); | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder("utf-8"); | |
| let buffer = ""; | |
| while (true) { | |
| const { value, done } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop(); | |
| for (let line of lines) { | |
| if (!line.trim()) continue; | |
| try { | |
| const data = JSON.parse(line); | |
| if (data.status === "error") { | |
| clearInterval(progressInterval); | |
| alert("🚨 Backend Error: " + data.message); | |
| statusText.innerText = "❌ Error: " + data.message; | |
| document.getElementById('progressBar').style.backgroundColor = "#ef4444"; | |
| break; | |
| } | |
| else if (data.status === "phase") { | |
| statusText.innerText = data.message; | |
| if(data.phase === 1) targetVisualProgress = 25; | |
| else if(data.phase === 2) targetVisualProgress = 50; | |
| else if(data.phase === 3) targetVisualProgress = 75; | |
| else if(data.phase === 4) targetVisualProgress = 80; | |
| currentVisualProgress = targetVisualProgress; | |
| document.getElementById('progressBar').style.width = currentVisualProgress + '%'; | |
| } | |
| else if (data.status === "extracted") { | |
| statusText.innerText = `Extracting ${data.current} of ${data.total} fields...`; | |
| targetVisualProgress = 80 + ((data.current / data.total) * 20); | |
| currentVisualProgress = targetVisualProgress; | |
| document.getElementById('progressBar').style.width = currentVisualProgress + '%'; | |
| const isError = data.value.includes("Error") || data.value.includes("Not Found"); | |
| const valClass = isError ? "error-highlight" : "extracted-value-highlight"; | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = `<td>${data.sheet}</td><td>${data.column}</td><td class="${valClass}">${data.value}</td><td>Pg: ${data.pages.join(', ')}</td><td>${data.latency}s</td>`; | |
| document.getElementById('logsTable').querySelector('tbody').prepend(tr); | |
| latencyHistory.push(data.latency); | |
| const avgTime = latencyHistory.reduce((a, b) => a + b, 0) / latencyHistory.length; | |
| const itemsLeft = data.total - data.current; | |
| const isCloud = document.getElementById('modelSelection').value.startsWith("gemini"); | |
| const divisor = isCloud ? 8 : 1; | |
| const secLeft = Math.round(itemsLeft * avgTime / divisor); | |
| if (itemsLeft > 0) { | |
| timeRemainingEl.innerText = `Estimated Time Remaining: ~${secLeft} seconds`; | |
| } | |
| } | |
| else if (data.status === "rate_limit" || data.status === "done") { | |
| clearInterval(progressInterval); | |
| currentVisualProgress = 100; | |
| document.getElementById('progressBar').style.width = '100%'; | |
| generatedFilePath = data.file_path; | |
| document.getElementById('downloadArea').style.display = 'block'; | |
| if(data.status === "rate_limit") { | |
| statusText.innerText = "⚠️ Pipeline paused due to API Limits or Failures."; | |
| retryBtn.style.display = 'block'; | |
| } else { | |
| statusText.innerText = "✅ Extraction Successfully Completed!"; | |
| timeRemainingEl.innerText = "All parallel agents resolved."; | |
| } | |
| } | |
| } catch (e) { console.error("Parse err", line); } | |
| } | |
| } | |
| } catch (err) { | |
| alert("Network communication failure: " + err.message); | |
| clearInterval(progressInterval); | |
| } finally { | |
| submitBtn.disabled = false; | |
| } | |
| } | |
| document.getElementById('uploadForm').addEventListener('submit', (e) => { e.preventDefault(); executePipeline(false); }); | |
| document.getElementById('retryBtn').addEventListener('click', () => { executePipeline(true); }); | |
| document.getElementById('downloadBtn').addEventListener('click', () => { | |
| if(generatedFilePath) window.location.href = `/api/download?path=${encodeURIComponent(generatedFilePath)}`; | |
| }); | |
| document.getElementById('sendChatBtn').addEventListener('click', async () => { | |
| const input = document.getElementById('chatInput'); | |
| const chatBox = document.getElementById('chatBox'); | |
| const modelSelection = document.getElementById('modelSelection').value; | |
| const apiKey = document.getElementById('apiKey').value; | |
| const text = input.value.trim(); | |
| if(!text) return; | |
| chatBox.innerHTML += `<p><strong>User:</strong> ${text}</p>`; | |
| input.value = ""; | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| try { | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ query: text, model_selection: modelSelection, api_key: apiKey }) | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok) { | |
| chatBox.innerHTML += ` | |
| <div style="background-color: #fef2f2; border-left: 4px solid #ef4444; padding: 10px; margin: 10px 0; border-radius: 4px;"> | |
| <p style="color: #b91c1c; margin: 0;"><strong>⚠️ System Alert:</strong> ${data.detail}</p> | |
| </div>`; | |
| } else { | |
| let sourceInfo = data.citations_pages.length > 0 ? `<br><small style="color: #2563eb; font-size: 0.85rem;">[Sources Traced on Pages: ${data.citations_pages.join(', ')}]</small>` : ''; | |
| chatBox.innerHTML += `<p><strong>Agent (${modelSelection}):</strong> ${data.response} ${sourceInfo}</p>`; | |
| } | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| } catch (err) { | |
| chatBox.innerHTML += `<p style="color: red;">Failed to communicate with API.</p>`; | |
| } | |
| }); | |
| </script> | |
| <!-- Browser-Side PDF and OCR Engines --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"></script> | |
| <script> | |
| pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; | |
| </script> | |
| </body> | |
| </html> |