document.addEventListener('DOMContentLoaded', () => { // ---- 1. DOM Element References ---- const elements = { webcam: document.getElementById('webcam'), captionText: document.getElementById('caption-text'), confidenceFill: document.getElementById('confidence-fill'), confidenceText: document.getElementById('confidence-text'), captionTimestamp: document.getElementById('caption-timestamp'), startButton: document.getElementById('startButton'), stopButton: document.getElementById('stopButton'), muteButton: document.getElementById('muteButton'), settingsButton: document.getElementById('settingsButton'), fullscreenButton: document.getElementById('fullscreenButton'), connectionStatus: document.getElementById('connection-status'), fpsCounter: document.getElementById('fps-counter'), recordingIndicator: document.getElementById('recording-indicator'), latencyValue: document.getElementById('latency-value'), accuracyValue: document.getElementById('accuracy-value'), processedFrames: document.getElementById('processed-frames'), captionsCount: document.getElementById('captions-count'), historyList: document.getElementById('history-list'), deviceInfo: document.getElementById('device-info'), resolutionInfo: document.getElementById('resolution-info'), cacheInfo: document.getElementById('cache-info'), settingsModal: document.getElementById('settingsModal'), closeSettings: document.getElementById('closeSettings'), saveSettings: document.getElementById('saveSettings'), resetSettings: document.getElementById('resetSettings'), frameRateSelect: document.getElementById('frameRateSelect'), qualitySlider: document.getElementById('qualitySlider'), qualityValue: document.getElementById('qualityValue'), audioToggle: document.getElementById('audioToggle'), statusMessage: document.getElementById('status-message'), toastContainer: document.getElementById('toastContainer') }; // ---- 2. Application State & Settings ---- let socket; let stream; let frameSenderInterval; let isCapturing = false; let captionHistory = []; let settings = { frameRate: 15, quality: 0.7, audio: true }; let performance = { sentFrames: 0, receivedFrames: 0, captionsGenerated: 0, totalConfidence: 0, startTime: 0, latencyBuffer: [] }; const LATENCY_BUFFER_SIZE = 20; // ---- 3. Core Application Logic ---- /** * Starts the video analysis process. */ const startAnalysis = async () => { if (isCapturing) return; try { // Get webcam stream stream = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } }, audio: false }); elements.webcam.srcObject = stream; await elements.webcam.play(); isCapturing = true; // Update UI updateUIForStartState(); connectSocket(); } catch (err) { console.error("Error accessing webcam:", err); showToast("Webcam Error", "Could not access the webcam. Please check permissions.", "error"); updateUIForStopState(); } }; /** * Stops the video analysis process. */ const stopAnalysis = () => { if (!isCapturing) return; // Stop intervals and streams clearInterval(frameSenderInterval); frameSenderInterval = null; stream?.getTracks().forEach(track => track.stop()); socket?.disconnect(); // Cancel any ongoing speech window.speechSynthesis.cancel(); // Reset state isCapturing = false; elements.webcam.srcObject = null; updateUIForStopState(); showToast("Analysis Stopped", "Real-time captioning has been turned off.", "info"); }; /** * Connects to the WebSocket server and sets up event listeners. */ const connectSocket = () => { // Use the current host and port, but with the ws:// protocol socket = io(window.location.origin, { transports: ['websocket'], upgrade: false }); socket.on('connect', () => { console.log('Connected to server! SID:', socket.id); elements.connectionStatus.textContent = "Connected"; elements.connectionStatus.style.color = 'var(--success-color)'; showToast("Connected", "Successfully connected to the AI server.", "success"); startFrameSending(); }); socket.on('caption', handleCaption); socket.on('disconnect', () => { console.log('Disconnected from server.'); elements.connectionStatus.textContent = "Disconnected"; elements.connectionStatus.style.color = 'var(--danger-color)'; if (isCapturing) { stopAnalysis(); } }); socket.on('connect_error', (error) => { console.error('Connection error:', error); showToast("Connection Error", "Failed to connect to the server.", "error"); stopAnalysis(); }); }; /** * Initializes the interval for sending video frames to the server. */ const startFrameSending = () => { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d', { alpha: false }); frameSenderInterval = setInterval(() => { if (!isCapturing || elements.webcam.paused || elements.webcam.ended) { return; } // Match the server's expected image size canvas.width = 384; canvas.height = 384; context.drawImage(elements.webcam, 0, 0, canvas.width, canvas.height); const dataUrl = canvas.toDataURL('image/jpeg', settings.quality); socket.emit('image', dataUrl); performance.sentFrames++; updatePerformanceUI(); }, 1000 / settings.frameRate); }; // ---- 4. UI Update Functions ---- /** * Handles incoming captions from the server. * @param {object} data - The caption data from the server. */ const handleCaption = (data) => { performance.receivedFrames++; performance.captionsGenerated++; performance.totalConfidence += data.confidence; // Update main caption display elements.captionText.textContent = data.caption; const confidencePercent = (data.confidence * 100).toFixed(0); elements.confidenceFill.style.width = `${confidencePercent}%`; elements.confidenceText.textContent = `${confidencePercent}%`; const timestamp = new Date(data.timestamp * 1000); elements.captionTimestamp.textContent = timestamp.toLocaleTimeString(); // Calculate latency const latency = (Date.now() / 1000) - data.timestamp; performance.latencyBuffer.push(latency); if (performance.latencyBuffer.length > LATENCY_BUFFER_SIZE) { performance.latencyBuffer.shift(); } // Add to history updateHistory(data.caption, confidencePercent, timestamp); // Speak the caption if (settings.audio) { speakCaption(data.caption); } }; /** * Updates the UI to reflect the "capturing started" state. */ const updateUIForStartState = () => { elements.startButton.disabled = true; elements.stopButton.disabled = false; elements.recordingIndicator.classList.add('active'); elements.statusMessage.textContent = "AI analysis is active..."; // Reset performance metrics performance = { sentFrames: 0, receivedFrames: 0, captionsGenerated: 0, totalConfidence: 0, startTime: Date.now(), latencyBuffer: [] }; elements.resolutionInfo.textContent = `${elements.webcam.videoWidth}x${elements.webcam.videoHeight}`; elements.historyList.innerHTML = '