let socket; let mediaRecorder; let audioChunks = []; let isRecording = false; let displayWindowOpen = false; let currentTimeout; // Store current settings let displaySettings = { textColor: '#ffffff', bgColor: '#000000', fontFamily: 'Arial, sans-serif', fontSize: '32px', duration: 5, googleFont: null }; // Wait for DOM to be fully loaded document.addEventListener('DOMContentLoaded', () => { // Initialize socket connection try { socket = io('http://localhost:3000'); socket.on('connect', () => { console.log('Connected to server'); }); socket.on('connect_error', (error) => { console.error('Socket connection error:', error); }); } catch (error) { console.error('Failed to initialize socket:', error); } const startBtn = document.getElementById('start-btn'); const stopBtn = document.getElementById('stop-btn'); const backendSelect = document.getElementById('backend-select'); const durationInput = document.getElementById('duration-input'); const colorInput = document.getElementById('color-input'); const bgColorInput = document.getElementById('bg-color-input'); const fontSelect = document.getElementById('font-select'); const fontSizeInput = document.getElementById('font-size-input'); const fontSizeDisplay = document.getElementById('font-size-display'); const transcriptionText = document.getElementById('transcription-text'); const transcriptionDisplay = document.getElementById('transcription-display'); const toggleDisplayBtn = document.getElementById('toggle-display-btn'); const googleFontLink = document.getElementById('google-font-link'); const audioLevelFill = document.getElementById('audio-level-fill'); const audioLevelText = document.getElementById('audio-level-text'); const queueStatus = document.getElementById('queue-status'); const chunkSizeSelect = document.getElementById('chunk-size-select'); const settingsBtn = document.getElementById('settings-btn'); // Load available backends async function loadBackends() { try { const response = await fetch('http://localhost:3000/api/backends'); const data = await response.json(); backendSelect.innerHTML = ''; data.backends.forEach(backend => { const option = document.createElement('option'); option.value = backend.id; option.textContent = backend.name + (backend.available ? '' : ' (Unavailable)'); option.disabled = !backend.available; backendSelect.appendChild(option); }); backendSelect.value = data.current; } catch (error) { console.error('Failed to load backends:', error); } } // Initialize loadBackends(); loadAppSettings(); // Update queue status periodically async function updateQueueStatus() { try { const response = await fetch('http://localhost:3000/api/queue-status'); const status = await response.json(); queueStatus.textContent = `Queue: ${status.queueLength}, Running: ${status.runningJobs}`; } catch (error) { // Silently fail - not critical } } setInterval(updateQueueStatus, 2000); // Update every 2 seconds // Save app setting async function saveAppSetting(key, value) { try { await fetch('http://localhost:3000/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [key]: value }) }); } catch (error) { console.warn('Could not save app setting:', key, error); } } // Load app settings async function loadAppSettings() { try { const response = await fetch('http://localhost:3000/api/settings'); const settings = await response.json(); // Apply settings to UI elements if (settings.selectedBackend) { backendSelect.value = settings.selectedBackend; } if (settings.textDuration) { durationInput.value = settings.textDuration; } if (settings.textColor) { colorInput.value = settings.textColor; } if (settings.backgroundColor) { bgColorInput.value = settings.backgroundColor; } if (settings.fontFamily) { fontSelect.value = settings.fontFamily; } if (settings.fontSize) { fontSizeInput.value = settings.fontSize; fontSizeDisplay.textContent = settings.fontSize + 'px'; } if (settings.chunkSize) { chunkSizeSelect.value = settings.chunkSize; } // Update display settings updateDisplaySettings(); console.log('App settings loaded'); } catch (error) { console.warn('Could not load app settings, using defaults:', error); } } // Update display settings and notify display window function updateDisplaySettings() { displaySettings = { textColor: colorInput.value, bgColor: bgColorInput.value, fontFamily: fontSelect.value, fontSize: fontSizeInput.value + 'px', duration: parseInt(durationInput.value), googleFont: fontSelect.options[fontSelect.selectedIndex].getAttribute('data-google') }; // Apply to local display transcriptionText.style.color = displaySettings.textColor; transcriptionText.style.fontSize = displaySettings.fontSize; transcriptionText.style.fontFamily = displaySettings.fontFamily; transcriptionDisplay.style.backgroundColor = displaySettings.bgColor; // Update Google Font link if (displaySettings.googleFont) { googleFontLink.href = `https://fonts.googleapis.com/css2?family=${displaySettings.googleFont}&display=swap`; } // Send to display window if (window.electronAPI) { window.electronAPI.updateDisplaySettings(displaySettings); } } // Font selection handling with auto-save fontSelect.addEventListener('change', () => { updateDisplaySettings(); saveAppSetting('fontFamily', fontSelect.value); const selectedOption = fontSelect.options[fontSelect.selectedIndex]; const googleFont = selectedOption.getAttribute('data-google'); saveAppSetting('googleFont', googleFont); }); colorInput.addEventListener('change', () => { updateDisplaySettings(); saveAppSetting('textColor', colorInput.value); }); bgColorInput.addEventListener('change', () => { updateDisplaySettings(); saveAppSetting('backgroundColor', bgColorInput.value); }); durationInput.addEventListener('change', () => { updateDisplaySettings(); saveAppSetting('textDuration', parseInt(durationInput.value)); }); fontSizeInput.addEventListener('input', (e) => { fontSizeDisplay.textContent = e.target.value + 'px'; updateDisplaySettings(); saveAppSetting('fontSize', parseInt(e.target.value)); }); chunkSizeSelect.addEventListener('change', () => { saveAppSetting('chunkSize', parseInt(chunkSizeSelect.value)); }); // Backend selection backendSelect.addEventListener('change', async (e) => { try { await fetch('http://localhost:3000/api/backend', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ backend: e.target.value }) }); // Save to settings saveAppSetting('selectedBackend', e.target.value); } catch (error) { console.error('Failed to switch backend:', error); } }); // Display window toggle toggleDisplayBtn.addEventListener('click', async () => { if (window.electronAPI) { displayWindowOpen = await window.electronAPI.toggleDisplayWindow(); toggleDisplayBtn.textContent = displayWindowOpen ? 'Close Display Window' : 'Open Display Window'; // Send current settings to the new window if (displayWindowOpen) { setTimeout(() => { window.electronAPI.updateDisplaySettings(displaySettings); }, 500); } } }); // Listen for display window close event if (window.electronAPI) { window.electronAPI.onDisplayWindowClosed(() => { displayWindowOpen = false; toggleDisplayBtn.textContent = 'Open Display Window'; }); } // Socket event listeners if (socket) { socket.on('transcription', (data) => { displayTranscription(data.text); // Also send to display window via IPC if (window.electronAPI && displayWindowOpen) { window.electronAPI.sendTranscription(data); } }); } function displayTranscription(text) { transcriptionText.textContent = text; transcriptionText.classList.remove('fade-out'); if (currentTimeout) { clearTimeout(currentTimeout); } const duration = displaySettings.duration * 1000; currentTimeout = setTimeout(() => { transcriptionText.classList.add('fade-out'); setTimeout(() => { transcriptionText.textContent = ''; }, 300); }, duration); } // Audio level monitoring let audioContext; let analyser; let microphone; let dataArray; let audioLevelInterval; function startAudioLevelMonitoring(stream) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); analyser = audioContext.createAnalyser(); microphone = audioContext.createMediaStreamSource(stream); // Configure analyser for better responsiveness analyser.fftSize = 512; analyser.smoothingTimeConstant = 0.3; analyser.minDecibels = -90; analyser.maxDecibels = -10; const bufferLength = analyser.frequencyBinCount; dataArray = new Uint8Array(bufferLength); microphone.connect(analyser); function updateAudioLevel() { if (!isRecording) return; analyser.getByteTimeDomainData(dataArray); // Calculate RMS of time domain data (more accurate for level detection) let sum = 0; for (let i = 0; i < bufferLength; i++) { const sample = (dataArray[i] - 128) / 128; // Convert to -1 to 1 range sum += sample * sample; } const rms = Math.sqrt(sum / bufferLength); // Apply some smoothing and scaling const level = Math.min(rms * 10, 1); // Scale up and cap at 1 // Update UI const percentage = Math.round(level * 100); audioLevelFill.style.width = `${percentage}%`; audioLevelText.textContent = `${percentage}%`; // Store current level for threshold checking window.currentAudioLevel = level; } // Update at 60fps for smooth animation audioLevelInterval = setInterval(updateAudioLevel, 16); } // Audio recording async function startRecording() { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // Start audio level monitoring startAudioLevelMonitoring(stream); const options = { mimeType: 'audio/webm' }; if (!MediaRecorder.isTypeSupported(options.mimeType)) { options.mimeType = 'audio/ogg'; if (!MediaRecorder.isTypeSupported(options.mimeType)) { options.mimeType = ''; } } mediaRecorder = new MediaRecorder(stream, options); mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { audioChunks.push(event.data); } }; mediaRecorder.onstop = async () => { const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); audioChunks = []; await sendAudioForTranscription(audioBlob); }; mediaRecorder.start(); isRecording = true; // Auto-stop and restart recording based on selected chunk size const chunkSize = parseInt(chunkSizeSelect.value); const recordingInterval = setInterval(() => { if (isRecording && mediaRecorder.state === 'recording') { mediaRecorder.stop(); setTimeout(() => { if (isRecording) { audioChunks = []; mediaRecorder.start(); } }, 100); } else if (!isRecording) { clearInterval(recordingInterval); } }, chunkSize); startBtn.disabled = true; stopBtn.disabled = false; } catch (error) { console.error('Error starting recording:', error); alert('Failed to start recording. Please check microphone permissions.'); } } function stopRecording() { if (mediaRecorder && mediaRecorder.state !== 'inactive') { mediaRecorder.stop(); mediaRecorder.stream.getTracks().forEach(track => track.stop()); } isRecording = false; startBtn.disabled = false; stopBtn.disabled = true; // Stop audio level monitoring if (audioLevelInterval) { clearInterval(audioLevelInterval); audioLevelInterval = null; } if (audioContext) { audioContext.close(); audioContext = null; } // Reset audio level display audioLevelFill.style.width = '0%'; audioLevelText.textContent = '0%'; } async function sendAudioForTranscription(audioBlob) { const formData = new FormData(); formData.append('audio', audioBlob, 'audio.webm'); try { const response = await fetch('http://localhost:3000/api/transcribe', { method: 'POST', body: formData }); if (!response.ok) { const error = await response.json(); console.error('Transcription error:', error); } } catch (error) { console.error('Failed to send audio:', error); } } // Event listeners startBtn.addEventListener('click', () => { console.log('Start button clicked'); startRecording(); }); stopBtn.addEventListener('click', () => { console.log('Stop button clicked'); stopRecording(); }); // Initial settings application updateDisplaySettings(); // Settings button settingsBtn.addEventListener('click', () => { window.location.href = 'settings.html'; }); });