Archived
First commit of local Closed Caption App
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
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';
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
let currentTimeout;
|
||||
let displaySettings = {
|
||||
textColor: '#ffffff',
|
||||
bgColor: '#000000',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
fontSize: '32px',
|
||||
duration: 5
|
||||
};
|
||||
|
||||
const transcriptionText = document.getElementById('transcription-text');
|
||||
const displayContainer = document.getElementById('display-container');
|
||||
const googleFontLink = document.getElementById('google-font-link');
|
||||
|
||||
// Apply settings
|
||||
function applySettings(settings) {
|
||||
displaySettings = { ...displaySettings, ...settings };
|
||||
|
||||
transcriptionText.style.color = displaySettings.textColor;
|
||||
transcriptionText.style.fontSize = displaySettings.fontSize;
|
||||
transcriptionText.style.fontFamily = displaySettings.fontFamily;
|
||||
displayContainer.style.backgroundColor = displaySettings.bgColor;
|
||||
|
||||
// Handle Google Fonts
|
||||
if (settings.googleFont) {
|
||||
googleFontLink.href = `https://fonts.googleapis.com/css2?family=${settings.googleFont}&display=swap`;
|
||||
}
|
||||
}
|
||||
|
||||
// Display transcription
|
||||
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);
|
||||
}
|
||||
|
||||
// Listen for IPC messages
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI.onTranscription((data) => {
|
||||
displayTranscription(data.text);
|
||||
});
|
||||
|
||||
window.electronAPI.onUpdateSettings((settings) => {
|
||||
applySettings(settings);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
let isCalibrating = false;
|
||||
let calibrationData = [];
|
||||
let calibrationInterval;
|
||||
let audioContext;
|
||||
let analyser;
|
||||
let microphone;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Get all form elements
|
||||
const elements = {
|
||||
openaiKey: document.getElementById('openai-key'),
|
||||
elevenlabsKey: document.getElementById('elevenlabs-key'),
|
||||
whisperModel: document.getElementById('whisper-model'),
|
||||
gpuAcceleration: document.getElementById('gpu-acceleration'),
|
||||
audioThreshold: document.getElementById('audio-threshold'),
|
||||
thresholdValue: document.getElementById('threshold-value'),
|
||||
chunkSize: document.getElementById('chunk-size'),
|
||||
defaultBackend: document.getElementById('default-backend'),
|
||||
textDuration: document.getElementById('text-duration'),
|
||||
textColor: document.getElementById('text-color'),
|
||||
bgColor: document.getElementById('bg-color'),
|
||||
fontFamily: document.getElementById('font-family'),
|
||||
fontSize: document.getElementById('font-size'),
|
||||
fontSizeValue: document.getElementById('font-size-value'),
|
||||
serverPort: document.getElementById('server-port'),
|
||||
|
||||
// Calibration elements
|
||||
startCalibration: document.getElementById('start-calibration'),
|
||||
stopCalibration: document.getElementById('stop-calibration'),
|
||||
calibrationProgress: document.getElementById('calibration-progress'),
|
||||
ambientLevel: document.getElementById('ambient-level'),
|
||||
calibrationStatus: document.getElementById('calibration-status'),
|
||||
|
||||
// Action buttons
|
||||
saveSettings: document.getElementById('save-settings'),
|
||||
resetSettings: document.getElementById('reset-settings'),
|
||||
testConnection: document.getElementById('test-connection'),
|
||||
detectGpu: document.getElementById('detect-gpu'),
|
||||
statusMessage: document.getElementById('status-message')
|
||||
};
|
||||
|
||||
// Update threshold value display
|
||||
elements.audioThreshold.addEventListener('input', (e) => {
|
||||
elements.thresholdValue.textContent = parseFloat(e.target.value).toFixed(3);
|
||||
});
|
||||
|
||||
// Update font size value display
|
||||
elements.fontSize.addEventListener('input', (e) => {
|
||||
elements.fontSizeValue.textContent = e.target.value + 'px';
|
||||
});
|
||||
|
||||
// Load settings on page load
|
||||
loadSettings();
|
||||
|
||||
// Save settings
|
||||
elements.saveSettings.addEventListener('click', saveSettings);
|
||||
|
||||
// Reset settings
|
||||
elements.resetSettings.addEventListener('click', resetSettings);
|
||||
|
||||
// Test API connections
|
||||
elements.testConnection.addEventListener('click', testApiConnections);
|
||||
|
||||
// Detect GPU
|
||||
elements.detectGpu.addEventListener('click', detectGpu);
|
||||
|
||||
// Calibration controls
|
||||
elements.startCalibration.addEventListener('click', startCalibration);
|
||||
elements.stopCalibration.addEventListener('click', stopCalibration);
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/api/settings');
|
||||
const settings = await response.json();
|
||||
|
||||
// Populate form fields
|
||||
elements.openaiKey.value = settings.openaiApiKey || '';
|
||||
elements.elevenlabsKey.value = settings.elevenlabsApiKey || '';
|
||||
elements.whisperModel.value = settings.whisperModel || 'base';
|
||||
elements.gpuAcceleration.value = settings.gpuAcceleration || 'none';
|
||||
elements.audioThreshold.value = settings.audioThreshold || 0.01;
|
||||
elements.thresholdValue.textContent = (settings.audioThreshold || 0.01).toFixed(3);
|
||||
elements.chunkSize.value = settings.chunkSize || 2000;
|
||||
elements.defaultBackend.value = settings.selectedBackend || 'whisper-local';
|
||||
elements.textDuration.value = settings.textDuration || 5;
|
||||
elements.textColor.value = settings.textColor || '#ffffff';
|
||||
elements.bgColor.value = settings.backgroundColor || '#000000';
|
||||
elements.fontFamily.value = settings.fontFamily || 'Arial, sans-serif';
|
||||
elements.fontSize.value = settings.fontSize || 32;
|
||||
elements.fontSizeValue.textContent = (settings.fontSize || 32) + 'px';
|
||||
elements.serverPort.value = settings.serverPort || 3000;
|
||||
|
||||
// Update ambient noise level display
|
||||
if (settings.ambientNoiseLevel !== null) {
|
||||
elements.ambientLevel.textContent = `Current: ${settings.ambientNoiseLevel.toFixed(4)}`;
|
||||
}
|
||||
|
||||
showStatus('Settings loaded successfully', 'success');
|
||||
} catch (error) {
|
||||
console.error('Error loading settings:', error);
|
||||
showStatus('Error loading settings', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
try {
|
||||
const settings = {
|
||||
openaiApiKey: elements.openaiKey.value.trim(),
|
||||
elevenlabsApiKey: elements.elevenlabsKey.value.trim(),
|
||||
whisperModel: elements.whisperModel.value,
|
||||
gpuAcceleration: elements.gpuAcceleration.value,
|
||||
audioThreshold: parseFloat(elements.audioThreshold.value),
|
||||
chunkSize: parseInt(elements.chunkSize.value),
|
||||
selectedBackend: elements.defaultBackend.value,
|
||||
textDuration: parseInt(elements.textDuration.value),
|
||||
textColor: elements.textColor.value,
|
||||
backgroundColor: elements.bgColor.value,
|
||||
fontFamily: elements.fontFamily.value,
|
||||
fontSize: parseInt(elements.fontSize.value),
|
||||
googleFont: elements.fontFamily.options[elements.fontFamily.selectedIndex].getAttribute('data-google'),
|
||||
serverPort: parseInt(elements.serverPort.value)
|
||||
};
|
||||
|
||||
const response = await fetch('http://localhost:3000/api/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showStatus('Settings saved successfully! Some changes may require a restart.', 'success');
|
||||
} else {
|
||||
throw new Error('Failed to save settings');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
showStatus('Error saving settings', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function resetSettings() {
|
||||
if (!confirm('Are you sure you want to reset all settings to defaults? This cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/api/settings/reset', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await loadSettings();
|
||||
showStatus('Settings reset to defaults', 'success');
|
||||
} else {
|
||||
throw new Error('Failed to reset settings');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error resetting settings:', error);
|
||||
showStatus('Error resetting settings', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function detectGpu() {
|
||||
showStatus('Detecting GPU...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/api/detect-gpu', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
let message = 'GPU Detection Results:\n';
|
||||
message += `NVIDIA CUDA: ${result.cuda ? 'Available' : 'Not found'}\n`;
|
||||
message += `AMD ROCm: ${result.rocm ? 'Available' : 'Not found'}\n`;
|
||||
message += `Intel OpenVINO: ${result.openvino ? 'Available' : 'Not found'}\n`;
|
||||
|
||||
if (result.recommended) {
|
||||
message += `\nRecommended: ${result.recommended}`;
|
||||
elements.gpuAcceleration.value = result.recommended;
|
||||
}
|
||||
|
||||
showStatus(message.replace(/\n/g, '<br>'), 'success');
|
||||
} catch (error) {
|
||||
console.error('Error detecting GPU:', error);
|
||||
showStatus('Error detecting GPU', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function testApiConnections() {
|
||||
showStatus('Testing API connections...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/api/test-connections', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
openaiApiKey: elements.openaiKey.value.trim(),
|
||||
elevenlabsApiKey: elements.elevenlabsKey.value.trim()
|
||||
})
|
||||
});
|
||||
|
||||
const results = await response.json();
|
||||
|
||||
let message = 'API Connection Test Results:\n';
|
||||
if (results.openai) {
|
||||
message += `✓ OpenAI: ${results.openai.success ? 'Connected' : 'Failed - ' + results.openai.error}\n`;
|
||||
}
|
||||
if (results.elevenlabs) {
|
||||
message += `✓ ElevenLabs: ${results.elevenlabs.success ? 'Connected' : 'Failed - ' + results.elevenlabs.error}\n`;
|
||||
}
|
||||
|
||||
showStatus(message.replace(/\n/g, '<br>'), results.openai?.success || results.elevenlabs?.success ? 'success' : 'error');
|
||||
} catch (error) {
|
||||
console.error('Error testing connections:', error);
|
||||
showStatus('Error testing API connections', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function startCalibration() {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
analyser = audioContext.createAnalyser();
|
||||
microphone = audioContext.createMediaStreamSource(stream);
|
||||
|
||||
analyser.fftSize = 512;
|
||||
analyser.smoothingTimeConstant = 0.3;
|
||||
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
|
||||
microphone.connect(analyser);
|
||||
|
||||
isCalibrating = true;
|
||||
calibrationData = [];
|
||||
elements.startCalibration.disabled = true;
|
||||
elements.stopCalibration.disabled = false;
|
||||
elements.calibrationStatus.textContent = 'Calibrating... Stay quiet for 10 seconds.';
|
||||
|
||||
let progress = 0;
|
||||
const calibrationDuration = 10000; // 10 seconds
|
||||
const updateInterval = 100; // Update every 100ms
|
||||
const totalUpdates = calibrationDuration / updateInterval;
|
||||
|
||||
calibrationInterval = setInterval(() => {
|
||||
if (!isCalibrating) return;
|
||||
|
||||
analyser.getByteTimeDomainData(dataArray);
|
||||
|
||||
// Calculate RMS
|
||||
let sum = 0;
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
const sample = (dataArray[i] - 128) / 128;
|
||||
sum += sample * sample;
|
||||
}
|
||||
const rms = Math.sqrt(sum / bufferLength);
|
||||
calibrationData.push(rms);
|
||||
|
||||
progress++;
|
||||
const progressPercent = (progress / totalUpdates) * 100;
|
||||
elements.calibrationProgress.style.width = `${progressPercent}%`;
|
||||
|
||||
if (progress >= totalUpdates) {
|
||||
finishCalibration();
|
||||
}
|
||||
}, updateInterval);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error starting calibration:', error);
|
||||
showStatus('Error accessing microphone for calibration', 'error');
|
||||
stopCalibration();
|
||||
}
|
||||
}
|
||||
|
||||
function stopCalibration() {
|
||||
isCalibrating = false;
|
||||
|
||||
if (calibrationInterval) {
|
||||
clearInterval(calibrationInterval);
|
||||
calibrationInterval = null;
|
||||
}
|
||||
|
||||
if (audioContext) {
|
||||
audioContext.close();
|
||||
audioContext = null;
|
||||
}
|
||||
|
||||
elements.startCalibration.disabled = false;
|
||||
elements.stopCalibration.disabled = true;
|
||||
elements.calibrationProgress.style.width = '0%';
|
||||
elements.calibrationStatus.textContent = 'Calibration stopped.';
|
||||
}
|
||||
|
||||
async function finishCalibration() {
|
||||
if (calibrationData.length === 0) {
|
||||
stopCalibration();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate average ambient noise level
|
||||
const avgLevel = calibrationData.reduce((a, b) => a + b, 0) / calibrationData.length;
|
||||
|
||||
// Set threshold slightly above ambient level
|
||||
const suggestedThreshold = Math.max(avgLevel * 3, 0.005);
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/api/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
ambientNoiseLevel: avgLevel,
|
||||
audioThreshold: suggestedThreshold
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
elements.ambientLevel.textContent = `Current: ${avgLevel.toFixed(4)}`;
|
||||
elements.audioThreshold.value = suggestedThreshold;
|
||||
elements.thresholdValue.textContent = suggestedThreshold.toFixed(3);
|
||||
elements.calibrationStatus.textContent = `Calibration complete! New threshold: ${suggestedThreshold.toFixed(3)}`;
|
||||
showStatus('Ambient noise calibration completed successfully', 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving calibration:', error);
|
||||
showStatus('Error saving calibration results', 'error');
|
||||
}
|
||||
|
||||
stopCalibration();
|
||||
}
|
||||
|
||||
function showStatus(message, type) {
|
||||
elements.statusMessage.textContent = message;
|
||||
elements.statusMessage.innerHTML = message; // Allow HTML for test results
|
||||
elements.statusMessage.className = `status-${type}`;
|
||||
elements.statusMessage.style.display = 'block';
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(() => {
|
||||
elements.statusMessage.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
// Add status message styles
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.status-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
||||
.status-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||||
.status-info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
Reference in New Issue
Block a user