From 98cba5f5490c66daf4375509d4edd8181e1be1dc Mon Sep 17 00:00:00 2001 From: Jatin K Malik Date: Mon, 23 Jun 2025 18:05:29 -0500 Subject: [PATCH] feat: Package VOSK model with installation for faster first-run experience (#32) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Package VOSK model with installation for faster first-run experience 🚀 Installation Improvements: - Add VOSK small model download during installation process - Eliminate first-run model download delay for better user experience - Add --skip-models option for offline installations - Include wget, curl, unzip in system dependencies for model download 🔍 Model Management Enhancements: - Add multi-location model search (user, system-wide, pre-installed) - Prioritize pre-installed models over downloads - Add informative logging for model source detection - Create marker files to identify installer-provided models 📦 Installation Script Features: - Robust model download with progress indicators - Internet connectivity checking before download attempts - Graceful fallback when download fails (runtime download) - Comprehensive error handling and cleanup - Installation summary includes model status 🔧 Recognition Manager Updates: - Check multiple model locations in priority order: 1. User's local models directory 2. System-wide installation directories 3. Download to user directory if not found - Enhanced logging for model source identification - Backward compatibility with existing installations 💡 Benefits: - Faster application startup (no initial model download) - Better offline installation support - Reduced first-run friction for users - Maintains fallback to runtime download if needed - Clear feedback about model installation status This addresses the issue where users had to wait for model downloads on first application launch, making VocaLinux ready to use immediately after installation. * fix: Move install_vosk_models function definition before its call The function was being called before it was defined, causing a 'command not found' error during installation. Fixed by moving the function definition to before the installation steps where it's called. This resolves the installation error: ./install.sh: line 701: install_vosk_models: command not found --- install.sh | 126 +++++++++++++++++- .../speech_recognition/recognition_manager.py | 41 +++++- 2 files changed, 162 insertions(+), 5 deletions(-) diff --git a/install.sh b/install.sh index ff71c21..3d02fa6 100755 --- a/install.sh +++ b/install.sh @@ -26,6 +26,7 @@ INSTALL_MODE="user" RUN_TESTS="no" DEV_MODE="no" VENV_DIR="venv" +SKIP_MODELS="no" while [[ $# -gt 0 ]]; do case $1 in @@ -41,6 +42,10 @@ while [[ $# -gt 0 ]]; do VENV_DIR="${1#*=}" shift ;; + --skip-models) + SKIP_MODELS="yes" + shift + ;; --help) echo "Vocalinux Installer" echo "Usage: $0 [options]" @@ -48,6 +53,7 @@ while [[ $# -gt 0 ]]; do echo " --dev Install in development mode with all dev dependencies" echo " --test Run tests after installation" echo " --venv-dir=PATH Specify custom virtual environment directory (default: venv)" + echo " --skip-models Skip downloading VOSK models during installation" echo " --help Show this help message" exit 0 ;; @@ -170,10 +176,10 @@ install_system_dependencies() { print_info "Installing system dependencies..." # Define package names for different distributions - local APT_PACKAGES="python3-pip python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-appindicator3-0.1 libgirepository1.0-dev python3-dev portaudio19-dev python3-venv" - local DNF_PACKAGES="python3-pip python3-gobject gtk3 libappindicator-gtk3 gobject-introspection-devel python3-devel portaudio-devel python3-virtualenv" - local PACMAN_PACKAGES="python-pip python-gobject gtk3 libappindicator-gtk3 gobject-introspection python-cairo portaudio python-virtualenv" - local ZYPPER_PACKAGES="python3-pip python3-gobject python3-gobject-cairo gtk3 libappindicator-gtk3 gobject-introspection-devel python3-devel portaudio-devel python3-virtualenv" + local APT_PACKAGES="python3-pip python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-appindicator3-0.1 libgirepository1.0-dev python3-dev portaudio19-dev python3-venv wget curl unzip" + local DNF_PACKAGES="python3-pip python3-gobject gtk3 libappindicator-gtk3 gobject-introspection-devel python3-devel portaudio-devel python3-virtualenv wget curl unzip" + local PACMAN_PACKAGES="python-pip python-gobject gtk3 libappindicator-gtk3 gobject-introspection python-cairo portaudio python-virtualenv wget curl unzip" + local ZYPPER_PACKAGES="python3-pip python3-gobject python3-gobject-cairo gtk3 libappindicator-gtk3 gobject-introspection-devel python3-devel portaudio-devel python3-virtualenv wget curl unzip" local MISSING_PACKAGES="" local INSTALL_CMD="" @@ -578,6 +584,101 @@ if ! install_python_package; then exit 1 fi +# Function to download and install VOSK models +install_vosk_models() { + print_info "Installing VOSK speech recognition models..." + + # Create models directory + local MODELS_DIR="$DATA_DIR/models" + mkdir -p "$MODELS_DIR" + + # Define model information + local SMALL_MODEL_URL="https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip" + local SMALL_MODEL_NAME="vosk-model-small-en-us-0.15" + local SMALL_MODEL_PATH="$MODELS_DIR/$SMALL_MODEL_NAME" + + # Check if small model already exists + if [ -d "$SMALL_MODEL_PATH" ]; then + print_info "Small VOSK model already exists at $SMALL_MODEL_PATH" + return 0 + fi + + # Check internet connectivity + if ! command -v wget >/dev/null 2>&1 && ! command -v curl >/dev/null 2>&1; then + print_warning "Neither wget nor curl found. Cannot download VOSK models." + print_warning "Models will be downloaded on first application run." + return 1 + fi + + # Test internet connectivity + if ! ping -c 1 google.com >/dev/null 2>&1; then + print_warning "No internet connection detected." + print_warning "VOSK models will be downloaded on first application run." + return 1 + fi + + print_info "Downloading small VOSK model (approximately 40MB)..." + print_info "This may take a few minutes depending on your internet connection." + + local TEMP_ZIP="$MODELS_DIR/$(basename $SMALL_MODEL_URL)" + + # Download the model + if command -v wget >/dev/null 2>&1; then + if ! wget --progress=bar:force:noscroll -O "$TEMP_ZIP" "$SMALL_MODEL_URL" 2>&1; then + print_error "Failed to download VOSK model with wget" + rm -f "$TEMP_ZIP" + return 1 + fi + elif command -v curl >/dev/null 2>&1; then + if ! curl -L --progress-bar -o "$TEMP_ZIP" "$SMALL_MODEL_URL"; then + print_error "Failed to download VOSK model with curl" + rm -f "$TEMP_ZIP" + return 1 + fi + fi + + # Verify download + if [ ! -f "$TEMP_ZIP" ] || [ ! -s "$TEMP_ZIP" ]; then + print_error "Downloaded model file is empty or missing" + rm -f "$TEMP_ZIP" + return 1 + fi + + print_info "Extracting VOSK model..." + + # Extract the model + if command -v unzip >/dev/null 2>&1; then + if ! unzip -q "$TEMP_ZIP" -d "$MODELS_DIR"; then + print_error "Failed to extract VOSK model" + rm -f "$TEMP_ZIP" + return 1 + fi + else + print_error "unzip command not found. Cannot extract VOSK model." + rm -f "$TEMP_ZIP" + return 1 + fi + + # Clean up zip file + rm -f "$TEMP_ZIP" + + # Verify extraction + if [ -d "$SMALL_MODEL_PATH" ]; then + print_success "VOSK small model installed successfully at $SMALL_MODEL_PATH" + + # Set proper permissions + chmod -R 755 "$SMALL_MODEL_PATH" + + # Create a marker file to indicate this model was pre-installed + echo "$(date)" > "$SMALL_MODEL_PATH/.vocalinux_preinstalled" + + return 0 + else + print_error "VOSK model extraction failed - directory not found" + return 1 + fi +} + # Function to install desktop entry with error handling install_desktop_entry() { print_info "Installing desktop entry..." @@ -690,6 +791,14 @@ install_desktop_entry || print_warning "Desktop entry installation failed" # Install icons install_icons || print_warning "Icon installation failed" +# Install VOSK models +if [ "$SKIP_MODELS" = "no" ]; then + install_vosk_models || print_warning "VOSK model installation failed - models will be downloaded on first run" +else + print_info "Skipping VOSK model installation (--skip-models specified)" + print_info "Models will be downloaded automatically on first application run" +fi + # Update icon cache update_icon_cache @@ -812,6 +921,15 @@ print_installation_summary() { print_info "- Configuration: $CONFIG_DIR" print_info "- Data directory: $DATA_DIR" + # Check if VOSK model was installed + if [ "$SKIP_MODELS" = "no" ] && [ -d "$DATA_DIR/models/vosk-model-small-en-us-0.15" ]; then + print_info "- VOSK small model: $DATA_DIR/models/vosk-model-small-en-us-0.15" + elif [ "$SKIP_MODELS" = "yes" ]; then + print_info "- VOSK models: Will be downloaded on first run (--skip-models used)" + else + print_info "- VOSK models: Will be downloaded on first run (installation failed)" + fi + echo print_info "To activate the virtual environment in the future, run:" print_info " source activate-vocalinux.sh" diff --git a/src/vocalinux/speech_recognition/recognition_manager.py b/src/vocalinux/speech_recognition/recognition_manager.py index a3e9603..c18ad4e 100644 --- a/src/vocalinux/speech_recognition/recognition_manager.py +++ b/src/vocalinux/speech_recognition/recognition_manager.py @@ -8,6 +8,7 @@ currently supporting VOSK and Whisper. import json import logging import os +import sys import threading import time from pathlib import Path @@ -21,6 +22,11 @@ logger = logging.getLogger(__name__) # Define constants MODELS_DIR = os.path.expanduser("~/.local/share/vocalinux/models") +# Alternative locations for pre-installed models +SYSTEM_MODELS_DIRS = [ + "/usr/local/share/vocalinux/models", + "/usr/share/vocalinux/models", +] class SpeechRecognitionManager: @@ -86,6 +92,16 @@ class SpeechRecognitionManager: f"VOSK model not found at {self.vosk_model_path}. Downloading..." ) self._download_vosk_model() + # Update path after download + self.vosk_model_path = self._get_vosk_model_path() + else: + # Check if this is a pre-installed model + if any(self.vosk_model_path.startswith(sys_dir) for sys_dir in SYSTEM_MODELS_DIRS): + logger.info(f"Using pre-installed VOSK model from {self.vosk_model_path}") + elif os.path.exists(os.path.join(self.vosk_model_path, ".vocalinux_preinstalled")): + logger.info(f"Using installer-provided VOSK model from {self.vosk_model_path}") + else: + logger.info(f"Using existing VOSK model from {self.vosk_model_path}") logger.info(f"Loading VOSK model from {self.vosk_model_path}") # Ensure previous model/recognizer are released if re-initializing @@ -129,7 +145,23 @@ class SpeechRecognitionManager: } model_name = model_map.get(self.model_size, model_map["small"]) - return os.path.join(MODELS_DIR, model_name) + + # First, check user's local models directory + user_model_path = os.path.join(MODELS_DIR, model_name) + if os.path.exists(user_model_path): + logger.debug(f"Found user model at: {user_model_path}") + return user_model_path + + # Then check system-wide installation directories + for system_dir in SYSTEM_MODELS_DIRS: + system_model_path = os.path.join(system_dir, model_name) + if os.path.exists(system_model_path): + logger.info(f"Found pre-installed model at: {system_model_path}") + return system_model_path + + # If not found anywhere, return the user path (will be created if needed) + logger.debug(f"No existing model found, will use: {user_model_path}") + return user_model_path def _download_vosk_model(self): """Download the VOSK model if it doesn't exist.""" @@ -149,8 +181,15 @@ class SpeechRecognitionManager: raise ValueError(f"Unknown model size: {self.model_size}") model_name = os.path.basename(url).replace(".zip", "") + + # Always download to user's local directory model_path = os.path.join(MODELS_DIR, model_name) zip_path = os.path.join(MODELS_DIR, os.path.basename(url)) + + # Create models directory if it doesn't exist + os.makedirs(MODELS_DIR, exist_ok=True) + + logger.info(f"Downloading VOSK {self.model_size} model to user directory: {model_path}") # Download the model logger.info(f"Downloading VOSK model from {url}")