| from fastapi import FastAPI, UploadFile, File, Form |
| from fastapi.responses import HTMLResponse |
| import pandas as pd |
| import io |
| import re |
| import os |
| from google import genai |
| from PIL import Image |
|
|
| app = FastAPI() |
|
|
| |
| @app.get("/") |
| def main_page(): |
| return HTMLResponse(""" |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <title>PLC Reader</title> |
| <style> |
| body { font-family: system-ui, sans-serif; padding: 20px; background-color: #f8fafc; color: #0f172a; text-align: center; margin: 0;} |
| .container { max-width: 600px; margin: auto; background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); } |
| input[type="password"] { width: 100%; padding: 12px; margin-bottom: 20px; border: 1px solid #cbd5e1; border-radius: 6px; box-sizing: border-box; font-size: 16px;} |
| input[type="file"] { display: none; } |
| .camera-btn { display: block; background-color: #4338ca; color: white; padding: 15px; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; border: none; width: 100%; box-sizing: border-box; margin-bottom: 20px;} |
| |
| /* New Styles for Image Preview and Loading Spinner */ |
| #preview-container { display: none; margin-bottom: 20px; } |
| #preview-img { max-width: 100%; border-radius: 8px; border: 2px solid #e2e8f0; } |
| .loader { display: none; font-weight: bold; color: #4338ca; margin: 20px 0; font-size: 1.1em;} |
| .spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid rgba(67,56,202,.3); border-radius: 50%; border-top-color: #4338ca; animation: spin 1s ease-in-out infinite; vertical-align: middle; margin-right: 8px;} |
| @keyframes spin { to { transform: rotate(360deg); } } |
| |
| /* Table Styles injected by backend */ |
| .table-container { overflow-x: auto; margin-top: 15px; text-align: left; } |
| table { border-collapse: collapse; width: 100%; background: white; font-size: 14px;} |
| th, td { border: 1px solid #e2e8f0; padding: 8px; white-space: nowrap; } |
| th { background-color: #f1f5f9; } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <h2 style="margin-top:0; color: #1e293b;">🏭 PLC Data READER</h2> |
| <p style="color: #64748b; margin-bottom: 25px;">Capture sinter plant log sheets to digitize tabular data.</p> |
| |
| <input type="password" id="api-key" placeholder="You are ready to go......"> |
| |
| <label class="camera-btn" id="camera-label"> |
| <span id="btn-text">📸 Tap to Open Rear Camera</span> |
| <input type="file" id="file-input" accept="image/*" capture="environment"> |
| </label> |
| |
| <div id="preview-container"> |
| <h4 style="margin-bottom: 10px; color: #475569; text-align: left;">Captured Image:</h4> |
| <img id="preview-img" alt="Captured your image"> |
| </div> |
| |
| <div id="loading" class="loader"> |
| <span class="spinner"></span> Analyzing your image... this takes a few seconds. |
| </div> |
| |
| <div id="results"></div> |
| </div> |
| |
| <script> |
| // This JavaScript handles the background processing so the page doesn't refresh |
| document.getElementById('file-input').addEventListener('change', function(e) { |
| const file = e.target.files[0]; |
| if (!file) return; |
| |
| // 1. Instantly display the image on the screen |
| const reader = new FileReader(); |
| reader.onload = function(event) { |
| document.getElementById('preview-img').src = event.target.result; |
| document.getElementById('preview-container').style.display = 'block'; |
| }; |
| reader.readAsDataURL(file); |
| |
| // 2. Hide the camera button and show the loading spinner |
| document.getElementById('camera-label').style.display = 'none'; |
| document.getElementById('results').innerHTML = ''; // Clear old tables |
| document.getElementById('loading').style.display = 'block'; |
| |
| // 3. Send the image to the Python backend in the background |
| const formData = new FormData(); |
| formData.append('file', file); |
| formData.append('api_key_input', document.getElementById('api-key').value); |
| |
| fetch('/extract', { |
| method: 'POST', |
| body: formData |
| }) |
| .then(response => response.text()) |
| .then(html => { |
| // 4. Extraction complete! Hide loading, show tables, restore button |
| document.getElementById('loading').style.display = 'none'; |
| document.getElementById('results').innerHTML = html; |
| |
| const camLabel = document.getElementById('camera-label'); |
| camLabel.style.display = 'block'; |
| document.getElementById('btn-text').innerText = '📸 Take Another Picture'; |
| }) |
| .catch(error => { |
| document.getElementById('loading').style.display = 'none'; |
| document.getElementById('results').innerHTML = '<div style="color:#ef4444; padding:15px;"><b>❌ Connection Error</b><br>' + error + '</div>'; |
| document.getElementById('camera-label').style.display = 'block'; |
| }); |
| }); |
| </script> |
| </body> |
| </html> |
| """) |
|
|
| |
| @app.post("/extract") |
| async def extract_data(api_key_input: str = Form(""), file: UploadFile = File(...)): |
| try: |
| api_key = os.environ.get("GEMINI_API_KEY") or api_key_input |
| if not api_key: |
| return HTMLResponse("<div style='padding:15px; background:#fef2f2; color:#991b1b; border-radius:8px;'><b>⚠️ Missing API Key.</b> Please enter it above.</div>") |
|
|
| image_bytes = await file.read() |
| img = Image.open(io.BytesIO(image_bytes)) |
| |
| client = genai.Client(api_key=api_key) |
| prompt = "Carefully extract all the data tables from this dashboard image. Format the output strictly as CSV. Enclose the CSV data inside ```csv ``` code blocks. Ensure column headers are included." |
| |
| response = client.models.generate_content( |
| model="gemini-2.5-flash", |
| contents=[prompt, img] |
| ) |
| |
| csv_blocks = re.findall(r'```csv\n(.*?)\n```', response.text, re.DOTALL) |
| |
| if not csv_blocks: |
| return HTMLResponse(f"<div style='padding:15px; background:#fffbeb; color:#92400e; border-radius:8px;'><b>⚠️ No tables found.</b> The model couldn't detect clear tables.<br><br>Raw Output:<br><pre style='white-space:pre-wrap; font-size:12px;'>{response.text}</pre></div>") |
| |
| |
| html_output = f"<h3 style='color:#16a34a; margin-top:30px; border-top: 2px dashed #cbd5e1; padding-top:20px; text-align: left;'>✅ Extracted {len(csv_blocks)} Table(s)</h3>" |
| |
| for i, csv_data in enumerate(csv_blocks): |
| df = pd.read_csv(io.StringIO(csv_data)) |
| html_output += f"<h4 style='color:#334155; margin-bottom:5px; margin-top: 20px; text-align:left;'>Table {i+1}</h4>" |
| html_output += f"<div class='table-container'>{df.to_html(index=False, border=0)}</div>" |
| |
| return HTMLResponse(html_output) |
| |
| except Exception as e: |
| return HTMLResponse(f"<div style='padding:15px; background:#fef2f2; color:#991b1b; border-radius:8px;'><b>❌ Error Processing Image:</b><br>{str(e)}</div>") |