"""PySide6 settings dialog for configuring the application.""" from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QLineEdit, QComboBox, QCheckBox, QSlider, QPushButton, QMessageBox, QGroupBox, QScrollArea, QWidget, QFileDialog, QColorDialog ) from PySide6.QtCore import Qt from PySide6.QtGui import QScreen, QFontDatabase, QColor from typing import Callable, List, Tuple class SettingsDialog(QDialog): """Dialog window for application settings using PySide6.""" def __init__( self, parent, config, audio_devices: List[Tuple[int, str]], compute_devices: List[Tuple[str, str]], on_save: Callable = None ): """ Initialize settings dialog. Args: parent: Parent window config: Configuration object audio_devices: List of (device_index, device_name) tuples compute_devices: List of (device_id, device_description) tuples on_save: Callback function when settings are saved """ super().__init__(parent) self.config = config self.audio_devices = audio_devices self.compute_devices = compute_devices self.on_save = on_save # Window configuration self.setWindowTitle("Settings") self.setModal(True) # Calculate size based on screen size (80% of screen height, max 900px) screen = QScreen.availableGeometry(parent.screen() if parent else None) max_height = min(int(screen.height() * 0.8), 900) self.setMinimumSize(700, 500) self.resize(700, max_height) self._create_widgets() self._load_current_settings() def _create_widgets(self): """Create all settings widgets.""" # Main layout for the dialog (contains scroll area + buttons) main_layout = QVBoxLayout() main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) self.setLayout(main_layout) # Create scroll area for settings content scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) # Create content widget for scroll area content_widget = QWidget() content_layout = QVBoxLayout() content_layout.setSpacing(15) # Add spacing between groups content_layout.setContentsMargins(20, 20, 20, 20) # Add padding content_widget.setLayout(content_layout) scroll_area.setWidget(content_widget) # Add scroll area to main layout main_layout.addWidget(scroll_area) # User Settings Group user_group = QGroupBox("User Settings") user_layout = QFormLayout() user_layout.setSpacing(10) self.name_input = QLineEdit() self.name_input.setToolTip("Your display name shown in transcriptions and sent to multi-user server") user_layout.addRow("Display Name:", self.name_input) user_group.setLayout(user_layout) content_layout.addWidget(user_group) # Audio Settings Group audio_group = QGroupBox("Audio Settings") audio_layout = QFormLayout() audio_layout.setSpacing(10) self.audio_device_combo = QComboBox() self.audio_device_combo.setToolTip("Select your microphone or audio input device") device_names = [name for _, name in self.audio_devices] self.audio_device_combo.addItems(device_names) audio_layout.addRow("Input Device:", self.audio_device_combo) audio_group.setLayout(audio_layout) content_layout.addWidget(audio_group) # Transcription Settings Group transcription_group = QGroupBox("Transcription Settings") transcription_layout = QFormLayout() transcription_layout.setSpacing(10) self.model_combo = QComboBox() self.model_combo.setToolTip( "Whisper model size:\n" "• tiny/tiny.en - Fastest, lowest quality\n" "• base/base.en - Good balance for real-time\n" "• small/small.en - Better quality, slower\n" "• medium/medium.en - High quality, much slower\n" "• large-v1/v2/v3 - Best quality, very slow\n" "(.en models are English-only, faster)" ) self.model_combo.addItems([ "tiny", "tiny.en", "base", "base.en", "small", "small.en", "medium", "medium.en", "large-v1", "large-v2", "large-v3" ]) transcription_layout.addRow("Model Size:", self.model_combo) self.compute_device_combo = QComboBox() self.compute_device_combo.setToolTip("Hardware to use for transcription (GPU is 5-10x faster than CPU)") device_descs = [desc for _, desc in self.compute_devices] self.compute_device_combo.addItems(device_descs) transcription_layout.addRow("Compute Device:", self.compute_device_combo) self.compute_type_combo = QComboBox() self.compute_type_combo.setToolTip( "Precision for model calculations:\n" "• default - Automatic selection\n" "• int8 - Fastest, uses less memory\n" "• float16 - GPU only, good balance\n" "• float32 - Slowest, best quality" ) self.compute_type_combo.addItems(["default", "int8", "float16", "float32"]) transcription_layout.addRow("Compute Type:", self.compute_type_combo) self.lang_combo = QComboBox() self.lang_combo.setToolTip("Language to transcribe (auto-detect or specific language)") self.lang_combo.addItems(["auto", "en", "es", "fr", "de", "it", "pt", "ru", "zh", "ja", "ko"]) transcription_layout.addRow("Language:", self.lang_combo) self.beam_size_combo = QComboBox() self.beam_size_combo.setToolTip( "Beam search size for decoding:\n" "• Higher = Better quality but slower\n" "• 1 = Greedy (fastest)\n" "• 5 = Good balance (recommended)\n" "• 10 = Best quality (slowest)" ) self.beam_size_combo.addItems(["1", "2", "3", "5", "8", "10"]) transcription_layout.addRow("Beam Size:", self.beam_size_combo) transcription_group.setLayout(transcription_layout) content_layout.addWidget(transcription_group) # Realtime Preview Group realtime_group = QGroupBox("Realtime Preview (Optional)") realtime_layout = QFormLayout() realtime_layout.setSpacing(10) self.realtime_enabled_check = QCheckBox() self.realtime_enabled_check.setToolTip( "Enable live preview transcriptions using a faster model\n" "Shows instant results while processing final transcription in background" ) realtime_layout.addRow("Enable Preview:", self.realtime_enabled_check) self.realtime_model_combo = QComboBox() self.realtime_model_combo.setToolTip("Faster model for instant preview (tiny or base recommended)") self.realtime_model_combo.addItems(["tiny", "tiny.en", "base", "base.en"]) realtime_layout.addRow("Preview Model:", self.realtime_model_combo) self.realtime_pause_input = QLineEdit() self.realtime_pause_input.setToolTip( "Seconds between preview updates:\n" "• Lower values = More responsive, more frequent updates\n" "• Higher values = Less CPU usage, updates less often\n" "• 0.1 is recommended for real-time streaming\n" "• Try 0.05 for even faster updates" ) realtime_layout.addRow("Preview Update Interval (s):", self.realtime_pause_input) realtime_group.setLayout(realtime_layout) content_layout.addWidget(realtime_group) # VAD (Voice Activity Detection) Group vad_group = QGroupBox("Voice Activity Detection") vad_layout = QFormLayout() vad_layout.setSpacing(10) # Silero VAD sensitivity slider silero_layout = QHBoxLayout() self.silero_slider = QSlider(Qt.Horizontal) self.silero_slider.setMinimum(0) self.silero_slider.setMaximum(100) self.silero_slider.setValue(40) self.silero_slider.valueChanged.connect(self._update_silero_label) self.silero_slider.setToolTip( "Silero VAD sensitivity (0.0-1.0):\n" "• Lower values = More sensitive (detects quieter speech)\n" "• Higher values = Less sensitive (requires louder speech)\n" "• 0.4 is recommended for most environments" ) silero_layout.addWidget(self.silero_slider) self.silero_label = QLabel("0.4") silero_layout.addWidget(self.silero_label) vad_layout.addRow("Silero Sensitivity:", silero_layout) # WebRTC VAD sensitivity self.webrtc_combo = QComboBox() self.webrtc_combo.setToolTip( "WebRTC VAD aggressiveness:\n" "• 0 = Least aggressive (detects more speech)\n" "• 3 = Most aggressive (filters more noise)\n" "• 3 is recommended for noisy environments" ) self.webrtc_combo.addItems(["0 (most sensitive)", "1", "2", "3 (least sensitive)"]) vad_layout.addRow("WebRTC Sensitivity:", self.webrtc_combo) self.silero_onnx_check = QCheckBox("Enable (2-3x faster)") self.silero_onnx_check.setToolTip( "Use ONNX runtime for Silero VAD:\n" "• 2-3x faster processing\n" "• 30% lower CPU usage\n" "• Same quality\n" "• Recommended: Enabled" ) vad_layout.addRow("ONNX Acceleration:", self.silero_onnx_check) vad_group.setLayout(vad_layout) content_layout.addWidget(vad_group) # Advanced Timing Group timing_group = QGroupBox("Advanced Timing Settings") timing_layout = QFormLayout() timing_layout.setSpacing(10) self.post_silence_input = QLineEdit() self.post_silence_input.setToolTip( "Seconds of silence after speech before finalizing transcription:\n" "• Lower = Faster response but may cut off slow speech\n" "• Higher = More complete sentences but slower\n" "• 0.3s is recommended for real-time streaming" ) timing_layout.addRow("Post-Speech Silence (s):", self.post_silence_input) self.min_recording_input = QLineEdit() self.min_recording_input.setToolTip( "Minimum length of audio to transcribe (in seconds):\n" "• Filters out very short sounds/noise\n" "• 0.5s is recommended" ) timing_layout.addRow("Min Recording Length (s):", self.min_recording_input) self.pre_buffer_input = QLineEdit() self.pre_buffer_input.setToolTip( "Buffer before speech detection (in seconds):\n" "• Captures the start of words that triggered VAD\n" "• Prevents cutting off the first word\n" "• 0.2s is recommended" ) timing_layout.addRow("Pre-Recording Buffer (s):", self.pre_buffer_input) self.continuous_mode_check = QCheckBox() self.continuous_mode_check.setToolTip( "Fast Speaker Mode:\n" "• For speakers who talk quickly without pauses\n" "• Reduces silence detection thresholds\n" "• Produces more frequent transcription outputs\n" "• May result in more fragmented sentences" ) timing_layout.addRow("Fast Speaker Mode:", self.continuous_mode_check) timing_group.setLayout(timing_layout) content_layout.addWidget(timing_group) # Display Settings Group display_group = QGroupBox("Display Settings") display_layout = QFormLayout() display_layout.setSpacing(10) self.timestamps_check = QCheckBox() self.timestamps_check.setToolTip("Show timestamp before each transcription line") display_layout.addRow("Show Timestamps:", self.timestamps_check) self.maxlines_input = QLineEdit() self.maxlines_input.setToolTip( "Maximum number of transcription lines to display:\n" "• Older lines are automatically removed\n" "• Set to 50-100 for OBS to prevent scroll bars" ) display_layout.addRow("Max Lines:", self.maxlines_input) # Font source selector (shared for local display and server sync) self.display_font_source_combo = QComboBox() self.display_font_source_combo.addItems(["System Font", "Web-Safe", "Google Font", "Custom File"]) self.display_font_source_combo.setToolTip( "Choose font for local display and server sync:\n" "• System Font - Local only (won't work with server sync)\n" "• Web-Safe - Universal fonts (Arial, Comic Sans, etc.)\n" "• Google Font - Free fonts from fonts.google.com\n" "• Custom File - Upload your own font file" ) self.display_font_source_combo.currentTextChanged.connect(self._on_display_font_source_changed) display_layout.addRow("Font Source:", self.display_font_source_combo) # System font selector self.font_family_combo = QComboBox() self.font_family_combo.setToolTip("Font family for transcription display (system fonts)") self.font_family_combo.setEditable(True) self.font_family_combo.setMaxVisibleItems(20) system_fonts = QFontDatabase.families() common_fonts = ["Courier", "Arial", "Times New Roman", "Consolas", "Monaco", "Monospace"] ordered_fonts = [] for font in common_fonts: if font in system_fonts: ordered_fonts.append(font) for font in sorted(system_fonts): if font not in ordered_fonts: ordered_fonts.append(font) self.font_family_combo.addItems(ordered_fonts) display_layout.addRow("System Font:", self.font_family_combo) # Web-safe font selector for display self.display_websafe_combo = QComboBox() display_websafe_fonts = [ "Arial", "Arial Black", "Comic Sans MS", "Courier New", "Georgia", "Impact", "Lucida Console", "Lucida Sans Unicode", "Palatino Linotype", "Tahoma", "Times New Roman", "Trebuchet MS", "Verdana" ] self.display_websafe_combo.addItems(display_websafe_fonts) self.display_websafe_combo.setToolTip("Web-safe fonts work on all systems") display_layout.addRow("Web-Safe Font:", self.display_websafe_combo) # Google Font selector for display self.display_google_font_combo = QComboBox() display_google_fonts = [ "Roboto", "Open Sans", "Lato", "Montserrat", "Poppins", "Nunito", "Raleway", "Ubuntu", "Rubik", "Work Sans", "Inter", "Outfit", "Quicksand", "Comfortaa", "Varela Round", "Playfair Display", "Merriweather", "Lora", "PT Serif", "Crimson Text", "Roboto Mono", "Source Code Pro", "Fira Code", "JetBrains Mono", "IBM Plex Mono", "Bebas Neue", "Oswald", "Righteous", "Bangers", "Permanent Marker", "Pacifico", "Lobster", "Dancing Script", "Caveat", "Satisfy" ] self.display_google_font_combo.addItems(display_google_fonts) self.display_google_font_combo.setToolTip("Select a Google Font for display") display_layout.addRow("Google Font:", self.display_google_font_combo) # Custom font file picker (for server sync upload) custom_font_layout = QHBoxLayout() self.display_custom_font_input = QLineEdit() self.display_custom_font_input.setPlaceholderText("No file selected") self.display_custom_font_input.setReadOnly(True) self.display_custom_font_input.setToolTip( "Select a font file to use:\n" "• Supports .ttf, .otf, .woff, .woff2 files\n" "• Font is uploaded to server automatically when using Server Sync" ) custom_font_layout.addWidget(self.display_custom_font_input) self.display_custom_font_browse = QPushButton("Browse...") self.display_custom_font_browse.clicked.connect(self._browse_display_custom_font) custom_font_layout.addWidget(self.display_custom_font_browse) display_layout.addRow("Custom Font File:", custom_font_layout) self.font_size_input = QLineEdit() self.font_size_input.setToolTip("Font size in pixels (12-20 recommended)") display_layout.addRow("Font Size:", self.font_size_input) self.fade_seconds_input = QLineEdit() self.fade_seconds_input.setToolTip( "Seconds before transcriptions fade out:\n" "• 0 = Never fade (all transcriptions stay visible)\n" "• 10-30 = Good for OBS overlays" ) display_layout.addRow("Fade After (seconds):", self.fade_seconds_input) # Color settings color_label = QLabel("Color Settings") color_label.setStyleSheet("font-weight: bold; margin-top: 10px;") display_layout.addRow("", color_label) # User name color picker user_color_layout = QHBoxLayout() self.user_color_button = QPushButton() self.user_color_button.setFixedSize(100, 30) self.user_color_button.setCursor(Qt.PointingHandCursor) self.user_color_button.setToolTip("Click to change user name color") self.user_color_button.clicked.connect(self._pick_user_color) user_color_layout.addWidget(self.user_color_button) self.user_color_hex = QLabel("#4CAF50") self.user_color_hex.setStyleSheet("font-family: monospace;") user_color_layout.addWidget(self.user_color_hex) user_color_layout.addStretch() display_layout.addRow("User Name Color:", user_color_layout) # Text color picker text_color_layout = QHBoxLayout() self.text_color_button = QPushButton() self.text_color_button.setFixedSize(100, 30) self.text_color_button.setCursor(Qt.PointingHandCursor) self.text_color_button.setToolTip("Click to change text color") self.text_color_button.clicked.connect(self._pick_text_color) text_color_layout.addWidget(self.text_color_button) self.text_color_hex = QLabel("#FFFFFF") self.text_color_hex.setStyleSheet("font-family: monospace;") text_color_layout.addWidget(self.text_color_hex) text_color_layout.addStretch() display_layout.addRow("Text Color:", text_color_layout) # Background color picker bg_color_layout = QHBoxLayout() self.bg_color_button = QPushButton() self.bg_color_button.setFixedSize(100, 30) self.bg_color_button.setCursor(Qt.PointingHandCursor) self.bg_color_button.setToolTip("Click to change background color (with transparency)") self.bg_color_button.clicked.connect(self._pick_background_color) bg_color_layout.addWidget(self.bg_color_button) self.bg_color_hex = QLabel("#000000B3") self.bg_color_hex.setStyleSheet("font-family: monospace;") bg_color_layout.addWidget(self.bg_color_hex) bg_color_layout.addStretch() display_layout.addRow("Background Color:", bg_color_layout) display_group.setLayout(display_layout) content_layout.addWidget(display_group) # Initially show only System Font (default) self._on_display_font_source_changed("System Font") # Server Sync Group server_group = QGroupBox("Multi-User Server Sync (Optional)") server_layout = QFormLayout() server_layout.setSpacing(10) self.server_enabled_check = QCheckBox() self.server_enabled_check.setToolTip( "Enable multi-user server synchronization:\n" "• Share transcriptions with other users in real-time\n" "• Requires Node.js server (see server/nodejs/README.md)\n" "• All users in same room see combined transcriptions" ) server_layout.addRow("Enable Server Sync:", self.server_enabled_check) self.server_url_input = QLineEdit() self.server_url_input.setPlaceholderText("http://your-server:3000/api/send") self.server_url_input.setToolTip("URL of your Node.js multi-user server's /api/send endpoint") server_layout.addRow("Server URL:", self.server_url_input) self.server_room_input = QLineEdit() self.server_room_input.setPlaceholderText("my-room-name") self.server_room_input.setToolTip( "Room name for multi-user sessions:\n" "• All users with same room name see each other's transcriptions\n" "• Use unique room names for different groups/streams" ) server_layout.addRow("Room Name:", self.server_room_input) self.server_passphrase_input = QLineEdit() self.server_passphrase_input.setEchoMode(QLineEdit.Password) self.server_passphrase_input.setPlaceholderText("shared-secret") self.server_passphrase_input.setToolTip( "Shared secret passphrase for room access:\n" "• All users must use same passphrase to join room\n" "• Prevents unauthorized access to your transcriptions" ) server_layout.addRow("Passphrase:", self.server_passphrase_input) # Note about font settings font_note = QLabel("Font settings are in Display Settings above") font_note.setStyleSheet("color: #666; font-style: italic;") server_layout.addRow("", font_note) server_group.setLayout(server_layout) content_layout.addWidget(server_group) # Remote Processing Group remote_group = QGroupBox("Remote Processing (GPU Offload)") remote_layout = QFormLayout() remote_layout.setSpacing(10) self.remote_enabled_check = QCheckBox() self.remote_enabled_check.setToolTip( "Enable remote transcription processing:\n" "• Offload transcription to a GPU-equipped server\n" "• Reduces local CPU/GPU usage\n" "• Requires running the remote transcription service" ) remote_layout.addRow("Enable Remote Processing:", self.remote_enabled_check) self.remote_url_input = QLineEdit() self.remote_url_input.setPlaceholderText("ws://your-server:8765/ws/transcribe") self.remote_url_input.setToolTip( "WebSocket URL of the remote transcription service:\n" "• Format: ws://host:port/ws/transcribe\n" "• Use wss:// for secure connections" ) remote_layout.addRow("Server URL:", self.remote_url_input) self.remote_api_key_input = QLineEdit() self.remote_api_key_input.setEchoMode(QLineEdit.Password) self.remote_api_key_input.setPlaceholderText("your-api-key") self.remote_api_key_input.setToolTip( "API key for authentication with the remote service" ) remote_layout.addRow("API Key:", self.remote_api_key_input) self.remote_fallback_check = QCheckBox("Enable") self.remote_fallback_check.setChecked(True) self.remote_fallback_check.setToolTip( "Fall back to local transcription if remote service is unavailable" ) remote_layout.addRow("Fallback to Local:", self.remote_fallback_check) remote_group.setLayout(remote_layout) content_layout.addWidget(remote_group) # Updates Group updates_group = QGroupBox("Software Updates") updates_layout = QFormLayout() updates_layout.setSpacing(10) self.update_auto_check = QCheckBox("Enable") self.update_auto_check.setToolTip( "Automatically check for updates when the application starts" ) updates_layout.addRow("Check on Startup:", self.update_auto_check) self.check_updates_button = QPushButton("Check for Updates Now") self.check_updates_button.setToolTip( "Manually check for available updates" ) self.check_updates_button.clicked.connect(self._check_for_updates_now) updates_layout.addRow("", self.check_updates_button) updates_group.setLayout(updates_layout) content_layout.addWidget(updates_group) # Add stretch to push everything to the top content_layout.addStretch() # Buttons (outside scroll area, always visible at bottom) button_container = QWidget() button_layout = QHBoxLayout() button_layout.setContentsMargins(20, 10, 20, 10) button_layout.addStretch() self.cancel_button = QPushButton("Cancel") self.cancel_button.clicked.connect(self.reject) button_layout.addWidget(self.cancel_button) self.save_button = QPushButton("Save") self.save_button.clicked.connect(self._save_settings) self.save_button.setDefault(True) button_layout.addWidget(self.save_button) button_container.setLayout(button_layout) main_layout.addWidget(button_container) def _update_silero_label(self, value): """Update the Silero sensitivity label.""" self.silero_label.setText(f"{value / 100:.2f}") def _open_fonts_folder(self): """Open the custom fonts folder in the system file manager.""" import subprocess import sys from pathlib import Path fonts_dir = self.config.fonts_dir # Ensure the folder exists fonts_dir.mkdir(parents=True, exist_ok=True) # Open the folder in the system file manager if sys.platform == 'win32': subprocess.run(['explorer', str(fonts_dir)]) elif sys.platform == 'darwin': subprocess.run(['open', str(fonts_dir)]) else: # Linux subprocess.run(['xdg-open', str(fonts_dir)]) def _on_display_font_source_changed(self, source: str): """Show/hide display font inputs based on selected source.""" # Hide all font-specific inputs first self.font_family_combo.setVisible(False) self.display_websafe_combo.setVisible(False) self.display_google_font_combo.setVisible(False) self.display_custom_font_input.setVisible(False) self.display_custom_font_browse.setVisible(False) # Find the form layout rows and hide/show labels too parent = self.display_font_source_combo.parent() display_layout = parent.layout() if parent else None if display_layout and hasattr(display_layout, 'rowCount'): for i in range(display_layout.rowCount()): label = display_layout.itemAt(i, QFormLayout.LabelRole) field = display_layout.itemAt(i, QFormLayout.FieldRole) if label and field: label_widget = label.widget() if label_widget: label_text = label_widget.text() if label_text == "System Font:": label_widget.setVisible(source == "System Font") elif label_text == "Web-Safe Font:": label_widget.setVisible(source == "Web-Safe") elif label_text == "Google Font:": label_widget.setVisible(source == "Google Font") elif label_text == "Custom Font File:": label_widget.setVisible(source == "Custom File") # Show the relevant input if source == "System Font": self.font_family_combo.setVisible(True) elif source == "Web-Safe": self.display_websafe_combo.setVisible(True) elif source == "Google Font": self.display_google_font_combo.setVisible(True) elif source == "Custom File": self.display_custom_font_input.setVisible(True) self.display_custom_font_browse.setVisible(True) def _browse_display_custom_font(self): """Browse for a custom font file.""" file_path, _ = QFileDialog.getOpenFileName( self, "Select Font File", "", "Font Files (*.ttf *.otf *.woff *.woff2);;All Files (*)" ) if file_path: self.display_custom_font_input.setText(file_path) def _update_color_button_style(self, button: QPushButton, color_hex: str): """Update a color button's background to show the selected color.""" # Handle colors with alpha (8-char hex) if len(color_hex) == 9: # #RRGGBBAA # For display, we just show the color (alpha is visible through the button style) rgb = color_hex[:7] alpha_hex = color_hex[7:9] alpha = int(alpha_hex, 16) / 255 button.setStyleSheet( f"background-color: {rgb}; " f"border: 2px solid #888; " f"border-radius: 4px; " f"opacity: {alpha};" ) else: button.setStyleSheet( f"background-color: {color_hex}; " f"border: 2px solid #888; " f"border-radius: 4px;" ) def _pick_user_color(self): """Open color dialog for user name color.""" current_color = QColor(self.user_color_hex.text()) color = QColorDialog.getColor(current_color, self, "Select User Name Color") if color.isValid(): hex_color = color.name() self.user_color_hex.setText(hex_color) self._update_color_button_style(self.user_color_button, hex_color) def _pick_text_color(self): """Open color dialog for text color.""" current_color = QColor(self.text_color_hex.text()) color = QColorDialog.getColor(current_color, self, "Select Text Color") if color.isValid(): hex_color = color.name() self.text_color_hex.setText(hex_color) self._update_color_button_style(self.text_color_button, hex_color) def _pick_background_color(self): """Open color dialog for background color (with alpha support).""" current_hex = self.bg_color_hex.text() current_color = QColor(current_hex[:7] if len(current_hex) > 7 else current_hex) if len(current_hex) == 9: current_color.setAlpha(int(current_hex[7:9], 16)) color = QColorDialog.getColor( current_color, self, "Select Background Color", QColorDialog.ShowAlphaChannel ) if color.isValid(): # Include alpha in hex format: #RRGGBBAA hex_color = f"{color.name()}{color.alpha():02X}" self.bg_color_hex.setText(hex_color) self._update_color_button_style(self.bg_color_button, hex_color) def _load_current_settings(self): """Load current settings from config.""" # User settings self.name_input.setText(self.config.get('user.name', 'User')) # Audio settings current_device = self.config.get('audio.input_device', 'default') for idx, (dev_idx, dev_name) in enumerate(self.audio_devices): if str(dev_idx) == current_device or (current_device == 'default' and idx == 0): self.audio_device_combo.setCurrentIndex(idx) break # Transcription settings model = self.config.get('transcription.model', 'base.en') self.model_combo.setCurrentText(model) current_compute = self.config.get('transcription.device', 'auto') for idx, (dev_id, dev_desc) in enumerate(self.compute_devices): if dev_id == current_compute or (current_compute == 'auto' and idx == 0): self.compute_device_combo.setCurrentIndex(idx) break compute_type = self.config.get('transcription.compute_type', 'default') self.compute_type_combo.setCurrentText(compute_type) lang = self.config.get('transcription.language', 'en') self.lang_combo.setCurrentText(lang) beam_size = self.config.get('transcription.beam_size', 5) self.beam_size_combo.setCurrentText(str(beam_size)) # Realtime preview self.realtime_enabled_check.setChecked(self.config.get('transcription.enable_realtime_transcription', False)) realtime_model = self.config.get('transcription.realtime_model', 'tiny.en') self.realtime_model_combo.setCurrentText(realtime_model) self.realtime_pause_input.setText(str(self.config.get('transcription.realtime_processing_pause', 0.1))) # VAD settings silero_sens = self.config.get('transcription.silero_sensitivity', 0.4) self.silero_slider.setValue(int(silero_sens * 100)) self._update_silero_label(int(silero_sens * 100)) webrtc_sens = self.config.get('transcription.webrtc_sensitivity', 3) self.webrtc_combo.setCurrentIndex(webrtc_sens) self.silero_onnx_check.setChecked(self.config.get('transcription.silero_use_onnx', True)) # Advanced timing self.post_silence_input.setText(str(self.config.get('transcription.post_speech_silence_duration', 0.3))) self.min_recording_input.setText(str(self.config.get('transcription.min_length_of_recording', 0.5))) self.pre_buffer_input.setText(str(self.config.get('transcription.pre_recording_buffer_duration', 0.2))) self.continuous_mode_check.setChecked(self.config.get('transcription.continuous_mode', False)) # Display settings self.timestamps_check.setChecked(self.config.get('display.show_timestamps', True)) self.maxlines_input.setText(str(self.config.get('display.max_lines', 100))) # Display font settings display_font_source = self.config.get('display.font_source', 'System Font') self.display_font_source_combo.setCurrentText(display_font_source) font_family = self.config.get('display.font_family', 'Courier') self.font_family_combo.setCurrentText(font_family) self.display_websafe_combo.setCurrentText(self.config.get('display.websafe_font', 'Arial')) display_google_font = self.config.get('display.google_font', 'Roboto') if display_google_font: self.display_google_font_combo.setCurrentText(display_google_font) self.display_custom_font_input.setText(self.config.get('display.custom_font_file', '')) self._on_display_font_source_changed(display_font_source) self.font_size_input.setText(str(self.config.get('display.font_size', 12))) self.fade_seconds_input.setText(str(self.config.get('display.fade_after_seconds', 10))) # Color settings user_color = self.config.get('display.user_color', '#4CAF50') self.user_color_hex.setText(user_color) self._update_color_button_style(self.user_color_button, user_color) text_color = self.config.get('display.text_color', '#FFFFFF') self.text_color_hex.setText(text_color) self._update_color_button_style(self.text_color_button, text_color) bg_color = self.config.get('display.background_color', '#000000B3') self.bg_color_hex.setText(bg_color) self._update_color_button_style(self.bg_color_button, bg_color) # Server sync settings self.server_enabled_check.setChecked(self.config.get('server_sync.enabled', False)) self.server_url_input.setText(self.config.get('server_sync.url', '')) self.server_room_input.setText(self.config.get('server_sync.room', 'default')) self.server_passphrase_input.setText(self.config.get('server_sync.passphrase', '')) # Remote processing settings self.remote_enabled_check.setChecked(self.config.get('remote_processing.enabled', False)) self.remote_url_input.setText(self.config.get('remote_processing.server_url', '')) self.remote_api_key_input.setText(self.config.get('remote_processing.api_key', '')) self.remote_fallback_check.setChecked(self.config.get('remote_processing.fallback_to_local', True)) # Update settings self.update_auto_check.setChecked(self.config.get('updates.auto_check', True)) def _save_settings(self): """Save settings to config.""" try: # User settings self.config.set('user.name', self.name_input.text()) # Audio settings selected_audio_idx = self.audio_device_combo.currentIndex() dev_idx, _ = self.audio_devices[selected_audio_idx] self.config.set('audio.input_device', str(dev_idx)) # Transcription settings self.config.set('transcription.model', self.model_combo.currentText()) selected_compute_idx = self.compute_device_combo.currentIndex() dev_id, _ = self.compute_devices[selected_compute_idx] self.config.set('transcription.device', dev_id) self.config.set('transcription.compute_type', self.compute_type_combo.currentText()) self.config.set('transcription.language', self.lang_combo.currentText()) self.config.set('transcription.beam_size', int(self.beam_size_combo.currentText())) # Realtime preview self.config.set('transcription.enable_realtime_transcription', self.realtime_enabled_check.isChecked()) self.config.set('transcription.realtime_model', self.realtime_model_combo.currentText()) self.config.set('transcription.realtime_processing_pause', float(self.realtime_pause_input.text())) # VAD settings self.config.set('transcription.silero_sensitivity', self.silero_slider.value() / 100.0) self.config.set('transcription.webrtc_sensitivity', self.webrtc_combo.currentIndex()) self.config.set('transcription.silero_use_onnx', self.silero_onnx_check.isChecked()) # Advanced timing self.config.set('transcription.post_speech_silence_duration', float(self.post_silence_input.text())) self.config.set('transcription.min_length_of_recording', float(self.min_recording_input.text())) self.config.set('transcription.pre_recording_buffer_duration', float(self.pre_buffer_input.text())) self.config.set('transcription.continuous_mode', self.continuous_mode_check.isChecked()) # Display settings self.config.set('display.show_timestamps', self.timestamps_check.isChecked()) max_lines = int(self.maxlines_input.text()) self.config.set('display.max_lines', max_lines) # Display font settings (also used for server sync) self.config.set('display.font_source', self.display_font_source_combo.currentText()) self.config.set('display.font_family', self.font_family_combo.currentText()) self.config.set('display.websafe_font', self.display_websafe_combo.currentText()) self.config.set('display.google_font', self.display_google_font_combo.currentText()) self.config.set('display.custom_font_file', self.display_custom_font_input.text()) font_size = int(self.font_size_input.text()) self.config.set('display.font_size', font_size) fade_seconds = int(self.fade_seconds_input.text()) self.config.set('display.fade_after_seconds', fade_seconds) # Color settings self.config.set('display.user_color', self.user_color_hex.text()) self.config.set('display.text_color', self.text_color_hex.text()) self.config.set('display.background_color', self.bg_color_hex.text()) # Server sync settings self.config.set('server_sync.enabled', self.server_enabled_check.isChecked()) self.config.set('server_sync.url', self.server_url_input.text()) self.config.set('server_sync.room', self.server_room_input.text()) self.config.set('server_sync.passphrase', self.server_passphrase_input.text()) # Remote processing settings self.config.set('remote_processing.enabled', self.remote_enabled_check.isChecked()) self.config.set('remote_processing.server_url', self.remote_url_input.text()) self.config.set('remote_processing.api_key', self.remote_api_key_input.text()) self.config.set('remote_processing.fallback_to_local', self.remote_fallback_check.isChecked()) # Update settings self.config.set('updates.auto_check', self.update_auto_check.isChecked()) # Call save callback (which will show the success message) if self.on_save: self.on_save() else: # Only show message if no callback QMessageBox.information(self, "Settings Saved", "Settings have been saved successfully!") self.accept() except ValueError as e: QMessageBox.critical(self, "Invalid Input", f"Please check your input values:\n{e}") except Exception as e: QMessageBox.critical(self, "Error", f"Failed to save settings:\n{e}") def _check_for_updates_now(self): """Manually check for updates.""" from version import __version__ from client.update_checker import UpdateChecker # Get settings from config (hardcoded defaults) gitea_url = self.config.get('updates.gitea_url', 'https://repo.anhonesthost.net') owner = self.config.get('updates.owner', 'streamer-tools') repo = self.config.get('updates.repo', 'local-transcription') # Disable button during check self.check_updates_button.setEnabled(False) self.check_updates_button.setText("Checking...") try: checker = UpdateChecker( current_version=__version__, gitea_url=gitea_url, owner=owner, repo=repo ) release_info, error = checker.check_for_update() if error: QMessageBox.warning(self, "Update Check Failed", f"Could not check for updates:\n{error}") elif release_info: # Update available msg = QMessageBox(self) msg.setWindowTitle("Update Available") msg.setIcon(QMessageBox.Information) msg.setText(f"A new version is available!\n\nCurrent: {__version__}\nNew: {release_info.version}") if release_info.release_notes: msg.setDetailedText(release_info.release_notes) msg.setStandardButtons(QMessageBox.Ok) msg.exec() else: QMessageBox.information( self, "No Updates", f"You are running the latest version ({__version__})." ) except Exception as e: QMessageBox.critical(self, "Error", f"Error checking for updates:\n{e}") finally: # Re-enable button self.check_updates_button.setEnabled(True) self.check_updates_button.setText("Check for Updates Now")