Spaces:
Sleeping
Sleeping
Download static/camera.html from cooldan/SPaCial_server_api: direct link, hf CLI and curl.
- Browser
- Download file 12 kB
-
https://huggingface.co/spaces/cooldan/SPaCial_server_api/resolve/main/static/camera.html
- Command line
-
hf download hf://spaces/cooldan/SPaCial_server_api/static/camera.html
-
curl -L -o camera.html https://huggingface.co/spaces/cooldan/SPaCial_server_api/resolve/main/static/camera.html
12 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>SPaCial AI - Live Scanner</title> | |
| <script src="https://telegram.org/js/telegram-web-app.js"></script> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; | |
| background: #000; | |
| overflow: hidden; | |
| position: fixed; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| #container { | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| #videoContainer { | |
| position: relative; | |
| flex: 1; | |
| overflow: hidden; | |
| background: #000; | |
| } | |
| #video { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| #canvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| } | |
| #overlay { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| right: 20px; | |
| background: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| padding: 15px; | |
| border-radius: 10px; | |
| font-size: 14px; | |
| z-index: 10; | |
| } | |
| #status { | |
| font-weight: bold; | |
| margin-bottom: 5px; | |
| } | |
| .zone-info { | |
| font-size: 12px; | |
| margin: 5px 0; | |
| color: #0f0; | |
| } | |
| #controls { | |
| background: rgba(0, 0, 0, 0.9); | |
| padding: 20px; | |
| display: flex; | |
| justify-content: space-around; | |
| align-items: center; | |
| } | |
| button { | |
| background: #0088cc; | |
| color: white; | |
| border: none; | |
| padding: 15px 30px; | |
| border-radius: 25px; | |
| font-size: 16px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| min-width: 120px; | |
| } | |
| button:active { | |
| transform: scale(0.95); | |
| } | |
| button:disabled { | |
| background: #666; | |
| cursor: not-allowed; | |
| } | |
| #captureBtn { | |
| background: #28a745; | |
| font-size: 18px; | |
| padding: 20px 40px; | |
| } | |
| #toggleScanBtn { | |
| background: #ffc107; | |
| } | |
| #toggleScanBtn.scanning { | |
| background: #dc3545; | |
| } | |
| .error { | |
| color: #ff4444; | |
| background: rgba(255, 68, 68, 0.2); | |
| padding: 10px; | |
| border-radius: 5px; | |
| margin-top: 10px; | |
| } | |
| .loading { | |
| display: inline-block; | |
| width: 12px; | |
| height: 12px; | |
| border: 2px solid #fff; | |
| border-radius: 50%; | |
| border-top-color: transparent; | |
| animation: spin 0.6s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| #zoneCount { | |
| font-size: 24px; | |
| color: #0f0; | |
| font-weight: bold; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="container"> | |
| <div id="videoContainer"> | |
| <video id="video" autoplay playsinline></video> | |
| <canvas id="canvas"></canvas> | |
| <div id="overlay"> | |
| <div id="status">πΈ Camera Ready</div> | |
| <div id="zoneCount">0 zones detected</div> | |
| <div id="details"></div> | |
| </div> | |
| </div> | |
| <div id="controls"> | |
| <button id="toggleScanBtn" onclick="toggleScanning()">Start Scan</button> | |
| <button id="captureBtn" onclick="captureResults()" disabled>Capture</button> | |
| </div> | |
| </div> | |
| <script> | |
| const tg = window.Telegram.WebApp; | |
| tg.ready(); | |
| tg.expand(); | |
| const video = document.getElementById('video'); | |
| const canvas = document.getElementById('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const statusEl = document.getElementById('status'); | |
| const zoneCountEl = document.getElementById('zoneCount'); | |
| const detailsEl = document.getElementById('details'); | |
| const toggleBtn = document.getElementById('toggleScanBtn'); | |
| const captureBtn = document.getElementById('captureBtn'); | |
| let isScanning = false; | |
| let scanInterval = null; | |
| let lastResults = null; | |
| let stream = null; | |
| // Get API URL (same host as mini app) | |
| const API_URL = window.location.origin; | |
| // Initialize camera | |
| async function initCamera() { | |
| try { | |
| stream = await navigator.mediaDevices.getUserMedia({ | |
| video: { | |
| facingMode: 'environment', | |
| width: { ideal: 1920 }, | |
| height: { ideal: 1080 } | |
| } | |
| }); | |
| video.srcObject = stream; | |
| video.addEventListener('loadedmetadata', () => { | |
| canvas.width = video.videoWidth; | |
| canvas.height = video.videoHeight; | |
| statusEl.textContent = 'β Camera Ready'; | |
| }); | |
| } catch (error) { | |
| statusEl.innerHTML = `β Camera Error: ${error.message}`; | |
| console.error('Camera error:', error); | |
| } | |
| } | |
| // Toggle scanning | |
| function toggleScanning() { | |
| if (isScanning) { | |
| stopScanning(); | |
| } else { | |
| startScanning(); | |
| } | |
| } | |
| // Start continuous scanning | |
| function startScanning() { | |
| isScanning = true; | |
| toggleBtn.textContent = 'Stop Scan'; | |
| toggleBtn.classList.add('scanning'); | |
| captureBtn.disabled = true; | |
| // Scan every 2 seconds | |
| scanInterval = setInterval(scanFrame, 2000); | |
| scanFrame(); // First scan immediately | |
| } | |
| // Stop scanning | |
| function stopScanning() { | |
| isScanning = false; | |
| toggleBtn.textContent = 'Start Scan'; | |
| toggleBtn.classList.remove('scanning'); | |
| captureBtn.disabled = false; | |
| if (scanInterval) { | |
| clearInterval(scanInterval); | |
| scanInterval = null; | |
| } | |
| } | |
| // Scan current frame | |
| async function scanFrame() { | |
| try { | |
| statusEl.innerHTML = 'β³ Processing... <span class="loading"></span>'; | |
| // Draw video frame to canvas | |
| ctx.drawImage(video, 0, 0, canvas.width, canvas.height); | |
| // Get image as blob | |
| const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.95)); | |
| // Send to OCR API | |
| const formData = new FormData(); | |
| formData.append('file', blob, 'frame.jpg'); | |
| formData.append('mode', 'fast'); | |
| const response = await fetch(`${API_URL}/ocr/process`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const result = await response.json(); | |
| lastResults = result; | |
| // Draw results | |
| drawResults(result); | |
| } catch (error) { | |
| statusEl.innerHTML = `β Error: ${error.message}`; | |
| console.error('Scan error:', error); | |
| } | |
| } | |
| // Draw OCR results on canvas | |
| function drawResults(result) { | |
| const zones = result.zones || []; | |
| // Clear previous drawings | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Update status | |
| statusEl.textContent = 'β Scanning...'; | |
| zoneCountEl.textContent = `${zones.length} zones detected`; | |
| // Draw zones | |
| zones.forEach((zone, index) => { | |
| const x = zone.x; | |
| const y = zone.y; | |
| const w = zone.width; | |
| const h = zone.height; | |
| // Draw green box | |
| ctx.strokeStyle = '#00ff00'; | |
| ctx.lineWidth = 3; | |
| ctx.strokeRect(x, y, w, h); | |
| // Draw text label with background | |
| const text = zone.text; | |
| const fontSize = Math.max(16, Math.min(w / text.length * 1.5, 24)); | |
| ctx.font = `bold ${fontSize}px Arial`; | |
| const textMetrics = ctx.measureText(text); | |
| const textWidth = textMetrics.width; | |
| const textHeight = fontSize; | |
| // Background for text | |
| ctx.fillStyle = 'rgba(0, 255, 0, 0.8)'; | |
| ctx.fillRect(x, y - textHeight - 8, textWidth + 10, textHeight + 8); | |
| // Text | |
| ctx.fillStyle = '#000'; | |
| ctx.fillText(text, x + 5, y - 8); | |
| }); | |
| // Show first few zones in details | |
| if (zones.length > 0) { | |
| const preview = zones.slice(0, 3).map((z, i) => | |
| `${i + 1}. ${z.text} (${Math.round(z.confidence * 100)}%)` | |
| ).join('<br>'); | |
| detailsEl.innerHTML = `<div class="zone-info">${preview}${zones.length > 3 ? '<br>...' : ''}</div>`; | |
| } else { | |
| detailsEl.innerHTML = ''; | |
| } | |
| } | |
| // Capture and send results | |
| async function captureResults() { | |
| if (!lastResults) { | |
| tg.showAlert('No results to capture! Start scanning first.'); | |
| return; | |
| } | |
| try { | |
| stopScanning(); | |
| statusEl.textContent = 'π€ Sending results...'; | |
| // Send results back to Telegram chat | |
| const zones = lastResults.zones || []; | |
| const message = `π OCR Results Captured!\n\nβ Found ${zones.length} text zones\n\nProcessing complete!`; | |
| // You can send data back to bot via bot API if needed | |
| // For now, just show confirmation | |
| tg.showAlert(`β Captured ${zones.length} zones!\n\nResults saved.`, () => { | |
| tg.close(); | |
| }); | |
| } catch (error) { | |
| statusEl.innerHTML = `<div class="error">Error: ${error.message}</div>`; | |
| } | |
| } | |
| // Handle Telegram back button | |
| tg.BackButton.show(); | |
| tg.BackButton.onClick(() => { | |
| if (stream) { | |
| stream.getTracks().forEach(track => track.stop()); | |
| } | |
| tg.close(); | |
| }); | |
| // Initialize on load | |
| initCamera(); | |
| </script> | |
| </body> | |
| </html> | |