| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Certificate Verifier - with Annotated Images</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: 30px; |
| } |
| |
| .header h1 { |
| font-size: 2.5em; |
| margin-bottom: 10px; |
| } |
| |
| .card { |
| background: white; |
| border-radius: 15px; |
| padding: 30px; |
| box-shadow: 0 10px 30px rgba(0,0,0,0.2); |
| margin-bottom: 20px; |
| } |
| |
| .upload-area { |
| border: 3px dashed #667eea; |
| border-radius: 10px; |
| padding: 40px; |
| text-align: center; |
| cursor: pointer; |
| transition: all 0.3s; |
| } |
| |
| .upload-area:hover { |
| border-color: #764ba2; |
| background: #f8f9ff; |
| } |
| |
| .upload-area.dragover { |
| background: #e8ebff; |
| border-color: #764ba2; |
| } |
| |
| .btn { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: white; |
| border: none; |
| padding: 12px 30px; |
| border-radius: 25px; |
| font-size: 16px; |
| cursor: pointer; |
| transition: transform 0.2s; |
| margin: 10px 5px; |
| } |
| |
| .btn:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 5px 15px rgba(0,0,0,0.2); |
| } |
| |
| .btn:disabled { |
| opacity: 0.5; |
| cursor: not-allowed; |
| } |
| |
| .checkbox-group { |
| margin: 20px 0; |
| padding: 15px; |
| background: #f8f9ff; |
| border-radius: 10px; |
| } |
| |
| .checkbox-group label { |
| display: flex; |
| align-items: center; |
| font-size: 16px; |
| cursor: pointer; |
| margin: 10px 0; |
| } |
| |
| .checkbox-group input[type="checkbox"] { |
| width: 20px; |
| height: 20px; |
| margin-right: 10px; |
| cursor: pointer; |
| } |
| |
| .loading { |
| display: none; |
| text-align: center; |
| padding: 20px; |
| } |
| |
| .spinner { |
| border: 4px solid #f3f3f3; |
| border-top: 4px solid #667eea; |
| border-radius: 50%; |
| width: 50px; |
| height: 50px; |
| animation: spin 1s linear infinite; |
| margin: 0 auto; |
| } |
| |
| @keyframes spin { |
| 0% { transform: rotate(0deg); } |
| 100% { transform: rotate(360deg); } |
| } |
| |
| .result { |
| display: none; |
| margin-top: 20px; |
| } |
| |
| .result-authentic { |
| background: #d4edda; |
| border-left: 5px solid #28a745; |
| padding: 20px; |
| border-radius: 5px; |
| } |
| |
| .result-fake { |
| background: #f8d7da; |
| border-left: 5px solid #dc3545; |
| padding: 20px; |
| border-radius: 5px; |
| } |
| |
| .result-suspicious { |
| background: #fff3cd; |
| border-left: 5px solid #ffc107; |
| padding: 20px; |
| border-radius: 5px; |
| } |
| |
| .image-container { |
| margin-top: 20px; |
| } |
| |
| .image-comparison { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 20px; |
| margin-top: 20px; |
| } |
| |
| .image-box { |
| text-align: center; |
| } |
| |
| .image-box h3 { |
| margin-bottom: 10px; |
| color: #333; |
| } |
| |
| .image-box img { |
| max-width: 100%; |
| border-radius: 10px; |
| box-shadow: 0 5px 15px rgba(0,0,0,0.1); |
| } |
| |
| .details { |
| margin-top: 20px; |
| padding: 15px; |
| background: #f8f9fa; |
| border-radius: 5px; |
| } |
| |
| .detail-row { |
| display: flex; |
| justify-content: space-between; |
| padding: 8px 0; |
| border-bottom: 1px solid #dee2e6; |
| } |
| |
| .detail-row:last-child { |
| border-bottom: none; |
| } |
| |
| .legend { |
| background: #f8f9ff; |
| padding: 15px; |
| border-radius: 10px; |
| margin-top: 20px; |
| } |
| |
| .legend h4 { |
| margin-bottom: 10px; |
| color: #667eea; |
| } |
| |
| .legend-item { |
| display: flex; |
| align-items: center; |
| margin: 8px 0; |
| } |
| |
| .legend-color { |
| width: 30px; |
| height: 20px; |
| border-radius: 3px; |
| margin-right: 10px; |
| border: 2px solid #333; |
| } |
| |
| @media (max-width: 768px) { |
| .image-comparison { |
| grid-template-columns: 1fr; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="header"> |
| <h1>π Certificate Verifier</h1> |
| <p>AI-Powered Certificate Authentication with Visual Seal Detection</p> |
| </div> |
| |
| <div class="card"> |
| <div class="upload-area" id="uploadArea"> |
| <h2>π Upload Certificate</h2> |
| <p>Click to select or drag & drop your certificate image</p> |
| <input type="file" id="fileInput" accept="image/*" style="display: none;"> |
| </div> |
| |
| <div class="checkbox-group"> |
| <label> |
| <input type="checkbox" id="returnImageCheck" checked> |
| <strong>Show Annotated Image</strong> - Display certificate with colored boxes around seals (π’ Authentic / π΄ Fake) |
| </label> |
| <label> |
| <input type="checkbox" id="sealVerificationCheck" checked> |
| <strong>Enable Seal Verification</strong> - Use AI to detect and verify seals |
| </label> |
| </div> |
| |
| <div style="text-align: center;"> |
| <button class="btn" id="verifyBtn" disabled>π Verify Certificate</button> |
| <button class="btn" id="clearBtn" style="background: #6c757d;">ποΈ Clear</button> |
| </div> |
| |
| <div class="loading" id="loading"> |
| <div class="spinner"></div> |
| <p style="margin-top: 15px;">Analyzing certificate...</p> |
| </div> |
| |
| <div class="result" id="result"></div> |
| </div> |
| |
| <div class="legend"> |
| <h4>π How to Read the Annotated Image:</h4> |
| <div class="legend-item"> |
| <div class="legend-color" style="background-color: rgb(0, 255, 0);"></div> |
| <span><strong>Green Box:</strong> Authentic seal detected with high confidence</span> |
| </div> |
| <div class="legend-item"> |
| <div class="legend-color" style="background-color: rgb(255, 0, 0);"></div> |
| <span><strong>Red Box:</strong> Fake seal detected - certificate may be fraudulent</span> |
| </div> |
| <div class="legend-item"> |
| <div class="legend-color" style="background-color: rgb(255, 255, 0);"></div> |
| <span><strong>Yellow Box:</strong> Suspicious seal - requires manual review</span> |
| </div> |
| </div> |
| </div> |
| |
| <script> |
| const uploadArea = document.getElementById('uploadArea'); |
| const fileInput = document.getElementById('fileInput'); |
| const verifyBtn = document.getElementById('verifyBtn'); |
| const clearBtn = document.getElementById('clearBtn'); |
| const loading = document.getElementById('loading'); |
| const result = document.getElementById('result'); |
| const returnImageCheck = document.getElementById('returnImageCheck'); |
| const sealVerificationCheck = document.getElementById('sealVerificationCheck'); |
| |
| let selectedFile = null; |
| let originalImageUrl = null; |
| |
| |
| uploadArea.addEventListener('click', () => fileInput.click()); |
| |
| |
| fileInput.addEventListener('change', (e) => { |
| if (e.target.files.length > 0) { |
| selectedFile = e.target.files[0]; |
| originalImageUrl = URL.createObjectURL(selectedFile); |
| uploadArea.innerHTML = `<p>β
Selected: ${selectedFile.name}</p>`; |
| verifyBtn.disabled = false; |
| } |
| }); |
| |
| |
| 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'); |
| |
| if (e.dataTransfer.files.length > 0) { |
| selectedFile = e.dataTransfer.files[0]; |
| originalImageUrl = URL.createObjectURL(selectedFile); |
| uploadArea.innerHTML = `<p>β
Selected: ${selectedFile.name}</p>`; |
| verifyBtn.disabled = false; |
| } |
| }); |
| |
| |
| clearBtn.addEventListener('click', () => { |
| selectedFile = null; |
| originalImageUrl = null; |
| fileInput.value = ''; |
| uploadArea.innerHTML = ` |
| <h2>π Upload Certificate</h2> |
| <p>Click to select or drag & drop your certificate image</p> |
| `; |
| result.style.display = 'none'; |
| verifyBtn.disabled = true; |
| }); |
| |
| |
| verifyBtn.addEventListener('click', async () => { |
| if (!selectedFile) return; |
| |
| |
| loading.style.display = 'block'; |
| result.style.display = 'none'; |
| verifyBtn.disabled = true; |
| |
| |
| const formData = new FormData(); |
| formData.append('files', selectedFile); |
| |
| |
| const returnImage = returnImageCheck.checked; |
| const sealVerification = sealVerificationCheck.checked; |
| const url = `http://localhost:8000/api/verify?return_image=${returnImage}&enable_seal_verification=${sealVerification}`; |
| |
| try { |
| const response = await fetch(url, { |
| method: 'POST', |
| body: formData |
| }); |
| |
| const data = await response.json(); |
| |
| |
| loading.style.display = 'none'; |
| |
| |
| displayResults(data); |
| |
| } catch (error) { |
| loading.style.display = 'none'; |
| result.innerHTML = ` |
| <div class="result-fake"> |
| <h3>β Error</h3> |
| <p>${error.message}</p> |
| </div> |
| `; |
| result.style.display = 'block'; |
| } |
| |
| verifyBtn.disabled = false; |
| }); |
| |
| function displayResults(data) { |
| const decision = data.decision; |
| let resultClass = 'result-suspicious'; |
| let icon = 'β οΈ'; |
| |
| if (decision === 'AUTHENTIC') { |
| resultClass = 'result-authentic'; |
| icon = 'β
'; |
| } else if (decision === 'FAKE') { |
| resultClass = 'result-fake'; |
| icon = 'β'; |
| } |
| |
| let html = ` |
| <div class="${resultClass}"> |
| <h2>${icon} ${decision}</h2> |
| <p><strong>Confidence:</strong> ${(data.confidence * 100).toFixed(1)}%</p> |
| <p><strong>Reason:</strong> ${data.reason}</p> |
| <p><strong>Processing Time:</strong> ${data.processing_time_seconds}s</p> |
| </div> |
| `; |
| |
| |
| if (data.annotated_image_url) { |
| html += ` |
| <div class="image-comparison"> |
| <div class="image-box"> |
| <h3>Original Certificate</h3> |
| <img src="${originalImageUrl}" alt="Original"> |
| </div> |
| <div class="image-box"> |
| <h3>π¨ Annotated (Seal Detection)</h3> |
| <img src="${data.annotated_image_url}" alt="Annotated"> |
| </div> |
| </div> |
| `; |
| } else if (originalImageUrl) { |
| html += ` |
| <div class="image-container"> |
| <h3>Certificate Image</h3> |
| <img src="${originalImageUrl}" alt="Certificate" style="max-width: 100%; border-radius: 10px;"> |
| </div> |
| `; |
| } |
| |
| |
| const sealInfo = data.details?.seal_verification; |
| if (sealInfo && sealInfo.total_seals > 0) { |
| html += ` |
| <div class="details"> |
| <h3>π Seal Detection Details</h3> |
| <div class="detail-row"> |
| <span>Total Seals:</span> |
| <strong>${sealInfo.total_seals}</strong> |
| </div> |
| <div class="detail-row"> |
| <span>β
Authentic:</span> |
| <strong>${sealInfo.authentic_seals}</strong> |
| </div> |
| <div class="detail-row"> |
| <span>β Fake:</span> |
| <strong>${sealInfo.fake_seals}</strong> |
| </div> |
| <div class="detail-row"> |
| <span>Detection Method:</span> |
| <strong>${sealInfo.detection_method}</strong> |
| </div> |
| </div> |
| `; |
| } |
| |
| |
| if (data.cropped_seals && data.cropped_seals.length > 0) { |
| html += ` |
| <div style="margin-top: 20px;"> |
| <h3>πΈ Cropped Seals</h3> |
| <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; margin-top: 15px;"> |
| `; |
| |
| data.cropped_seals.forEach(seal => { |
| const statusColor = seal.class === 'true' ? '#28a745' : seal.class === 'fake' ? '#dc3545' : '#ffc107'; |
| const statusIcon = seal.class === 'true' ? 'β
' : seal.class === 'fake' ? 'β' : 'β οΈ'; |
| const statusText = seal.class === 'true' ? 'Authentic' : seal.class === 'fake' ? 'Fake' : 'Suspicious'; |
| |
| html += ` |
| <div style="border: 3px solid ${statusColor}; border-radius: 10px; padding: 10px; text-align: center; background: white;"> |
| <img src="${seal.image_url}" alt="Seal ${seal.seal_number}" style="max-width: 100%; border-radius: 5px;"> |
| <div style="margin-top: 8px; font-weight: bold; color: ${statusColor};"> |
| ${statusIcon} Seal ${seal.seal_number} |
| </div> |
| <div style="font-size: 12px; color: #666;"> |
| ${statusText}<br> |
| ${(seal.confidence * 100).toFixed(1)}% |
| </div> |
| </div> |
| `; |
| }); |
| |
| html += ` |
| </div> |
| </div> |
| `; |
| } |
| |
| result.innerHTML = html; |
| result.style.display = 'block'; |
| } |
| </script> |
| </body> |
| </html> |
|
|