diff --git a/install.sh b/install.sh index 9f0cf2c..088ba7f 100755 --- a/install.sh +++ b/install.sh @@ -645,7 +645,8 @@ install_python_package() { # Function to verify package installation verify_package_installed() { local PKG_NAME="vocalinux" - python -c "import $PKG_NAME" 2>/dev/null + # Use venv python and set GI_TYPELIB_PATH for PyGObject + GI_TYPELIB_PATH=/usr/lib/girepository-1.0 "$VENV_DIR/bin/python" -c "import $PKG_NAME" 2>/dev/null return $? } @@ -782,18 +783,34 @@ VOSK_CONFIG # Clean up log file if installation was successful rm -rf "$PIP_LOG_DIR" - # Create symlink in ~/.local/bin for easy access - if [[ "$CLEANUP_ON_EXIT" == "yes" ]]; then - mkdir -p "$HOME/.local/bin" - ln -sf "$VENV_DIR/bin/vocalinux" "$HOME/.local/bin/vocalinux" - print_info "Created symlink: ~/.local/bin/vocalinux" + # Create wrapper scripts in ~/.local/bin for easy access + mkdir -p "$HOME/.local/bin" - # Check if ~/.local/bin is in PATH - if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then - print_warning "~/.local/bin is not in your PATH" - print_info "Add this line to your ~/.bashrc or ~/.zshrc:" - print_info ' export PATH="$HOME/.local/bin:$PATH"' - fi + # Create vocalinux wrapper script + cat > "$HOME/.local/bin/vocalinux" << WRAPPER_EOF +#!/bin/bash +# Wrapper script for Vocalinux that sets required environment variables +export GI_TYPELIB_PATH=/usr/lib/girepository-1.0 +exec "$VENV_DIR/bin/vocalinux" "\$@" +WRAPPER_EOF + chmod +x "$HOME/.local/bin/vocalinux" + print_info "Created wrapper: ~/.local/bin/vocalinux" + + # Create vocalinux-gui wrapper script + cat > "$HOME/.local/bin/vocalinux-gui" << WRAPPER_EOF +#!/bin/bash +# Wrapper script for Vocalinux GUI that sets required environment variables +export GI_TYPELIB_PATH=/usr/lib/girepository-1.0 +exec "$VENV_DIR/bin/vocalinux-gui" "\$@" +WRAPPER_EOF + chmod +x "$HOME/.local/bin/vocalinux-gui" + print_info "Created wrapper: ~/.local/bin/vocalinux-gui" + + # Check if ~/.local/bin is in PATH + if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then + print_warning "~/.local/bin is not in your PATH" + print_info "Add this line to your ~/.bashrc or ~/.zshrc:" + print_info ' export PATH="$HOME/.local/bin:$PATH"' fi return 0 @@ -1004,16 +1021,17 @@ install_desktop_entry() { return 1 } - # Update the desktop entry to use the venv - VENV_SCRIPT_PATH="$(realpath $VENV_DIR/bin/vocalinux)" - if [ ! -f "$VENV_SCRIPT_PATH" ]; then - print_warning "Vocalinux script not found at $VENV_SCRIPT_PATH" + # Update the desktop entry to use the wrapper script with GI_TYPELIB_PATH + WRAPPER_SCRIPT="$HOME/.local/bin/vocalinux-gui" + if [ ! -f "$WRAPPER_SCRIPT" ]; then + print_warning "Wrapper script not found at $WRAPPER_SCRIPT" print_warning "Desktop entry may not work correctly" else - sed -i "s|^Exec=vocalinux|Exec=$VENV_SCRIPT_PATH|" "$DESKTOP_DIR/vocalinux.desktop" || { + # Update Exec line to include GI_TYPELIB_PATH for PyGObject + sed -i "s|^Exec=vocalinux|Exec=env GI_TYPELIB_PATH=/usr/lib/girepository-1.0 $WRAPPER_SCRIPT|" "$DESKTOP_DIR/vocalinux.desktop" || { print_warning "Failed to update desktop entry path" } - print_info "Updated desktop entry to use virtual environment" + print_info "Updated desktop entry to use wrapper script with GI_TYPELIB_PATH" fi # Make desktop entry executable diff --git a/src/vocalinux/main.py b/src/vocalinux/main.py index c5c8178..dffccf9 100644 --- a/src/vocalinux/main.py +++ b/src/vocalinux/main.py @@ -33,8 +33,23 @@ def parse_arguments(): parser.add_argument( "--language", type=str, - choices=["en-us", "fr", "de", "ru"], - help="Speech recognition language (en-us, fr, de, ru)", + choices=[ + "auto", + "en-us", + "en-in", + "hi", + "es", + "fr", + "de", + "it", + "pt", + "ru", + "zh", + "ja", + "ko", + "ar", + ], + help="Speech recognition language (auto for auto-detect, en-us, hi, es, fr, de, it, pt, ru, zh, etc.)", ) parser.add_argument( "--engine", diff --git a/src/vocalinux/speech_recognition/recognition_manager.py b/src/vocalinux/speech_recognition/recognition_manager.py index 4acf4ed..1b5c993 100644 --- a/src/vocalinux/speech_recognition/recognition_manager.py +++ b/src/vocalinux/speech_recognition/recognition_manager.py @@ -301,10 +301,13 @@ class SpeechRecognitionManager: def _init_vosk(self): """Initialize the VOSK speech recognition engine.""" + # VOSK doesn't support auto-detect, so fall back to en-us for "auto" + vosk_language = "en-us" if self.language == "auto" else self.language + self.vosk_model_map = { - "small": VOSK_MODEL_INFO["small"]["languages"].get(self.language), - "medium": VOSK_MODEL_INFO["medium"]["languages"].get(self.language), - "large": VOSK_MODEL_INFO["large"]["languages"].get(self.language), + "small": VOSK_MODEL_INFO["small"]["languages"].get(vosk_language), + "medium": VOSK_MODEL_INFO["medium"]["languages"].get(vosk_language), + "large": VOSK_MODEL_INFO["large"]["languages"].get(vosk_language), } try: @@ -452,6 +455,8 @@ class SpeechRecognitionManager: lang = self.language if self.language == "en-us": lang = "en" + elif self.language == "auto": + lang = None # Auto-detect # Transcribe with Whisper (handles variable length audio automatically) result = self.model.transcribe( @@ -1140,6 +1145,7 @@ class SpeechRecognitionManager: self, engine: Optional[str] = None, model_size: Optional[str] = None, + language: Optional[str] = None, vad_sensitivity: Optional[int] = None, silence_timeout: Optional[float] = None, audio_device_index: Optional[int] = None, @@ -1152,13 +1158,14 @@ class SpeechRecognitionManager: Args: engine: The new speech recognition engine ("vosk" or "whisper"). model_size: The new model size. + language: The new language code (e.g., "en-us", "hi", "auto"). vad_sensitivity: New VAD sensitivity (for VOSK). silence_timeout: New silence timeout (for VOSK). audio_device_index: Audio input device index (None for default, -1 to clear). force_download: If True, download missing models (default: True for UI-triggered reconfigures). """ logger.info( - f"Reconfiguring speech engine. New settings: engine={engine}, model_size={model_size}, vad={vad_sensitivity}, silence={silence_timeout}, audio_device={audio_device_index}" + f"Reconfiguring speech engine. New settings: engine={engine}, model_size={model_size}, language={language}, vad={vad_sensitivity}, silence={silence_timeout}, audio_device={audio_device_index}" ) restart_needed = False @@ -1170,6 +1177,13 @@ class SpeechRecognitionManager: self.model_size = model_size restart_needed = True + # Language change requires restart for both engines + # Whisper needs to know the language for transcription + # VOSK needs to load a different model for the new language + if language is not None and language != self.language: + self.language = language + restart_needed = True + # Update VOSK specific params if provided if vad_sensitivity is not None: self.vad_sensitivity = max(1, min(5, int(vad_sensitivity))) diff --git a/src/vocalinux/ui/config_manager.py b/src/vocalinux/ui/config_manager.py index 83185e1..a37fe96 100644 --- a/src/vocalinux/ui/config_manager.py +++ b/src/vocalinux/ui/config_manager.py @@ -19,7 +19,7 @@ CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json") DEFAULT_CONFIG = { "speech_recognition": { # Changed section name "engine": "whisper", # "vosk" or "whisper" - whisper is default for better accuracy - "language": "en-us", # "en-us", "fr", "de" or "ru" + "language": "auto", # Auto-detect language (Whisper only) "model_size": "tiny", # Current model size (for backward compatibility) "vosk_model_size": "small", # Default model for VOSK engine "whisper_model_size": "tiny", # Default model for Whisper engine diff --git a/src/vocalinux/ui/logging_manager.py b/src/vocalinux/ui/logging_manager.py index 734d0ea..7215bdc 100644 --- a/src/vocalinux/ui/logging_manager.py +++ b/src/vocalinux/ui/logging_manager.py @@ -246,6 +246,8 @@ class LoggingHandler(logging.Handler): def __init__(self, logging_manager: LoggingManager): super().__init__() self.logging_manager = logging_manager + # Track depth to prevent recursive logging + self._emitting = False def emit(self, record: logging.LogRecord): """ @@ -254,7 +256,13 @@ class LoggingHandler(logging.Handler): Args: record: The logging record to emit """ + # Prevent recursive calls + if self._emitting: + return + try: + self._emitting = True + # Create our custom log record log_record = LogRecord( timestamp=datetime.fromtimestamp(record.created), @@ -270,6 +278,8 @@ class LoggingHandler(logging.Handler): except Exception: # Don't let logging errors break the application self.handleError(record) + finally: + self._emitting = False # Global logging manager instance diff --git a/src/vocalinux/ui/settings_dialog.py b/src/vocalinux/ui/settings_dialog.py index 83e1a3e..3494b0d 100644 --- a/src/vocalinux/ui/settings_dialog.py +++ b/src/vocalinux/ui/settings_dialog.py @@ -18,7 +18,7 @@ gi.require_version("Gtk", "3.0") from gi.repository import GLib, Gtk # noqa: E402 from ..common_types import RecognitionState # noqa: E402 -from ..utils.vosk_model_info import VOSK_MODEL_INFO +from ..utils.vosk_model_info import SUPPORTED_LANGUAGES, VOSK_MODEL_INFO # Avoid circular imports for type checking if TYPE_CHECKING: @@ -125,8 +125,9 @@ def _is_vosk_model_downloaded(size: str, language: str) -> bool: if size not in VOSK_MODEL_INFO: return False - if language not in VOSK_MODEL_INFO[size]["languages"]: - language = "en-us" # Fallback to en-us if language not found + # Auto-detect is not supported by VOSK, fall back to en-us + if language == "auto" or language not in VOSK_MODEL_INFO[size]["languages"]: + language = "en-us" model_name = VOSK_MODEL_INFO[size]["languages"][language] @@ -295,6 +296,11 @@ class SettingsDialog(Gtk.Dialog): self._test_active = False self._test_result = "" self._initializing = True # Flag to prevent auto-apply during initialization + self._populating_models = False # Flag to prevent model change handler during population + self._processing_language_change = ( + False # Flag to prevent recursive language change handling + ) + self._applying_settings = False # Flag to prevent recursive settings application self.add_buttons( Gtk.STOCK_CLOSE, @@ -391,6 +397,19 @@ class SettingsDialog(Gtk.Dialog): self.grid.attach(self.model_combo, 1, row, 1, 1) row += 1 + # Language Selection + self.grid.attach(Gtk.Label(label="Language:", halign=Gtk.Align.START), 0, row, 1, 1) + self.language_combo = Gtk.ComboBoxText() + self.language_combo.set_tooltip_text("Select primary language for speech recognition") + self.grid.attach(self.language_combo, 1, row, 1, 1) + row += 1 + + # Language warning label (shown for auto/unsupported) + self.language_warning = Gtk.Label(label="", use_markup=True, halign=Gtk.Align.START) + self.language_warning.set_margin_bottom(5) + self.grid.attach(self.language_warning, 0, row, 2, 1) + row += 1 + # Model legend (applies to both engines) model_legend = Gtk.Label( label="✓ = Downloaded ↓ = Will download ★ = Recommended", @@ -437,6 +456,9 @@ class SettingsDialog(Gtk.Dialog): # Add model change handler self.model_combo.connect("changed", self._on_model_changed) + # Add language change handler + self.language_combo.connect("changed", self._on_language_changed) + # ==================== SEPARATOR ==================== self.grid.attach(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), 0, row, 2, 1) row += 1 @@ -555,6 +577,16 @@ class SettingsDialog(Gtk.Dialog): # Populate model options for the selected engine self._populate_model_options() + # Populate language options + self._populate_language_options() + # Set the current language from settings + if self.language: + if not self.language_combo.set_active_id(self.language): + # Fallback to auto if language not found + logger.warning(f"Language '{self.language}' not found in options, using auto") + self.language_combo.set_active_id("auto") + self.language = "auto" + # Set non-dependent widgets directly self.vad_spin.set_value(self.current_vad) self.silence_spin.set_value(self.current_silence) @@ -598,52 +630,59 @@ class SettingsDialog(Gtk.Dialog): def _populate_model_options(self): """Populate model options based on the current engine selection.""" - # Clear existing items - self.model_combo.remove_all() + # Set flag to prevent model change handler from firing during population + self._populating_models = True + try: + # Clear existing items + self.model_combo.remove_all() - # Get the current engine text and convert to lowercase for lookup - engine_text = self.engine_combo.get_active_text() - if not engine_text: - logger.warning("No engine selected during model options population") - return + # Get the current engine text and convert to lowercase for lookup + engine_text = self.engine_combo.get_active_text() + if not engine_text: + logger.warning("No engine selected during model options population") + return - engine = engine_text.lower() - logger.info(f"Populating model options for engine: {engine}") + engine = engine_text.lower() + logger.info(f"Populating model options for engine: {engine}") - # Get the saved model size for THIS specific engine (not the generic one) - saved_model_for_engine = self.config_manager.get_model_size_for_engine(engine) - logger.info(f"Saved model for {engine}: {saved_model_for_engine}") + # Get the saved model size for THIS specific engine (not the generic one) + saved_model_for_engine = self.config_manager.get_model_size_for_engine(engine) + logger.info(f"Saved model for {engine}: {saved_model_for_engine}") - # Track which models are downloaded and find smallest downloaded - downloaded_models = [] - smallest_model = None + # Track which models are downloaded and find smallest downloaded + downloaded_models = [] + smallest_model = None - # Add model sizes for this engine - if engine in ENGINE_MODELS: - # Add all options for this engine - for size in ENGINE_MODELS[engine]: - if engine == "whisper" and size in WHISPER_MODEL_INFO: - info = WHISPER_MODEL_INFO[size] - is_downloaded = _is_whisper_model_downloaded(size) - status = "✓" if is_downloaded else "↓" - display_text = f"{size.capitalize()} ({_format_size(info['size_mb'])}) {status}" - if is_downloaded: - downloaded_models.append(size) - if smallest_model is None: - smallest_model = size - elif engine == "vosk" and size in VOSK_MODEL_INFO: - info = VOSK_MODEL_INFO[size] - is_downloaded = _is_vosk_model_downloaded(size, self.language) - status = "✓" if is_downloaded else "↓" - display_text = f"{size.capitalize()} ({_format_size(info['size_mb'])}) {status}" - if is_downloaded: - downloaded_models.append(size) - if smallest_model is None: - smallest_model = size - else: - display_text = size.capitalize() - # Use lowercase as ID, display text with info - self.model_combo.append(size.capitalize(), display_text) + # Add model sizes for this engine + if engine in ENGINE_MODELS: + # Add all options for this engine + for size in ENGINE_MODELS[engine]: + if engine == "whisper" and size in WHISPER_MODEL_INFO: + info = WHISPER_MODEL_INFO[size] + is_downloaded = _is_whisper_model_downloaded(size) + status = "✓" if is_downloaded else "↓" + display_text = ( + f"{size.capitalize()} ({_format_size(info['size_mb'])}) {status}" + ) + if is_downloaded: + downloaded_models.append(size) + if smallest_model is None: + smallest_model = size + elif engine == "vosk" and size in VOSK_MODEL_INFO: + info = VOSK_MODEL_INFO[size] + is_downloaded = _is_vosk_model_downloaded(size, self.language) + status = "✓" if is_downloaded else "↓" + display_text = ( + f"{size.capitalize()} ({_format_size(info['size_mb'])}) {status}" + ) + if is_downloaded: + downloaded_models.append(size) + if smallest_model is None: + smallest_model = size + else: + display_text = size.capitalize() + # Use lowercase as ID, display text with info + self.model_combo.append(size.capitalize(), display_text) # Determine which model to select: # 1. If saved model for this engine is downloaded, use it @@ -693,16 +732,46 @@ class SettingsDialog(Gtk.Dialog): # Log final selection logger.info(f"Final selected model: {self.model_combo.get_active_text()}") + finally: + # Clear flag to re-enable model change handler + self._populating_models = False def _on_engine_changed(self, widget): """Handle changes in the selected engine.""" + engine_text = self.engine_combo.get_active_text() + if not engine_text: + return + + engine = engine_text.lower() + + # Check if current language is supported by the new engine + current_lang = self.language_combo.get_active_id() + if current_lang: + if engine == "vosk" and ( + current_lang == "auto" or not SUPPORTED_LANGUAGES.get(current_lang, {}).get("vosk") + ): + # Switching to VOSK with unsupported language, fall back to en-us + self.language = "en-us" + elif engine == "whisper" and not current_lang: + # No language selected, default to auto for Whisper + self.language = "auto" + self._populate_model_options() + self._populate_language_options() + + # Set the active language in the combo after repopulating + # This will trigger _on_language_changed which will apply settings + self.language_combo.set_active_id(self.language) + self._update_engine_specific_ui() self._update_whisper_info() - self._auto_apply_settings() def _on_model_changed(self, widget): """Handle changes in the selected model.""" + # Don't process if we're populating models (prevents recursive calls) + if self._populating_models: + return + engine_text = self.engine_combo.get_active_text() if engine_text: engine = engine_text.lower() @@ -720,84 +789,160 @@ class SettingsDialog(Gtk.Dialog): """Handle changes in silence timeout.""" self._auto_apply_settings() + def _populate_language_options(self): + """Populate language dropdown with supported languages.""" + self.language_combo.remove_all() + engine = self.engine_combo.get_active_text() + if not engine: + return + + engine = engine.lower() + + for lang_code, lang_info in SUPPORTED_LANGUAGES.items(): + display_text = lang_info["name"] + + # Filter languages based on engine support + if engine == "vosk": + # VOSK: only show languages with available models (skip "auto" since VOSK doesn't support it) + has_model = lang_info["vosk"] is not None + if not has_model or lang_code == "auto": + continue # Skip languages not supported by VOSK + is_downloaded = _is_vosk_model_downloaded("small", lang_code) + display_text += " ✓" if is_downloaded else " ↓" + elif engine == "whisper": + # Whisper: show all languages, add warning for auto-detect + if lang_code == "auto": + display_text += " ⚠" + else: + continue # Unknown engine + + self.language_combo.append(lang_code, display_text) + + def _on_language_changed(self, widget): + """Handle language selection change.""" + # Guard against recursive calls + if self._processing_language_change: + return + + lang_code = self.language_combo.get_active_id() + if not lang_code: + return + + engine = self.engine_combo.get_active_text() + if not engine: + return + + self._processing_language_change = True + try: + engine = engine.lower() + lang_info = SUPPORTED_LANGUAGES.get(lang_code, {}) + + # Update warning label for auto-detect only + if lang_info.get("warning"): + self.language_warning.set_markup( + f"⚠ {lang_info['warning']}" + ) + else: + self.language_warning.set_markup("") + + # Update self.language for VOSK model download checks + self.language = lang_code + + # Refresh model options to update download status indicators + self._populate_model_options() + + self._auto_apply_settings() + finally: + self._processing_language_change = False + def _auto_apply_settings(self): """Automatically apply settings when changed.""" + # Guard against recursive calls + if self._applying_settings: + return + if self._initializing: return # Don't auto-apply during initialization if self._test_active: return # Don't apply during testing - settings = self.get_selected_settings() - engine = settings.get("engine", "vosk") - model_name = settings.get("model_size", "small") - - # Check if model needs to be downloaded - needs_download = False - if engine == "whisper" and not _is_whisper_model_downloaded(model_name): - needs_download = True - model_info = WHISPER_MODEL_INFO.get(model_name, {"size_mb": 500}) - elif engine == "vosk" and not _is_vosk_model_downloaded(model_name, self.language): - needs_download = True - model_info = VOSK_MODEL_INFO.get(model_name, {"size_mb": 50}) - - if needs_download: - # Show download dialog for models that need downloading - logger.info(f"Model {model_name} needs download, showing progress dialog") - download_dialog = ModelDownloadDialog( - self, - model_name, - model_info["size_mb"], - engine=engine, - language=self.language, - ) - - def progress_callback(fraction, speed, status): - """Update UI with download progress.""" - GLib.idle_add(download_dialog.update_progress, fraction, speed, status) - - def download_and_apply(): - try: - # Set up progress callback for downloads (both VOSK and Whisper) - self.speech_engine.set_download_progress_callback(progress_callback) - - # Check for cancellation periodically - def check_cancelled(): - if download_dialog.cancelled: - self.speech_engine.cancel_download() - return not download_dialog.cancelled - - # Start cancellation checker - cancel_check_id = GLib.timeout_add(100, check_cancelled) - - try: - self._apply_settings_internal(settings) - GLib.idle_add(download_dialog.set_complete, True, "") - # Refresh model list after download to update icons - GLib.idle_add(self._populate_model_options) - finally: - GLib.source_remove(cancel_check_id) - self.speech_engine.set_download_progress_callback(None) - - except Exception as e: - error_msg = str(e) - if "cancelled" in error_msg.lower(): - GLib.idle_add(download_dialog.set_complete, False, "Download cancelled") - elif engine == "whisper" and "no module named" in error_msg.lower(): - GLib.idle_add(download_dialog.set_complete, False, "Whisper not installed") - GLib.idle_add(self._show_whisper_install_dialog) - else: - GLib.idle_add(download_dialog.set_complete, False, error_msg[:100]) - - threading.Thread(target=download_and_apply, daemon=True).start() - download_dialog.run() - download_dialog.destroy() - return - - # Model already downloaded, just apply settings directly - logger.info(f"Auto-applying settings: {settings}") + if self._populating_models: + return # Don't apply during model population + self._applying_settings = True try: + settings = self.get_selected_settings() + engine = settings.get("engine", "vosk") + model_name = settings.get("model_size", "small") + + # Check if model needs to be downloaded + needs_download = False + if engine == "whisper" and not _is_whisper_model_downloaded(model_name): + needs_download = True + model_info = WHISPER_MODEL_INFO.get(model_name, {"size_mb": 500}) + elif engine == "vosk" and not _is_vosk_model_downloaded(model_name, self.language): + needs_download = True + model_info = VOSK_MODEL_INFO.get(model_name, {"size_mb": 50}) + + if needs_download: + # Show download dialog for models that need downloading + logger.info(f"Model {model_name} needs download, showing progress dialog") + download_dialog = ModelDownloadDialog( + self, + model_name, + model_info["size_mb"], + engine=engine, + language=self.language, + ) + + def progress_callback(fraction, speed, status): + """Update UI with download progress.""" + GLib.idle_add(download_dialog.update_progress, fraction, speed, status) + + def download_and_apply(): + try: + # Set up progress callback for downloads (both VOSK and Whisper) + self.speech_engine.set_download_progress_callback(progress_callback) + + # Check for cancellation periodically + def check_cancelled(): + if download_dialog.cancelled: + self.speech_engine.cancel_download() + return not download_dialog.cancelled + + # Start cancellation checker + cancel_check_id = GLib.timeout_add(100, check_cancelled) + + try: + self._apply_settings_internal(settings) + GLib.idle_add(download_dialog.set_complete, True, "") + # Refresh model list after download to update icons + GLib.idle_add(self._populate_model_options) + finally: + GLib.source_remove(cancel_check_id) + self.speech_engine.set_download_progress_callback(None) + + except Exception as e: + error_msg = str(e) + if "cancelled" in error_msg.lower(): + GLib.idle_add(download_dialog.set_complete, False, "Download cancelled") + elif engine == "whisper" and "no module named" in error_msg.lower(): + GLib.idle_add( + download_dialog.set_complete, False, "Whisper not installed" + ) + GLib.idle_add(self._show_whisper_install_dialog) + else: + GLib.idle_add(download_dialog.set_complete, False, error_msg[:100]) + + threading.Thread(target=download_and_apply, daemon=True).start() + download_dialog.run() + download_dialog.destroy() + return + + # Model already downloaded, just apply settings directly + logger.info(f"Auto-applying settings: {settings}") + # Update config manager self.config_manager.update_speech_recognition_settings(settings) self.config_manager.save_settings() @@ -807,6 +952,8 @@ class SettingsDialog(Gtk.Dialog): logger.info("Settings auto-applied successfully") except Exception as e: logger.error(f"Failed to auto-apply settings: {e}") + finally: + self._applying_settings = False def _update_engine_specific_ui(self): """Show/hide UI elements specific to the selected engine.""" @@ -926,11 +1073,15 @@ class SettingsDialog(Gtk.Dialog): engine_text = self.engine_combo.get_active_text() # Get model by ID (which is the capitalized name) not the display text model_id = self.model_combo.get_active_id() + # Get language by ID + language_id = self.language_combo.get_active_id() # Handle cases where combo boxes might be empty (shouldn't happen with defaults) engine = engine_text.lower() if engine_text else "vosk" # Extract model name from ID (which is the capitalized name like "Small") model_size = model_id.lower() if model_id else "small" + # Get language code (use "auto" as default) + language = language_id if language_id else "auto" vad = int(self.vad_spin.get_value()) silence = self.silence_spin.get_value() @@ -938,6 +1089,7 @@ class SettingsDialog(Gtk.Dialog): settings = { "engine": engine, "model_size": model_size, + "language": language, "vad_sensitivity": vad, "silence_timeout": silence, } diff --git a/src/vocalinux/utils/vosk_model_info.py b/src/vocalinux/utils/vosk_model_info.py index ac57fb9..9781a97 100644 --- a/src/vocalinux/utils/vosk_model_info.py +++ b/src/vocalinux/utils/vosk_model_info.py @@ -1,3 +1,60 @@ +# Language definitions with display names and Whisper/VOSK codes +# Supported languages for speech recognition +SUPPORTED_LANGUAGES = { + "auto": { + "name": "Auto-detect", + "whisper": None, + "vosk": None, + "warning": "Slower, may be less accurate", + }, + "en-us": { + "name": "English (US)", + "whisper": "en", + "vosk": "vosk-model-small-en-us-0.15", + }, + "hi": { + "name": "Hindi", + "whisper": "hi", + "vosk": "vosk-model-small-hi-0.22", + }, + "es": { + "name": "Spanish", + "whisper": "es", + "vosk": "vosk-model-small-es-0.42", + }, + "fr": { + "name": "French", + "whisper": "fr", + "vosk": "vosk-model-small-fr-0.22", + }, + "de": { + "name": "German", + "whisper": "de", + "vosk": "vosk-model-small-de-0.15", + }, + "it": { + "name": "Italian", + "whisper": "it", + "vosk": "vosk-model-small-it-0.22", + }, + "pt": { + "name": "Portuguese", + "whisper": "pt", + "vosk": "vosk-model-small-pt-0.3", + }, + "ru": { + "name": "Russian", + "whisper": "ru", + "vosk": "vosk-model-small-ru-0.22", + }, + "zh": { + "name": "Chinese", + "whisper": "zh", + "vosk": "vosk-model-small-cn-0.22", + }, +} + + # VOSK model metadata for display # The large option uses the same model as medium, because 0.42 seems unavailable # TODO @violog: specify correct size for each model @@ -7,9 +64,15 @@ VOSK_MODEL_INFO = { "desc": "Lightweight, fast", "languages": { "en-us": "vosk-model-small-en-us-0.15", + "en-in": "vosk-model-small-en-in-0.4", + "hi": "vosk-model-small-hi-0.22", + "es": "vosk-model-small-es-0.42", "fr": "vosk-model-small-fr-0.22", "de": "vosk-model-small-de-0.15", + "it": "vosk-model-small-it-0.22", + "pt": "vosk-model-small-pt-0.3", "ru": "vosk-model-small-ru-0.22", + "zh": "vosk-model-small-cn-0.22", }, }, "medium": { @@ -20,6 +83,10 @@ VOSK_MODEL_INFO = { "fr": "vosk-model-fr-0.22", "de": "vosk-model-de-0.21", "ru": "vosk-model-ru-0.22", + "hi": "vosk-model-hi-0.22", + "es": "vosk-model-es-0.42", + "pt": "vosk-model-pt-0.4", + "zh": "vosk-model-cn-0.22", }, }, "large": { @@ -30,6 +97,10 @@ VOSK_MODEL_INFO = { "fr": "vosk-model-fr-0.22", "de": "vosk-model-de-0.21", "ru": "vosk-model-ru-0.22", + "hi": "vosk-model-hi-0.22", + "es": "vosk-model-es-0.42", + "pt": "vosk-model-pt-0.4", + "zh": "vosk-model-cn-0.22", }, }, }