feat: Package VOSK model with installation for faster first-run experience (#32)

* 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
This commit is contained in:
Jatin K Malik
2025-06-23 18:05:29 -05:00
committed by GitHub
parent d265607d40
commit 98cba5f549
2 changed files with 162 additions and 5 deletions
+122 -4
View File
@@ -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"
@@ -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}")