Archived
444 lines
14 KiB
JavaScript
444 lines
14 KiB
JavaScript
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';
|
|
});
|
|
}); |