Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>eDOCr2 - Engineering Drawing OCR</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| .header { | |
| text-align: center; | |
| color: white; | |
| margin-bottom: 40px; | |
| } | |
| .header h1 { | |
| font-size: 3em; | |
| margin-bottom: 10px; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.3); | |
| } | |
| .header p { | |
| font-size: 1.2em; | |
| opacity: 0.9; | |
| } | |
| .card { | |
| background: white; | |
| border-radius: 20px; | |
| padding: 40px; | |
| box-shadow: 0 20px 60px rgba(0,0,0,0.3); | |
| margin-bottom: 30px; | |
| } | |
| .upload-area { | |
| border: 3px dashed #667eea; | |
| border-radius: 15px; | |
| padding: 60px 20px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| background: #f8f9ff; | |
| } | |
| .upload-area:hover { | |
| border-color: #764ba2; | |
| background: #f0f2ff; | |
| transform: translateY(-2px); | |
| } | |
| .upload-area.dragover { | |
| border-color: #764ba2; | |
| background: #e8ebff; | |
| } | |
| .upload-icon { | |
| font-size: 4em; | |
| margin-bottom: 20px; | |
| } | |
| .upload-text { | |
| font-size: 1.3em; | |
| color: #667eea; | |
| margin-bottom: 10px; | |
| font-weight: 600; | |
| } | |
| .upload-hint { | |
| color: #666; | |
| font-size: 0.9em; | |
| } | |
| #fileInput { | |
| display: none; | |
| } | |
| .btn { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| padding: 15px 40px; | |
| border-radius: 30px; | |
| font-size: 1.1em; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); | |
| font-weight: 600; | |
| } | |
| .btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6); | |
| } | |
| .btn:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .processing { | |
| display: none; | |
| text-align: center; | |
| padding: 40px; | |
| } | |
| .spinner { | |
| border: 4px solid #f3f3f3; | |
| border-top: 4px solid #667eea; | |
| border-radius: 50%; | |
| width: 60px; | |
| height: 60px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 20px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .results { | |
| display: none; | |
| } | |
| .result-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 30px; | |
| padding-bottom: 20px; | |
| border-bottom: 2px solid #eee; | |
| } | |
| .result-header h2 { | |
| color: #333; | |
| font-size: 2em; | |
| } | |
| .stats { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| } | |
| .stat-card { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 25px; | |
| border-radius: 15px; | |
| text-align: center; | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); | |
| } | |
| .stat-value { | |
| font-size: 2.5em; | |
| font-weight: bold; | |
| margin-bottom: 5px; | |
| } | |
| .stat-label { | |
| font-size: 0.9em; | |
| opacity: 0.9; | |
| } | |
| .mask-image { | |
| width: 100%; | |
| border-radius: 15px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.1); | |
| margin-bottom: 30px; | |
| } | |
| .data-section { | |
| margin-top: 30px; | |
| } | |
| .data-section h3 { | |
| color: #667eea; | |
| margin-bottom: 15px; | |
| font-size: 1.5em; | |
| } | |
| .data-content { | |
| background: #f8f9ff; | |
| padding: 20px; | |
| border-radius: 10px; | |
| max-height: 400px; | |
| overflow-y: auto; | |
| font-family: 'Courier New', monospace; | |
| font-size: 0.9em; | |
| } | |
| .error { | |
| background: #fee; | |
| color: #c33; | |
| padding: 20px; | |
| border-radius: 10px; | |
| border-left: 4px solid #c33; | |
| margin-top: 20px; | |
| } | |
| .success { | |
| background: #efe; | |
| color: #3c3; | |
| padding: 20px; | |
| border-radius: 10px; | |
| border-left: 4px solid #3c3; | |
| margin-top: 20px; | |
| } | |
| .btn-group { | |
| display: flex; | |
| gap: 15px; | |
| margin-top: 20px; | |
| } | |
| .btn-secondary { | |
| background: #6c757d; | |
| } | |
| .btn-secondary:hover { | |
| background: #5a6268; | |
| } | |
| @media (max-width: 768px) { | |
| .header h1 { | |
| font-size: 2em; | |
| } | |
| .card { | |
| padding: 20px; | |
| } | |
| .stats { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🔧 eDOCr2</h1> | |
| <p>Engineering Drawing OCR - Extract dimensions, tables, and GD&T symbols</p> | |
| </div> | |
| <div class="card"> | |
| <div id="uploadSection"> | |
| <div class="upload-area" id="uploadArea"> | |
| <div class="upload-icon">📄</div> | |
| <div class="upload-text">Drop your drawing here or click to browse</div> | |
| <div class="upload-hint">Supported formats: JPG, PNG, PDF (Max 50MB)</div> | |
| </div> | |
| <input type="file" id="fileInput" accept=".jpg,.jpeg,.png,.pdf"> | |
| </div> | |
| <div class="processing" id="processingSection"> | |
| <div class="spinner"></div> | |
| <h3>Processing your drawing...</h3> | |
| <p>This may take 10-30 seconds depending on complexity</p> | |
| </div> | |
| <div class="results" id="resultsSection"> | |
| <div class="result-header"> | |
| <h2>📊 Results</h2> | |
| <div class="btn-group"> | |
| <button class="btn" onclick="downloadResults()">⬇️ Download All</button> | |
| <button class="btn btn-secondary" onclick="resetApp()">🔄 Process Another</button> | |
| </div> | |
| </div> | |
| <div class="stats" id="statsSection"></div> | |
| <h3 style="color: #667eea; margin-bottom: 15px;">🎨 Annotated Drawing</h3> | |
| <img id="maskImage" class="mask-image" alt="Processed drawing"> | |
| <div class="data-section"> | |
| <h3>📋 Extracted Data</h3> | |
| <div class="data-content" id="dataContent"></div> | |
| </div> | |
| </div> | |
| <div id="messageArea"></div> | |
| </div> | |
| </div> | |
| <script> | |
| const uploadArea = document.getElementById('uploadArea'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const uploadSection = document.getElementById('uploadSection'); | |
| const processingSection = document.getElementById('processingSection'); | |
| const resultsSection = document.getElementById('resultsSection'); | |
| const messageArea = document.getElementById('messageArea'); | |
| let currentResultFolder = null; | |
| // Click to upload | |
| uploadArea.addEventListener('click', () => fileInput.click()); | |
| // Drag and drop | |
| uploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.add('dragover'); | |
| }); | |
| uploadArea.addEventListener('dragleave', () => { | |
| uploadArea.classList.remove('dragover'); | |
| }); | |
| uploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.remove('dragover'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| handleFile(files[0]); | |
| } | |
| }); | |
| // File input change | |
| fileInput.addEventListener('change', (e) => { | |
| if (e.target.files.length > 0) { | |
| handleFile(e.target.files[0]); | |
| } | |
| }); | |
| function handleFile(file) { | |
| // Validate file | |
| const validTypes = ['image/jpeg', 'image/png', 'application/pdf']; | |
| if (!validTypes.includes(file.type)) { | |
| showMessage('Invalid file type. Please upload JPG, PNG, or PDF.', 'error'); | |
| return; | |
| } | |
| if (file.size > 50 * 1024 * 1024) { | |
| showMessage('File too large. Maximum size is 50MB.', 'error'); | |
| return; | |
| } | |
| // Upload and process | |
| uploadFile(file); | |
| } | |
| async function uploadFile(file) { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| // Show processing | |
| uploadSection.style.display = 'none'; | |
| processingSection.style.display = 'block'; | |
| messageArea.innerHTML = ''; | |
| try { | |
| const response = await fetch('/upload', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| displayResults(result); | |
| } else { | |
| showMessage(`Error: ${result.error}`, 'error'); | |
| resetApp(); | |
| } | |
| } catch (error) { | |
| showMessage(`Upload failed: ${error.message}`, 'error'); | |
| resetApp(); | |
| } | |
| } | |
| function displayResults(result) { | |
| processingSection.style.display = 'none'; | |
| resultsSection.style.display = 'block'; | |
| currentResultFolder = result.output_dir; | |
| // Display stats | |
| const statsHTML = ` | |
| <div class="stat-card"> | |
| <div class="stat-value">${result.stats.tables_found}</div> | |
| <div class="stat-label">Tables Found</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value">${result.stats.gdt_symbols}</div> | |
| <div class="stat-label">GD&T Symbols</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value">${result.stats.dimensions}</div> | |
| <div class="stat-label">Dimensions</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value">${result.processing_time}s</div> | |
| <div class="stat-label">Processing Time</div> | |
| </div> | |
| `; | |
| document.getElementById('statsSection').innerHTML = statsHTML; | |
| // Display mask image | |
| document.getElementById('maskImage').src = `/results/${result.output_dir}/${result.mask_path}`; | |
| // Display extracted data | |
| const dataHTML = `<pre>${JSON.stringify(result.data, null, 2)}</pre>`; | |
| document.getElementById('dataContent').innerHTML = dataHTML; | |
| showMessage('Processing completed successfully!', 'success'); | |
| } | |
| function downloadResults() { | |
| if (currentResultFolder) { | |
| window.location.href = `/download/${currentResultFolder}`; | |
| } | |
| } | |
| function resetApp() { | |
| uploadSection.style.display = 'block'; | |
| processingSection.style.display = 'none'; | |
| resultsSection.style.display = 'none'; | |
| messageArea.innerHTML = ''; | |
| fileInput.value = ''; | |
| currentResultFolder = null; | |
| } | |
| function showMessage(message, type) { | |
| const className = type === 'error' ? 'error' : 'success'; | |
| messageArea.innerHTML = `<div class="${className}">${message}</div>`; | |
| } | |
| // Check server health on load | |
| fetch('/health') | |
| .then(r => r.json()) | |
| .then(data => { | |
| if (!data.models_loaded) { | |
| showMessage('Warning: Models not loaded. Please check server logs.', 'error'); | |
| } | |
| }) | |
| .catch(err => { | |
| showMessage('Cannot connect to server.', 'error'); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |