Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Screen Analysis & OCR Tool</title> | |
| <!-- Include Tesseract.js for OCR --> | |
| <script src="https://unpkg.com/tesseract.js@v2.1.0/dist/tesseract.min.js"></script> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: Arial, sans-serif; | |
| } | |
| body { | |
| background: #f0f2f5; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| .header { | |
| background: white; | |
| padding: 20px; | |
| border-radius: 8px; | |
| margin-bottom: 20px; | |
| text-align: center; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .controls { | |
| background: white; | |
| padding: 20px; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| margin-bottom: 20px; | |
| display: flex; | |
| gap: 10px; | |
| flex-wrap: wrap; | |
| } | |
| button { | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-weight: bold; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| min-width: 120px; | |
| justify-content: center; | |
| } | |
| .start-btn { | |
| background: #4CAF50; | |
| color: white; | |
| } | |
| .stop-btn { | |
| background: #f44336; | |
| color: white; | |
| } | |
| .clear-btn { | |
| background: #2196F3; | |
| color: white; | |
| } | |
| button:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| } | |
| .preview-area { | |
| background: black; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| margin-bottom: 20px; | |
| position: relative; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| #preview { | |
| width: 100%; | |
| aspect-ratio: 16/9; | |
| object-fit: contain; | |
| } | |
| .recording-dot { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| width: 12px; | |
| height: 12px; | |
| background: red; | |
| border-radius: 50%; | |
| animation: pulse 1s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| 100% { opacity: 1; } | |
| } | |
| .logs { | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| overflow: hidden; | |
| } | |
| .log-entry { | |
| padding: 20px; | |
| border-bottom: 1px solid #eee; | |
| display: grid; | |
| grid-template-columns: 300px 1fr; | |
| gap: 20px; | |
| } | |
| .screenshot-container { | |
| position: relative; | |
| } | |
| .screenshot { | |
| width: 100%; | |
| border-radius: 4px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .change-highlight { | |
| position: absolute; | |
| border: 2px solid red; | |
| background: rgba(255,0,0,0.2); | |
| pointer-events: none; | |
| } | |
| .info-panel { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| } | |
| .timestamp { | |
| color: #666; | |
| font-size: 14px; | |
| } | |
| .text-content { | |
| background: #f8f9fa; | |
| padding: 15px; | |
| border-radius: 4px; | |
| font-size: 14px; | |
| line-height: 1.5; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| } | |
| .actions { | |
| display: flex; | |
| gap: 10px; | |
| margin-top: 10px; | |
| } | |
| .download-btn { | |
| background: #4CAF50; | |
| color: white; | |
| padding: 8px 16px; | |
| border-radius: 4px; | |
| text-decoration: none; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| font-size: 14px; | |
| } | |
| .processing { | |
| color: #666; | |
| font-style: italic; | |
| } | |
| @media (max-width: 768px) { | |
| .log-entry { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>Screen Analysis & OCR Tool</h1> | |
| <p>Capture, analyze, and extract text from your screen</p> | |
| </div> | |
| <div class="controls"> | |
| <button id="startBtn" class="start-btn">▶ Start Capture</button> | |
| <button id="stopBtn" class="stop-btn" disabled>⬛ Stop</button> | |
| <button id="clearBtn" class="clear-btn">🗑 Clear Logs</button> | |
| </div> | |
| <div class="preview-area"> | |
| <video id="preview" autoplay></video> | |
| <div class="recording-dot" style="display: none;"></div> | |
| </div> | |
| <div class="logs" id="logContainer"></div> | |
| </div> | |
| <script> | |
| let mediaStream = null; | |
| let captureInterval = null; | |
| let lastImageData = null; | |
| // Initialize Tesseract | |
| const worker = Tesseract.createWorker(); | |
| (async () => { | |
| await worker.load(); | |
| await worker.loadLanguage('eng'); | |
| await worker.initialize('eng'); | |
| })(); | |
| const startBtn = document.getElementById('startBtn'); | |
| const stopBtn = document.getElementById('stopBtn'); | |
| const clearBtn = document.getElementById('clearBtn'); | |
| const preview = document.getElementById('preview'); | |
| const logContainer = document.getElementById('logContainer'); | |
| const recordingDot = document.querySelector('.recording-dot'); | |
| async function startCapture() { | |
| try { | |
| mediaStream = await navigator.mediaDevices.getDisplayMedia({ | |
| video: { cursor: "always" } | |
| }); | |
| preview.srcObject = mediaStream; | |
| startBtn.disabled = true; | |
| stopBtn.disabled = false; | |
| recordingDot.style.display = 'block'; | |
| captureInterval = setInterval(captureAndAnalyze, 1000); | |
| mediaStream.getVideoTracks()[0].onended = stopCapture; | |
| } catch (err) { | |
| console.error("Error starting capture:", err); | |
| } | |
| } | |
| function stopCapture() { | |
| if (mediaStream) { | |
| mediaStream.getTracks().forEach(track => track.stop()); | |
| preview.srcObject = null; | |
| } | |
| clearInterval(captureInterval); | |
| startBtn.disabled = false; | |
| stopBtn.disabled = true; | |
| recordingDot.style.display = 'none'; | |
| lastImageData = null; | |
| } | |
| function detectChanges(current, last) { | |
| const blockSize = 20; | |
| const threshold = 30; | |
| const changes = []; | |
| const width = current.width; | |
| const height = current.height; | |
| for (let y = 0; y < height; y += blockSize) { | |
| for (let x = 0; x < width; x += blockSize) { | |
| let diffCount = 0; | |
| const maxY = Math.min(y + blockSize, height); | |
| const maxX = Math.min(x + blockSize, width); | |
| for (let py = y; py < maxY; py++) { | |
| for (let px = x; px < maxX; px++) { | |
| const i = (py * width + px) * 4; | |
| if (Math.abs(current.data[i] - last.data[i]) > threshold || | |
| Math.abs(current.data[i + 1] - last.data[i + 1]) > threshold || | |
| Math.abs(current.data[i + 2] - last.data[i + 2]) > threshold) { | |
| diffCount++; | |
| } | |
| } | |
| } | |
| if (diffCount > (blockSize * blockSize * 0.3)) { | |
| changes.push({ | |
| x: x / width * 100, | |
| y: y / height * 100, | |
| width: Math.min(blockSize / width * 100, 100 - x / width * 100), | |
| height: Math.min(blockSize / height * 100, 100 - y / height * 100) | |
| }); | |
| } | |
| } | |
| } | |
| return changes; | |
| } | |
| async function captureAndAnalyze() { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = preview.videoWidth; | |
| canvas.height = preview.videoHeight; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(preview, 0, 0); | |
| const currentImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| let changes = []; | |
| if (lastImageData) { | |
| changes = detectChanges(currentImageData, lastImageData); | |
| } | |
| lastImageData = currentImageData; | |
| if (changes.length > 0 || !lastImageData) { | |
| const imageUrl = canvas.toDataURL('image/jpeg', 0.8); | |
| await addLogEntry(imageUrl, changes); | |
| } | |
| } | |
| async function performOCR(imageUrl) { | |
| try { | |
| const result = await worker.recognize(imageUrl); | |
| return result.data.text; | |
| } catch (error) { | |
| console.error('OCR Error:', error); | |
| return 'Error performing OCR'; | |
| } | |
| } | |
| async function addLogEntry(imageUrl, changes) { | |
| const logEntry = document.createElement('div'); | |
| logEntry.className = 'log-entry'; | |
| const timestamp = new Date().toLocaleString(); | |
| // Create initial structure with loading state | |
| logEntry.innerHTML = ` | |
| <div class="screenshot-container"> | |
| <img class="screenshot" src="${imageUrl}" alt="Screenshot"> | |
| ${changes.map(change => ` | |
| <div class="change-highlight" style=" | |
| left: ${change.x}%; | |
| top: ${change.y}%; | |
| width: ${change.width}%; | |
| height: ${change.height}%; | |
| "></div> | |
| `).join('')} | |
| </div> | |
| <div class="info-panel"> | |
| <div class="timestamp">${timestamp}</div> | |
| <div class="text-content processing">Processing text extraction...</div> | |
| <div class="actions"> | |
| <a href="${imageUrl}" download="screenshot-${Date.now()}.jpg" class="download-btn"> | |
| 💾 Download Image | |
| </a> | |
| </div> | |
| </div> | |
| `; | |
| logContainer.insertBefore(logEntry, logContainer.firstChild); | |
| // Perform OCR | |
| const extractedText = await performOCR(imageUrl); | |
| // Update text content | |
| const textContent = logEntry.querySelector('.text-content'); | |
| textContent.classList.remove('processing'); | |
| textContent.textContent = extractedText || 'No text detected'; | |
| } | |
| clearBtn.addEventListener('click', () => { | |
| logContainer.innerHTML = ''; | |
| }); | |
| startBtn.addEventListener('click', startCapture); | |
| stopBtn.addEventListener('click', stopCapture); | |
| // Cleanup when page is closed | |
| window.addEventListener('beforeunload', () => { | |
| worker.terminate(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |