feat: add language selector UI with auto-detection support (#87)
* feat: add language selector UI with auto-detection support This commit implements a comprehensive language selector for the GUI, extending support from 4 to 10 languages and adding Whisper auto-detect. Changes: - Add SUPPORTED_LANGUAGES constant with 10 languages (auto, en-us, hi, es, fr, de, it, pt, ru, zh) - Each language includes display name, Whisper code, VOSK model name, and optional warning - Extend VOSK_MODEL_INFO to include Hindi, Spanish, Italian, Portuguese, and Chinese models - Add language dropdown in Settings dialog with status indicators (✓ downloaded, ↓ needs download, ⚠ warning) - Add language warning label for auto-detect and engine compatibility messages - Update default language from "en-us" to "auto" for Whisper auto-detection - Handle "auto" language in Whisper (passes None for auto-detect) - Handle "auto" language in VOSK (falls back to "en-us" since VOSK doesn't support auto-detect) - Update CLI --language argument to include all supported languages Fixes the issue where Hindi speech was being transcribed as English because the language was hardcoded to "en-us" which Whisper interpreted as "force English transcription" rather than auto-detect. Related PRs: - #72 - Feature: Add language auto-detection and GUI selector (supersedes) - #71 - Feature: Add multi-language support for FR, DE, RU (builds upon) - #52 - Feature: 3 more languages for Whisper and VOSK (extends) - #68 - Comment out language parameter in transcribe call (addresses) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * style: format main.py with black * fix: use venv python and set GI_TYPELIB_PATH for verification * fix: create wrapper scripts that set GI_TYPELIB_PATH Problem: The symlinks in ~/.local/bin pointed to venv scripts that failed because PyGObject couldn't find the system typelib files (AppIndicator3 namespace not available). Solution: Create wrapper scripts in ~/.local/bin that: 1. Set GI_TYPELIB_PATH=/usr/lib/girepository-1.0 2. Execute the venv's vocalinux scripts This fixes the issue where vocalinux-gui would fail to start with: ValueError: Namespace AppIndicator3 not available * fix: set GI_TYPELIB_PATH in desktop entry Exec line When launching from the app drawer, the environment variables from the wrapper script weren't being inherited properly. By adding GI_TYPELIB_PATH directly in the desktop entry's Exec line, the app can now find the AppIndicator3 typelib files. * fix: always create wrapper scripts, not just for remote installs The wrapper scripts were only created when CLEANUP_ON_EXIT=yes, which is only set for remote (curl-based) installations. When running from a local repository, the wrapper scripts were never created, causing the desktop entry and launch commands to fail. * fix: prevent recursion in LoggingHandler when processing log records The custom LoggingHandler was triggering logging from within the logging system itself, causing infinite recursion when formatting log records. This could happen when the logging formatter or any operation during record processing triggered additional log messages. Fix: Add _emitting flag to prevent recursive calls to emit(). * fix: prevent infinite loop in settings dialog when changing language Add guard flags to prevent recursive event handler calls: - _populating_models: blocks _on_model_changed during model population - _processing_language_change: blocks recursive language change handling - _applying_settings: blocks recursive settings application These guards prevent the infinite loop that occurred when selecting a language whose VOSK model wasn't downloaded yet. Fixes issue where "Model small needs download" logged repeatedly. * fix: handle language parameter in reconfigure and improve UX - Fix reconfigure() to properly handle language parameter changes - Remove _processing_language_change check from _auto_apply_settings to allow settings to be applied when language changes - Hide unsupported languages in dropdown based on selected engine * VOSK: only show languages with available models (no "auto") * Whisper: show all supported languages including "auto" - Remove duplicate _auto_apply_settings call in _on_engine_changed - Set active language in combo after repopulating options This fixes: 1. Whisper continuing to use old language after switching (e.g., still transcribing in Hindi after switching to en-us) 2. Potential infinite loop when selecting languages for VOSK 3. User confusion by showing incompatible language options --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
co-authored by
Claude Opus 4.5
parent
6a1bf7513a
commit
ad84fd882a
+17
-2
@@ -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",
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+265
-113
@@ -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="<small>✓ = Downloaded ↓ = Will download ★ = Recommended</small>",
|
||||
@@ -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"<span foreground='orange'>⚠ {lang_info['warning']}</span>"
|
||||
)
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user