diff options
| author | yum <yum.food.vr@gmail.com> | 2025-07-23 17:41:49 -0700 |
|---|---|---|
| committer | yum <yum.food.vr@gmail.com> | 2025-07-23 17:41:49 -0700 |
| commit | 790c91d7ad515c3c0a22ca1341316265b8f0d779 (patch) | |
| tree | 28527bbcf87e8fab1d27eb76a1f5ea325b94d599 | |
| parent | 73de7cb2d8fb964e7f76ab55420e9bc331bf7bea (diff) | |
bugfixes
* fix model acquisition
* fix local beepsnd
* fix volume control
| -rw-r--r-- | app/hi.py | 45 | ||||
| -rw-r--r-- | app/requirements.txt | 1 | ||||
| -rw-r--r-- | app/stt.py | 62 | ||||
| -rw-r--r-- | config.yaml | 8 | ||||
| -rw-r--r-- | ui/config-schema.js | 2 | ||||
| -rw-r--r-- | ui/index.html | 13 | ||||
| -rw-r--r-- | ui/index.js | 17 | ||||
| -rw-r--r-- | ui/preload.js | 1 | ||||
| -rw-r--r-- | ui/renderer.js | 198 |
9 files changed, 196 insertions, 151 deletions
@@ -26,9 +26,6 @@ TESTS_ENABLED = True # 0 = quiet, 1 = verbose, 2 = very verbose LOG_LEVEL = 0 -# Global volume control (0.0 to 1.0) -VOLUME = 0.3 - APP_ROOT = os.path.dirname(os.path.abspath(__file__)) PROJECT_ROOT = os.path.dirname(APP_ROOT) @@ -347,7 +344,8 @@ def osc_thread(shared_data: SharedThreadData): if time.time() - last_change < 1.5: continue addr = "/chatbox/input" - print(f"Send {local_word}", flush=True) + if shared_data.cfg["enable_debug_mode"]: + print(f"Send {local_word}", flush=True) osc_client.send_message(addr, (local_word, True, False)) last_change = time.time() remote_word = local_word @@ -420,20 +418,16 @@ def vrInputThread(shared_data: SharedThreadData): if last_rising - last_medium_press_end < 1.0: # Type transcription - if shared_data.cfg["enable_local_beep"]: - play_sound_with_volume(waveform3) + play_sound_with_volume(waveform3, shared_data.cfg) else: - if shared_data.cfg["enable_local_beep"]: - play_sound_with_volume(waveform1) + play_sound_with_volume(waveform1, shared_data.cfg) elif now - last_rising > 0.5: # Medium press print("CLEARING", file=sys.stderr) last_medium_press_end = now state = PAUSE_STATE - - if shared_data.cfg["enable_local_beep"]: - play_sound_with_volume(waveform2) + play_sound_with_volume(waveform2, shared_data.cfg) # Flush the *entire* pipeline. shared_data.stream.pause(True) @@ -449,9 +443,7 @@ def vrInputThread(shared_data: SharedThreadData): state = PAUSE_STATE shared_data.stream.pause(True) - - if shared_data.cfg["enable_local_beep"]: - play_sound_with_volume(waveform1) + play_sound_with_volume(waveform1, shared_data.cfg) elif state == PAUSE_STATE: print("RECORDING", file=sys.stderr) state = RECORD_STATE @@ -469,9 +461,7 @@ def vrInputThread(shared_data: SharedThreadData): #audio_state.text += audio_state.preview_text shared_data.stream.pause(False) - - if shared_data.cfg["enable_local_beep"]: - play_sound_with_volume(waveform0) + play_sound_with_volume(waveform0, shared_data.cfg) def kbInputThread(shared_data: SharedThreadData): @@ -514,9 +504,7 @@ def kbInputThread(shared_data: SharedThreadData): if event == EVENT_DOUBLE_PRESS: print("CLEARING", file=sys.stderr) state = PAUSE_STATE - - if shared_data.cfg["enable_local_beep"]: - play_sound_with_volume(waveform2) + play_sound_with_volume(waveform2, shared_data.cfg) # Flush the *entire* pipeline. shared_data.stream.pause(True) @@ -530,11 +518,8 @@ def kbInputThread(shared_data: SharedThreadData): if state == RECORD_STATE: print("PAUSED", file=sys.stderr) state = PAUSE_STATE - shared_data.stream.pause(True) - - if shared_data.cfg["enable_local_beep"]: - play_sound_with_volume(waveform1) + play_sound_with_volume(waveform1, shared_data.cfg) elif state == PAUSE_STATE: print("RECORDING", file=sys.stderr) state = RECORD_STATE @@ -548,20 +533,16 @@ def kbInputThread(shared_data: SharedThreadData): if shared_data.cfg["enable_debug_mode"]: print("Toggle detected, committing preview text (2)", file=sys.stderr) - #audio_state.text += audio_state.preview_text - shared_data.stream.pause(False) + play_sound_with_volume(waveform0, shared_data.cfg) - if shared_data.cfg["enable_local_beep"]: - play_sound_with_volume(waveform0) - -def play_sound_with_volume(filepath): +def play_sound_with_volume(filepath, cfg): """Play a WAV file with adjusted volume""" - volume = VOLUME + volume = cfg.get("volume", 30) try: sound = pygame.mixer.Sound(filepath) - sound.set_volume(volume) + sound.set_volume(volume * 0.01) sound.play() except Exception as e: print(f"Error playing sound {filepath}: {e}", file=sys.stderr) diff --git a/app/requirements.txt b/app/requirements.txt index e68a16c..c8d69df 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -2,6 +2,7 @@ faster-whisper hf-xet keyboard langcodes +noisereduce pyaudio pygame pydub @@ -3,6 +3,7 @@ from faster_whisper import WhisperModel import langcodes import numpy as np import os +import noisereduce as nr try: from profanity_filter import ProfanityFilter PROFANITY_FILTER_AVAILABLE = True @@ -260,9 +261,13 @@ class NormalizingAudioCollector(AudioCollectorFilter): return frames class BoostingAudioCollector(AudioCollectorFilter): - def __init__(self, parent: AudioCollector, target_dBFS: float, cfg: typing.Dict): + def __init__(self, parent: AudioCollector, + target_dBFS: float, + max_gain_dB: float, + cfg: typing.Dict): AudioCollectorFilter.__init__(self, parent) self.target_dBFS = target_dBFS + self.max_gain_dB = max_gain_dB self.cfg = cfg def getAudio(self) -> bytes: @@ -270,9 +275,10 @@ class BoostingAudioCollector(AudioCollectorFilter): audio = AudioSegment(audio, sample_width=AudioStream.FRAME_SZ, frame_rate=AudioStream.FPS, channels=AudioStream.CHANNELS) + gain = min(self.target_dBFS - audio.dBFS, self.max_gain_dB) if self.cfg["enable_debug_mode"]: - print(f"Boosting audio from {audio.dBFS}dB to {self.target_dBFS}dB", file=sys.stderr) - audio = audio.apply_gain(self.target_dBFS - audio.dBFS) + print(f"Boosting audio by {gain} dB (from {audio.dBFS} to {audio.dBFS + gain})", flush=True) + audio = audio.apply_gain(gain) frames = np.array(audio.get_array_of_samples()) frames = np.int16(frames).tobytes() @@ -296,6 +302,26 @@ class CompressingAudioCollector(AudioCollectorFilter): return frames +class NoiseReducingAudioCollector(AudioCollectorFilter): + def __init__(self, parent: AudioCollector, cfg: typing.Dict): + AudioCollectorFilter.__init__(self, parent) + self.cfg = cfg + + def getAudio(self) -> bytes: + audio = self.parent.getAudio() + audio_array = np.frombuffer(audio, dtype=np.int16).astype(np.float32) + + reduced_audio = nr.reduce_noise( + y=audio_array, + sr=AudioStream.FPS, + ) + + # Convert back to int16 + reduced_audio = np.clip(reduced_audio, -32768, 32767) + frames = np.int16(reduced_audio).tobytes() + + return frames + class AudioSegmenter: def __init__(self, min_silence_ms=250, @@ -398,6 +424,12 @@ class Segment: avg_logprob = f"(avg_logprob: {self.avg_logprob}) " return f"{self.transcript} " + ts + wall_ts + no_speech + avg_logprob +def join_segments(a, b): + if len(a) > 0 and a[-1] != ' ': + return a + ' ' + b + else: + return a + b + class Whisper: def __init__(self, collector: AudioCollector, @@ -421,6 +453,9 @@ class Whisper: already_downloaded = os.path.exists(model_root) + if not already_downloaded: + print(f"Model {model_str} not already downloaded, downloading now...", flush=True) + self.model = WhisperModel(model_str, device = model_device, device_index = cfg["gpu_idx"], @@ -433,10 +468,12 @@ class Whisper: def update_context(self, committed_text: str): """Update the context with recently committed text.""" - self.recent_context = (self.recent_context + " " + committed_text).strip() - # Keep only the last N characters to avoid prompt getting too long + self.recent_context = join_segments(self.recent_context, committed_text).strip() + # Drop half of the context window. if len(self.recent_context) > self.context_window_chars: - self.recent_context = self.recent_context[-self.context_window_chars:] + words = self.recent_context.split() + words = words[len(words)//2:] + self.recent_context = ' '.join(words) def transcribe(self, frames: bytes = None) -> typing.List[Segment]: if frames is None: @@ -449,6 +486,8 @@ class Whisper: # Build context-aware prompt prompt = self._build_prompt() + print(f"Prompt: {prompt}", flush=True) + t0 = time.time() segments, info = self.model.transcribe( audio, @@ -698,8 +737,10 @@ def transcriptionThread(shared_data: SharedThreadData): stream = MicStream(shared_data.cfg) collector = AudioCollector(stream) collector = CompressingAudioCollector(collector) - collector = BoostingAudioCollector(collector, -12.0, shared_data.cfg) - collector = NormalizingAudioCollector(collector) + collector = BoostingAudioCollector(collector, -24.0, 24.0, + shared_data.cfg) + collector = NoiseReducingAudioCollector(collector, shared_data.cfg) + #collector = NormalizingAudioCollector(collector) whisper = Whisper(collector, shared_data.cfg) segmenter = AudioSegmenter(min_silence_ms=shared_data.cfg["min_silence_duration_ms"], max_speech_s=shared_data.cfg["max_speech_duration_s"], @@ -761,11 +802,6 @@ def transcriptionThread(shared_data: SharedThreadData): # breaking OSC pager. if len(shared_data.transcript) >= 1024: shared_data.transcript = shared_data.transcript[-512:] - def join_segments(a, b): - if len(a) > 0 and a[-1] != ' ': - return a + ' ' + b - else: - return a + b shared_data.transcript = \ join_segments(shared_data.transcript, commit.delta) shared_data.preview = commit.preview diff --git a/config.yaml b/config.yaml index 6f4b65b..dfa2e1f 100644 --- a/config.yaml +++ b/config.yaml @@ -1,8 +1,8 @@ compute_type: float16 language: english model: turbo -microphone: 1 -user_prompt: Use proper punctuation and grammar. Prefer spelled out numbers like one, eleven, twenty, etc. Mm. Phi, NOPPERS, clearrainbow, Noia, Kuuderekitten. +microphone: 4 +user_prompt: Use proper punctuation and grammar. Prefer spelled out numbers like one, eleven, twenty, etc. Mm. keybind: ctrl+alt+x button_hand: right button_type: b @@ -18,6 +18,7 @@ rows: 10 cols: 24 beam_size: 5 best_of: 5 +volume: 10 enable_debug_mode: 0 enable_previews: 1 save_audio: 1 @@ -26,6 +27,5 @@ enable_lowercase_filter: 0 enable_uppercase_filter: 0 enable_profanity_filter: 0 remove_trailing_period: 0 -reset_on_toggle: 0 -enable_local_beep: 1 +reset_on_toggle: 1 use_builtin: 1 diff --git a/ui/config-schema.js b/ui/config-schema.js index 6b11277..bf91fce 100644 --- a/ui/config-schema.js +++ b/ui/config-schema.js @@ -23,6 +23,7 @@ const CONFIG_SCHEMA = { cols: { type: 'number', default: 24 }, beam_size: { type: 'number', default: 5 }, best_of: { type: 'number', default: 5 }, + volume: { type: 'number', default: 30 }, // Boolean fields (stored as 1/0) enable_debug_mode: { type: 'boolean', default: 0 }, @@ -34,7 +35,6 @@ const CONFIG_SCHEMA = { enable_profanity_filter: { type: 'boolean', default: 0 }, remove_trailing_period: { type: 'boolean', default: 0 }, reset_on_toggle: { type: 'boolean', default: 0 }, - enable_local_beep: { type: 'boolean', default: 1 }, use_builtin: { type: 'boolean', default: 1 } }; diff --git a/ui/index.html b/ui/index.html index 99e64dd..19c41ce 100644 --- a/ui/index.html +++ b/ui/index.html @@ -248,10 +248,13 @@ <input type="checkbox" id="reset_on_toggle" class="mr-2"> <span class="checkbox-text">Reset transcript on toggle</span> </label> - <label for="enable_local_beep" class="checkbox-label"> - <input type="checkbox" id="enable_local_beep" checked class="mr-2"> - <span class="checkbox-text">Enable local beep sounds</span> - </label> + <div> + <label for="volume" class="form-label"> + Local Beep Volume + <span id="volume-display" class="text-gray-500 text-sm ml-2">30%</span> + </label> + <input type="range" id="volume" min="0" max="100" step="10" value="30" class="form-input w-full"> + </div> </div> </section> @@ -314,7 +317,7 @@ <button type="button" id="start-process" class="btn btn-green flex-1"> Start </button> - <button type="button" id="stop-process" class="btn btn-red flex-1" disabled> + <button type="button" id="stop-process" class="btn btn-red flex-1"> Stop </button> </div> diff --git a/ui/index.js b/ui/index.js index 24a7e13..5a5d0a6 100644 --- a/ui/index.js +++ b/ui/index.js @@ -530,19 +530,20 @@ ipcMain.handle('start-process', async () => { }); ipcMain.handle('stop-process', async () => { + if (!runningProcess) { + sendPythonOutput('No process to stop', 'info'); + return { success: true, forcefullyKilled: false }; + } + return new Promise((resolve) => { let forcefullyKilled = false; - - if (!runningProcess) { - resolve({ success: true, forcefullyKilled }); - } // Set up a timeout to force kill after 10 seconds const killTimeout = setTimeout(() => { if (runningProcess) { sendPythonOutput('Process did not stop gracefully, forcing termination...', 'stderr'); forcefullyKilled = true; - runningProcess.kill(); + runningProcess.kill('SIGKILL'); } }, 10000); @@ -562,10 +563,14 @@ ipcMain.handle('stop-process', async () => { // Send termination signal sendPythonOutput('Stopping process gracefully...', 'info'); - runningProcess.kill(); + runningProcess.kill('SIGTERM'); }); }); +ipcMain.handle('get-process-state', () => { + return { isRunning: runningProcess !== null }; +}); + // Clean up on app quit app.on('before-quit', () => { if (runningProcess) { diff --git a/ui/preload.js b/ui/preload.js index f2e0a81..6f6e54f 100644 --- a/ui/preload.js +++ b/ui/preload.js @@ -10,6 +10,7 @@ contextBridge.exposeInMainWorld('electronAPI', { resetVenv: () => ipcRenderer.invoke('reset-venv'), startProcess: () => ipcRenderer.invoke('start-process'), stopProcess: () => ipcRenderer.invoke('stop-process'), + getProcessState: () => ipcRenderer.invoke('get-process-state'), onPythonOutput: (callback) => ipcRenderer.on('python-output', (event, data) => callback(data)), onProcessStopped: (callback) => ipcRenderer.on('process-stopped', () => callback()) }); diff --git a/ui/renderer.js b/ui/renderer.js index 2f4c8f1..008e0da 100644 --- a/ui/renderer.js +++ b/ui/renderer.js @@ -1,6 +1,21 @@ // Import configuration schema const CONFIG_FIELDS = window.CONFIG_SCHEMA; +// Process state tracking +let isProcessRunning = false; +let buttonManager; +let loadingOverlay; + +// Auto-save functionality with debouncing +let saveTimeout; +const SAVE_DELAY = 500; +let isSettingValues = false; + +// Console management +const consoleContent = document.getElementById('console-content'); +const MAX_CONSOLE_LINES = 512; +let consoleLineCount = 0; + // Button management system class ButtonManager { constructor() { @@ -11,33 +26,30 @@ class ButtonManager { resetVenv: document.getElementById('reset-venv'), refreshMicrophones: document.getElementById('refresh-microphones') }; - - // Initialize button states on construction + + // Initialize button states - process is not running at startup this.setProcessStopped(); } - + setState(buttonName, disabled) { const button = this.buttons[buttonName]; if (!button) return; - + button.disabled = disabled; - if (disabled) { - button.classList.add('opacity-50', 'cursor-not-allowed'); - } else { - button.classList.remove('opacity-50', 'cursor-not-allowed'); - } } - + setProcessRunning() { this.setState('start', true); this.setState('stop', false); + isProcessRunning = true; } - + setProcessStopped() { this.setState('start', false); this.setState('stop', true); + isProcessRunning = false; } - + async withButtonLoading(buttonName, asyncFn) { this.setState(buttonName, true); try { @@ -48,8 +60,6 @@ class ButtonManager { } } -const buttonManager = new ButtonManager(); - // Add loading overlay management class LoadingOverlay { constructor() { @@ -57,8 +67,9 @@ class LoadingOverlay { this.form = document.getElementById('config-form'); this.messageElement = this.overlay.querySelector('p'); this.defaultMessage = 'Environment setup underway - please wait.'; + this.originalStates = new Map(); // Track original disabled states } - + show(message = null) { this.messageElement.textContent = message || this.defaultMessage; this.overlay.classList.remove('hidden'); @@ -66,68 +77,69 @@ class LoadingOverlay { const leftPanel = this.overlay.parentElement; const inputs = leftPanel.querySelectorAll('input, select, textarea, button'); inputs.forEach(input => { + // Store original disabled state before disabling + this.originalStates.set(input, input.disabled); input.disabled = true; input.classList.add('opacity-50'); }); } - + hide() { this.overlay.classList.add('hidden'); - // Re-enable all form inputs and buttons in the entire left panel + // Restore original states of form inputs and buttons const leftPanel = this.overlay.parentElement; const inputs = leftPanel.querySelectorAll('input, select, textarea, button'); inputs.forEach(input => { - input.disabled = false; + // Restore original disabled state + input.disabled = this.originalStates.get(input) || false; input.classList.remove('opacity-50'); }); + // Clear the stored states + this.originalStates.clear(); // Reset to default message this.messageElement.textContent = this.defaultMessage; } } -const loadingOverlay = new LoadingOverlay(); - -// Add a flag to prevent auto-save during programmatic updates -let isSettingValues = false; - // Handle status messages with better color management function showStatus(message, type = 'info') { const statusEl = document.getElementById('status-message'); statusEl.textContent = message; - + // Remove all status classes const statusClasses = ['hidden', 'bg-green-100', 'bg-red-100', 'bg-blue-100', 'text-green-800', 'text-red-800', 'text-blue-800']; statusEl.classList.remove(...statusClasses); - + // Add appropriate classes based on type const typeMap = { success: ['bg-green-100', 'text-green-800'], error: ['bg-red-100', 'text-red-800'], info: ['bg-blue-100', 'text-blue-800'] }; - + statusEl.classList.add(...(typeMap[type] || typeMap.info)); - + // Also log to console appendToConsole(message, type === 'error' ? 'stderr' : 'info'); - + setTimeout(() => statusEl.classList.add('hidden'), 5000); } // Get form values using field mappings function getFormValues() { const config = {}; - + for (const [fieldName, fieldConfig] of Object.entries(CONFIG_FIELDS)) { const element = document.getElementById(fieldName); if (!element) continue; - + switch (fieldConfig.type) { case 'boolean': config[fieldName] = element.checked ? 1 : 0; break; case 'number': - config[fieldName] = parseInt(element.value) || fieldConfig.default; + const numValue = parseInt(element.value); + config[fieldName] = isNaN(numValue) ? fieldConfig.default : numValue; break; case 'text': config[fieldName] = element.value || fieldConfig.default; @@ -136,20 +148,20 @@ function getFormValues() { config[fieldName] = element.value || fieldConfig.default; } } - + return config; } // Set form values using field mappings function setFormValues(config) { isSettingValues = true; // Disable auto-save temporarily - + for (const [fieldName, fieldConfig] of Object.entries(CONFIG_FIELDS)) { const element = document.getElementById(fieldName); if (!element) continue; - + const value = config[fieldName] ?? fieldConfig.default; - + switch (fieldConfig.type) { case 'boolean': element.checked = value === 1; @@ -161,7 +173,7 @@ function setFormValues(config) { element.value = value; } } - + // Handle use_builtin toggle state const useBuiltin = config.use_builtin === 1; const customChatboxInputs = ['block_width', 'num_blocks', 'rows', 'cols']; @@ -176,53 +188,54 @@ function setFormValues(config) { } } }); - + + // Update volume display + if (config.volume !== undefined) { + const volumePercent = Math.round(config.volume); + document.getElementById('volume-display').textContent = `${volumePercent}%`; + } + isSettingValues = false; // Re-enable auto-save } -// Console management -const consoleContent = document.getElementById('console-content'); -const MAX_CONSOLE_LINES = 512; -let consoleLineCount = 0; - function appendToConsole(message, type = 'stdout') { const timestamp = new Date().toLocaleTimeString(); const timestampSpan = document.createElement('span'); timestampSpan.className = 'console-timestamp'; timestampSpan.textContent = `[${timestamp}] `; - + const messageSpan = document.createElement('span'); messageSpan.className = `console-${type}`; messageSpan.textContent = message; - + const lineDiv = document.createElement('div'); lineDiv.appendChild(timestampSpan); lineDiv.appendChild(messageSpan); - + consoleContent.appendChild(lineDiv); consoleLineCount++; - + // Remove old lines if we exceed the limit if (consoleLineCount > MAX_CONSOLE_LINES) { // Calculate how many lines to remove (remove 10% to avoid frequent trimming) const linesToRemove = Math.floor(MAX_CONSOLE_LINES * 0.1); - + // Remove the oldest lines for (let i = 0; i < linesToRemove; i++) { if (consoleContent.firstChild) { consoleContent.removeChild(consoleContent.firstChild); } } - + consoleLineCount -= linesToRemove; - + // Add a notice that lines were trimmed const trimNotice = document.createElement('div'); trimNotice.className = 'console-info'; trimNotice.innerHTML = '<span class="console-timestamp">[System] </span><span class="console-info">... older lines removed to maintain performance ...</span>'; consoleContent.insertBefore(trimNotice, consoleContent.firstChild); } - + // Auto-scroll to bottom const pythonConsole = document.getElementById('python-console'); pythonConsole.scrollTop = pythonConsole.scrollHeight; @@ -242,24 +255,20 @@ async function handleAsyncAction(actionName, actionFn) { } } -// Auto-save functionality with debouncing -let saveTimeout; -const SAVE_DELAY = 500; - async function autoSaveConfig() { if (isSettingValues) return; - + clearTimeout(saveTimeout); saveTimeout = setTimeout(async () => { try { const config = getFormValues(); await window.electronAPI.saveConfig(config); showStatus('Configuration saved', 'success'); - + // Restart process if running - if (!buttonManager.buttons.stop.disabled) { + if (isProcessRunning) { appendToConsole('Restarting process with new configuration...', 'info'); - + try { await window.electronAPI.stopProcess(); await new Promise(resolve => setTimeout(resolve, 1000)); @@ -281,9 +290,9 @@ async function autoSaveConfig() { function setupAutoSave() { const form = document.getElementById('config-form'); const inputs = form.querySelectorAll('input, select, textarea'); - + inputs.forEach(input => { - const eventType = input.type === 'checkbox' ? 'change' : + const eventType = input.type === 'checkbox' ? 'change' : (input.type === 'number' || input.type === 'text' || input.tagName === 'TEXTAREA') ? 'input' : 'change'; input.addEventListener(eventType, autoSaveConfig); }); @@ -292,7 +301,7 @@ function setupAutoSave() { // Microphone loading async function loadMicrophones() { const microphoneSelect = document.getElementById('microphone'); - + try { // Check/install requirements during startup appendToConsole('Checking virtual environment and requirements...', 'info'); @@ -305,15 +314,15 @@ async function loadMicrophones() { appendToConsole('Loading available microphones...', 'info'); const microphones = await window.electronAPI.getMicrophones(); - + microphoneSelect.innerHTML = ''; - + if (microphones.length === 0) { microphoneSelect.innerHTML = '<option value="" disabled>No microphones found</option>'; appendToConsole('No microphones found', 'stderr'); return; } - + appendToConsole(`Found ${microphones.length} microphone(s)`, 'info'); microphones.forEach(mic => { const option = document.createElement('option'); @@ -322,7 +331,7 @@ async function loadMicrophones() { microphoneSelect.appendChild(option); appendToConsole(` - ${mic.name} (Device ${mic.index})`, 'stdout'); }); - + // Restore previously selected microphone try { const config = await window.electronAPI.loadConfig(); @@ -332,7 +341,7 @@ async function loadMicrophones() { } catch (error) { // Ignore config load errors here } - + } catch (error) { appendToConsole(`Failed to load microphones: ${error.message}`, 'stderr'); microphoneSelect.innerHTML = '<option value="" disabled>Error loading microphones</option>'; @@ -345,7 +354,7 @@ function setupEventHandlers() { document.getElementById('toggle-advanced').addEventListener('click', () => { const advancedSettings = document.getElementById('advanced-settings'); const chevron = document.getElementById('chevron'); - + if (advancedSettings.classList.contains('hidden')) { advancedSettings.classList.remove('hidden'); chevron.classList.add('rotate-90'); @@ -354,12 +363,12 @@ function setupEventHandlers() { chevron.classList.remove('rotate-90'); } }); - + // Use builtin chatbox toggle document.getElementById('use_builtin').addEventListener('change', (e) => { const customChatboxInputs = ['block_width', 'num_blocks', 'rows', 'cols']; const isBuiltin = e.target.checked; - + customChatboxInputs.forEach(inputId => { const input = document.getElementById(inputId); if (input) { @@ -372,7 +381,13 @@ function setupEventHandlers() { } }); }); - + + // Volume slider update + document.getElementById('volume').addEventListener('input', (e) => { + const volumePercent = Math.round(e.target.value); + document.getElementById('volume-display').textContent = `${volumePercent}%`; + }); + // Setup virtual environment document.getElementById('setup-venv').addEventListener('click', async () => { loadingOverlay.show('Setting up virtual environment - please wait...'); // Show overlay with custom message @@ -385,7 +400,7 @@ function setupEventHandlers() { loadingOverlay.hide(); // Always hide overlay when done } }); - + // Reset virtual environment document.getElementById('reset-venv').addEventListener('click', async () => { loadingOverlay.show('Resetting virtual environment - please wait...'); // Show overlay with custom message @@ -397,33 +412,33 @@ function setupEventHandlers() { loadingOverlay.hide(); // Always hide overlay when done } }); - + // Reset configuration document.getElementById('reset-config').addEventListener('click', async () => { const confirmReset = confirm('Are you sure you want to reset all settings to defaults? This cannot be undone.'); if (!confirmReset) return; - + try { // Stop process if running - const wasRunning = !buttonManager.buttons.stop.disabled; + const wasRunning = isProcessRunning; if (wasRunning) { appendToConsole('Stopping process before resetting configuration...', 'info'); await window.electronAPI.stopProcess(); buttonManager.setProcessStopped(); await new Promise(resolve => setTimeout(resolve, 500)); } - + // Reset configuration appendToConsole('Resetting configuration to defaults...', 'info'); const result = await window.electronAPI.resetConfig(); - + // Reload configuration with defaults const config = await window.electronAPI.loadConfig(); setFormValues(config); - + showStatus(result.message, 'success'); appendToConsole('Configuration reset successfully', 'info'); - + // Restart process if it was running if (wasRunning) { appendToConsole('Restarting process with default configuration...', 'info'); @@ -436,18 +451,18 @@ function setupEventHandlers() { appendToConsole(`Failed to reset configuration: ${error.message}`, 'stderr'); } }); - + // Refresh microphones document.getElementById('refresh-microphones').addEventListener('click', async () => { await buttonManager.withButtonLoading('refreshMicrophones', async () => { await loadMicrophones(); }); }); - + // Start process document.getElementById('start-process').addEventListener('click', async () => { buttonManager.setState('start', true); - + try { // The installRequirements function will now check if venv is set up. loadingOverlay.show('Verifying environment setup - please wait...'); // Show overlay with custom message @@ -457,7 +472,7 @@ function setupEventHandlers() { } finally { loadingOverlay.hide(); // Always hide overlay when done } - + await window.electronAPI.startProcess(); buttonManager.setProcessRunning(); appendToConsole('Process started successfully', 'info'); @@ -466,11 +481,11 @@ function setupEventHandlers() { buttonManager.setState('start', false); } }); - + // Stop process document.getElementById('stop-process').addEventListener('click', async () => { buttonManager.setState('stop', true); - + try { await window.electronAPI.stopProcess(); appendToConsole('Process stop initiated', 'info'); @@ -479,7 +494,7 @@ function setupEventHandlers() { buttonManager.setState('stop', false); } }); - + // Listen for process stopped event window.electronAPI.onProcessStopped(() => { buttonManager.setProcessStopped(); @@ -489,12 +504,15 @@ function setupEventHandlers() { // Initialize application window.addEventListener('load', async () => { appendToConsole('TaSTT Configuration UI initialized', 'info'); - + + loadingOverlay = new LoadingOverlay(); + buttonManager = new ButtonManager(); + // Set up Python output listener first so we capture all output window.electronAPI.onPythonOutput((data) => { appendToConsole(data.message, data.type); }); - + // Load configuration try { const config = await window.electronAPI.loadConfig(); @@ -503,11 +521,11 @@ window.addEventListener('load', async () => { } catch (error) { appendToConsole(`Failed to load configuration: ${error.message}`, 'stderr'); } - + // Load microphones await loadMicrophones(); - + // Setup event handlers and auto-save setupEventHandlers(); setupAutoSave(); -});
\ No newline at end of file +}); |
