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> |