summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/hi.py45
-rw-r--r--app/requirements.txt1
-rw-r--r--app/stt.py62
-rw-r--r--config.yaml8
-rw-r--r--ui/config-schema.js2
-rw-r--r--ui/index.html13
-rw-r--r--ui/index.js17
-rw-r--r--ui/preload.js1
-rw-r--r--ui/renderer.js198
9 files changed, 196 insertions, 151 deletions
diff --git a/app/hi.py b/app/hi.py
index 1297b37..bb09418 100644
--- a/app/hi.py
+++ b/app/hi.py
@@ -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
diff --git a/app/stt.py b/app/stt.py
index c1f4836..79ab0d1 100644
--- a/app/stt.py
+++ b/app/stt.py
@@ -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
+});