#!/bin/bash # Vocalinux Installer # This script installs the Vocalinux application and its dependencies set -e # Exit on error # Function to display colored output print_info() { echo -e "\e[1;34m[INFO]\e[0m $1" } print_success() { echo -e "\e[1;32m[SUCCESS]\e[0m $1" } print_error() { echo -e "\e[1;31m[ERROR]\e[0m $1" } print_warning() { echo -e "\e[1;33m[WARNING]\e[0m $1" } # Parse command line arguments INSTALL_MODE="user" RUN_TESTS="no" DEV_MODE="no" VENV_DIR="venv" SKIP_MODELS="no" WITH_WHISPER="no" WHISPER_CPU="no" NO_WHISPER_EXPLICIT="no" NON_INTERACTIVE="no" INTERACTIVE_MODE="no" HAS_NVIDIA_GPU="unknown" GPU_NAME="" GPU_MEMORY="" # Detect if running non-interactively (e.g., via curl | bash) if [ ! -t 0 ]; then NON_INTERACTIVE="yes" fi while [[ $# -gt 0 ]]; do case $1 in --dev) DEV_MODE="yes" shift ;; --test) RUN_TESTS="yes" shift ;; --venv-dir=*) VENV_DIR="${1#*=}" shift ;; --skip-models) SKIP_MODELS="yes" shift ;; --with-whisper) WITH_WHISPER="yes" shift ;; --whisper-cpu) WITH_WHISPER="yes" WHISPER_CPU="yes" shift ;; --no-whisper) WITH_WHISPER="no" NO_WHISPER_EXPLICIT="yes" NON_INTERACTIVE="yes" # Skip the prompt shift ;; --interactive|-i) INTERACTIVE_MODE="yes" shift ;; --tag=*) INSTALL_TAG="${1#*=}" shift ;; -y|--yes) NON_INTERACTIVE="yes" shift ;; --help) echo "Vocalinux Installer" echo "Usage: $0 [options]" echo "Options:" echo " --interactive, -i Guided interactive installation with recommendations" 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 speech models during installation" echo " --with-whisper Install Whisper AI with GPU/CUDA (included by default)" echo " --whisper-cpu Install Whisper with CPU-only PyTorch (smaller download, works on low-RAM)" echo " --no-whisper VOSK-only install (skips Whisper entirely, uses VOSK as default)" echo " --tag=TAG Install specific release tag (default: v0.4.1-alpha)" echo " -y, --yes Non-interactive mode (accept defaults)" echo " --help Show this help message" exit 0 ;; *) print_error "Unknown option: $1" echo "Use --help to see available options" exit 1 ;; esac done # If dev mode is enabled, automatically run tests if [[ "$DEV_MODE" == "yes" ]]; then RUN_TESTS="yes" fi # Display ASCII art banner cat << "EOF" ▗▖ ▗▖ ▗▄▖ ▗▄▄▖ ▗▄▖ ▗▖ ▗▄▄▄▖▗▖ ▗▖▗▖ ▗▖▗▖ ▗▖ ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌ ▐▌▐▌ █ ▐▛▚▖▐▌▐▌ ▐▌ ▝▚▞▘ ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▛▀▜▌▐▌ █ ▐▌ ▝▜▌▐▌ ▐▌ ▐▌ ▝▚▞▘ ▝▚▄▞▘▝▚▄▄▖▐▌ ▐▌▐▙▄▄▖▗▄█▄▖▐▌ ▐▌▝▚▄▞▘▗▞▘▝▚▖ Voice Dictation for Linux EOF print_info "Vocalinux Installer" print_info "==============================" echo "" # Default to installing from latest stable release instead of main branch INSTALL_TAG="${INSTALL_TAG:-v0.4.1-alpha}" # Check if running from within the vocalinux repo or remotely (via curl) REPO_URL="https://github.com/jatinkrmalik/vocalinux.git" INSTALL_DIR="" CLEANUP_ON_EXIT="no" if [ -f "setup.py" ] || [ -f "pyproject.toml" ]; then # Running from within the repo INSTALL_DIR="$(pwd)" print_info "Running from local repository: $INSTALL_DIR" # Convert VENV_DIR to absolute path for wrapper scripts VENV_DIR="$INSTALL_DIR/$VENV_DIR" else # Running remotely (e.g., via curl | bash) print_info "Installing Vocalinux version: ${INSTALL_TAG}" INSTALL_DIR="$HOME/.local/share/vocalinux-install" mkdir -p "$INSTALL_DIR" if [ -d "$INSTALL_DIR/.git" ]; then print_info "Updating existing clone..." cd "$INSTALL_DIR" git fetch origin "tag" "$INSTALL_TAG" git reset --hard "$INSTALL_TAG" else rm -rf "$INSTALL_DIR" git clone --depth 1 --branch "$INSTALL_TAG" "$REPO_URL" "$INSTALL_DIR" || { print_error "Failed to clone Vocalinux repository" exit 1 } cd "$INSTALL_DIR" fi CLEANUP_ON_EXIT="yes" print_info "Repository cloned to: $INSTALL_DIR" # When running remotely, install venv to user's home directory VENV_DIR="$HOME/.local/share/vocalinux/venv" fi # Change to install directory cd "$INSTALL_DIR" print_info "Using virtual environment: $VENV_DIR" [[ "$DEV_MODE" == "yes" ]] && print_info "Installing in development mode" [[ "$RUN_TESTS" == "yes" ]] && print_info "Tests will be run after installation" echo "" # Check if running as root if [ "$EUID" -eq 0 ]; then print_error "Please do not run this script as root or with sudo." exit 1 fi # Detect Linux distribution and version detect_distro() { if [ -f /etc/os-release ]; then . /etc/os-release DISTRO_NAME="$NAME" DISTRO_ID="$ID" DISTRO_VERSION="$VERSION_ID" DISTRO_FAMILY="unknown" # Determine distribution family if [[ "$ID" == "ubuntu" || "$ID_LIKE" == *"ubuntu"* || "$ID" == "pop" || "$ID" == "linuxmint" || "$ID" == "elementary" || "$ID" == "zorin" ]]; then DISTRO_FAMILY="ubuntu" elif [[ "$ID" == "debian" || "$ID_LIKE" == *"debian"* ]]; then DISTRO_FAMILY="debian" elif [[ "$ID" == "fedora" || "$ID_LIKE" == *"fedora"* || "$ID" == "rhel" || "$ID" == "centos" || "$ID" == "rocky" || "$ID" == "almalinux" ]]; then DISTRO_FAMILY="fedora" elif [[ "$ID" == "arch" || "$ID_LIKE" == *"arch"* || "$ID" == "manjaro" || "$ID" == "endeavouros" ]]; then DISTRO_FAMILY="arch" elif [[ "$ID" == "opensuse" || "$ID_LIKE" == *"suse"* ]]; then DISTRO_FAMILY="suse" elif [[ "$ID" == "gentoo" ]]; then DISTRO_FAMILY="gentoo" elif [[ "$ID" == "alpine" ]]; then DISTRO_FAMILY="alpine" elif [[ "$ID" == "void" ]]; then DISTRO_FAMILY="void" elif [[ "$ID" == "solus" ]]; then DISTRO_FAMILY="solus" elif [[ "$ID" == "mageia" ]]; then DISTRO_FAMILY="mageia" fi print_info "Detected: $DISTRO_NAME $DISTRO_VERSION ($DISTRO_FAMILY family)" return 0 else print_error "Could not detect Linux distribution (missing /etc/os-release)" return 1 fi } # Check minimum required version for Ubuntu-based systems check_ubuntu_version() { local MIN_VERSION="18.04" if [[ "$DISTRO_FAMILY" == "ubuntu" ]]; then if [[ $(echo -e "$DISTRO_VERSION\n$MIN_VERSION" | sort -V | head -n1) == "$MIN_VERSION" || "$DISTRO_VERSION" == "$MIN_VERSION" ]]; then return 0 else print_error "This application requires Ubuntu $MIN_VERSION or newer. Detected: $DISTRO_VERSION" return 1 fi fi return 0 } # Detect NVIDIA GPU presence detect_nvidia_gpu() { # Check if nvidia-smi command exists and can successfully query GPU if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi >/dev/null 2>&1; then # Extract GPU information for user feedback GPU_NAME=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -n1) GPU_MEMORY=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader 2>/dev/null | head -n1) HAS_NVIDIA_GPU="yes" return 0 else HAS_NVIDIA_GPU="no" return 1 fi } # Detect GI_TYPELIB_PATH for cross-distro compatibility detect_typelib_path() { # Try pkg-config first (most reliable) if command -v pkg-config >/dev/null 2>&1; then local path=$(pkg-config --variable=typelibdir gobject-introspection-1.0 2>/dev/null) if [ -n "$path" ] && [ -d "$path" ]; then echo "$path" return 0 fi fi # Fallback to common distribution-specific paths # Order matters: more specific paths first for path in \ /usr/lib/x86_64-linux-gnu/girepository-1.0 \ /usr/lib/aarch64-linux-gnu/girepository-1.0 \ /usr/lib/arm-linux-gnueabihf/girepository-1.0 \ /usr/lib/riscv64-linux-gnu/girepository-1.0 \ /usr/lib/powerpc64le-linux-gnu/girepository-1.0 \ /usr/lib/s390x-linux-gnu/girepository-1.0 \ /usr/lib64/girepository-1.0 \ /usr/lib/girepository-1.0 \ /usr/local/lib/girepository-1.0 \ /usr/local/lib64/girepository-1.0; do if [ -d "$path" ]; then echo "$path" return 0 fi done # Ultimate fallback - will cause issues if wrong, but at least we try echo "/usr/lib/girepository-1.0" return 1 } # Print section header for interactive mode clear_screen() { if [ -t 1 ] && command -v clear >/dev/null 2>&1 && [ -n "${TERM:-}" ]; then clear >/dev/null 2>&1 || true fi } print_header() { local title="$1" echo "" echo "============================================================" echo " $title" echo "============================================================" } # Function to run interactive guided installation run_interactive_install() { clear_screen cat << "EOF" Interactive Installation Guide =============================== EOF echo "Welcome! This guided installation will help you set up Vocalinux" echo "with the best options for your system." echo "" echo "Both speech engines are 100% offline, local, and private." echo "Your voice data never leaves your computer." echo "" # Step 1: Detect and display system info print_header "Step 1: Your System" echo "Detected: $DISTRO_NAME $DISTRO_VERSION" detect_nvidia_gpu if [[ "$HAS_NVIDIA_GPU" == "yes" ]]; then echo "GPU: $GPU_NAME ($GPU_MEMORY)" RECOMMENDED_ENGINE="whisper-gpu" RECOMMENDED_LABEL="Whisper AI with GPU acceleration" else echo "GPU: None detected (or no NVIDIA drivers)" RECOMMENDED_ENGINE="whisper-cpu" RECOMMENDED_LABEL="Whisper AI (CPU-only)" fi echo "" # Step 2: Choose installation type print_header "Step 2: Installation Type" echo "Choose what to install:" echo "" echo " 1. Full install (recommended)" echo " • Whisper AI - highest accuracy, 99+ languages" echo " • VOSK - fast fallback engine" echo " • Can switch between engines in Settings" echo "" echo " 2. Minimal install" echo " • VOSK only - lightweight and fast" echo " • Works on older/low-RAM systems" echo " • ~40MB vs ~2GB+ for full install" echo "" read -p "Choose installation type [1-2] (default: 1): " ENGINE_CHOICE ENGINE_CHOICE=${ENGINE_CHOICE:-1} if [[ "$ENGINE_CHOICE" == "2" ]]; then WITH_WHISPER="no" NO_WHISPER_EXPLICIT="yes" SELECTED_ENGINE="Minimal (VOSK only)" else WITH_WHISPER="yes" SELECTED_ENGINE="Full (Whisper + VOSK)" # Step 3: Whisper variant (if Whisper selected) print_header "Step 3: Whisper Installation Type" echo "How would you like to install Whisper?" echo "" echo " 1. GPU version (~2.3GB) - For NVIDIA GPUs" echo " 2. CPU-only (~200MB) - For any system" echo "" if [[ "$HAS_NVIDIA_GPU" == "yes" ]]; then DEFAULT_WHISPER="1" echo " -> Recommended: Option 1 (GPU detected)" else DEFAULT_WHISPER="2" echo " -> Recommended: Option 2 (no GPU detected)" fi echo "" read -p "Choose installation type [1-2] (default: $DEFAULT_WHISPER): " WHISPER_CHOICE WHISPER_CHOICE=${WHISPER_CHOICE:-$DEFAULT_WHISPER} if [[ "$WHISPER_CHOICE" == "2" ]]; then WHISPER_CPU="yes" SELECTED_VARIANT="Whisper AI (CPU-only)" else SELECTED_VARIANT="Whisper AI (GPU)" fi fi # Step 4: Download models now or later? print_header "Step 4: Model Download" echo "Speech recognition models need to be downloaded." echo "" echo " 1. Download now (recommended) - Faster first run" echo " 2. Download later - Smaller install, models download on first use" echo "" read -p "Download models now? [1-2] (default: 1): " MODELS_CHOICE MODELS_CHOICE=${MODELS_CHOICE:-1} if [[ "$MODELS_CHOICE" == "2" ]]; then SKIP_MODELS="yes" fi # Summary print_header "Installation Summary" echo "Install type: $SELECTED_ENGINE" if [[ "$WITH_WHISPER" == "yes" ]]; then echo "Whisper variant: ${SELECTED_VARIANT:-GPU}" fi echo "Models: $([[ "$SKIP_MODELS" == "yes" ]] && echo "Download on first use" || echo "Download now")" echo "" read -p "Press Enter to continue, or Ctrl+C to cancel..." echo "" # Set non-interactive to skip the old prompt NON_INTERACTIVE="yes" } # Detect distribution detect_distro # Check compatibility if [[ "$DISTRO_FAMILY" != "ubuntu" ]]; then print_warning "This installer is primarily designed for Ubuntu-based systems. Your system: $DISTRO_NAME" print_warning "The application may still work, but you might need to install dependencies manually." if [[ "$NON_INTERACTIVE" == "yes" ]]; then print_info "Non-interactive mode: continuing anyway..." else read -p "Do you want to continue anyway? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi fi else # Check version for Ubuntu-based systems if ! check_ubuntu_version; then if [[ "$NON_INTERACTIVE" == "yes" ]]; then print_info "Non-interactive mode: continuing anyway..." else read -p "Do you want to continue anyway? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi fi fi fi # Check if interactive mode is requested if [[ "$INTERACTIVE_MODE" == "yes" ]]; then # Check if we have a TTY (required for interactive mode) if [ ! -t 0 ]; then print_error "Interactive mode requires a terminal (TTY)." print_error "Please run from a terminal, or use quick install mode:" print_error " curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh | bash" exit 1 fi # Run interactive installation run_interactive_install fi # Auto-detect GPU for non-interactive mode when no explicit Whisper choice # This helps users who run curl | bash without any flags if [[ "$NON_INTERACTIVE" == "yes" ]] && [[ "$WITH_WHISPER" == "no" ]] && [[ "$NO_WHISPER_EXPLICIT" == "no" ]] && [[ "$INTERACTIVE_MODE" == "no" ]]; then detect_nvidia_gpu if [[ "$HAS_NVIDIA_GPU" == "yes" ]]; then WITH_WHISPER="yes" print_info "NVIDIA GPU detected: $GPU_NAME" print_info "Installing Whisper with GPU support..." else WITH_WHISPER="yes" WHISPER_CPU="yes" print_info "No NVIDIA GPU detected" print_info "Installing Whisper with CPU-only PyTorch (~200MB)..." fi echo "" fi # Function to check if a command exists command_exists() { command -v "$1" >/dev/null 2>&1 } # Function to check if a package is installed (for apt-based systems) apt_package_installed() { dpkg -s "$1" >/dev/null 2>&1 } # Function to check if a package is installed (for dnf-based systems) dnf_package_installed() { rpm -q "$1" >/dev/null 2>&1 } # Function to check if a package is installed (for pacman-based systems) pacman_package_installed() { pacman -Q "$1" >/dev/null 2>&1 } # Function to install system dependencies based on the detected distribution install_system_dependencies() { print_info "Installing system dependencies..." # Define package names for different distributions local APT_PACKAGES_UBUNTU="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 APT_PACKAGES_DEBIAN_BASE="python3-pip python3-gi python3-gi-cairo gir1.2-gtk-3.0 libcairo2-dev python3-dev portaudio19-dev python3-venv wget curl unzip" local APT_PACKAGES_DEBIAN_11_12="$APT_PACKAGES_DEBIAN_BASE libgirepository1.0-dev gir1.2-ayatanaappindicator3-0.1" local APT_PACKAGES_DEBIAN_13_PLUS="$APT_PACKAGES_DEBIAN_BASE libgirepository-2.0-dev gir1.2-ayatanaappindicator3-0.1" 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" # Gentoo uses Portage and different package naming convention local EMERGE_PACKAGES="dev-python/pygobject:3 x11-libs/gtk+:3 dev-libs/libappindicator:3 media-libs/portaudio dev-lang/python:3.8" # Alpine Linux uses apk and has musl libc local APK_PACKAGES="py3-gobject3 py3-pip gtk+3.0 py3-cairo portaudio-dev py3-virtualenv wget curl unzip" # Void Linux uses xbps local XBPS_PACKAGES="python3-pip python3-gobject gtk+3 libappindicator gobject-introspection portaudio-devel python3-devel wget curl unzip" # Solus uses eopkg local EOPKG_PACKAGES="python3-pip python3-gobject gtk3 libappindicator gobject-introspection-devel portaudio-devel python3-virtualenv wget curl unzip" local MISSING_PACKAGES="" local INSTALL_CMD="" local UPDATE_CMD="" case "$DISTRO_FAMILY" in ubuntu|debian) local APT_PACKAGES="$APT_PACKAGES_UBUNTU" if [[ "$DISTRO_FAMILY" == "debian" ]]; then local DEBIAN_MAJOR="${DISTRO_VERSION%%.*}" if [[ "$DEBIAN_MAJOR" =~ ^[0-9]+$ ]] && [ "$DEBIAN_MAJOR" -ge 13 ]; then APT_PACKAGES="$APT_PACKAGES_DEBIAN_13_PLUS" else APT_PACKAGES="$APT_PACKAGES_DEBIAN_11_12" fi fi # Check for missing packages for pkg in $APT_PACKAGES; do if ! apt_package_installed "$pkg"; then MISSING_PACKAGES="$MISSING_PACKAGES $pkg" fi done if [ -n "$MISSING_PACKAGES" ]; then print_info "Installing missing packages:$MISSING_PACKAGES" sudo apt update || { print_error "Failed to update package lists"; exit 1; } # Handle appindicator package for Ubuntu (old package deprecated in newer releases) if echo "$MISSING_PACKAGES" | grep -q "gir1.2-appindicator3-0.1"; then FILTERED_PACKAGES=$(echo "$MISSING_PACKAGES" | sed 's/gir1.2-appindicator3-0.1//' | xargs) if ! sudo apt install -y gir1.2-appindicator3-0.1 2>/dev/null; then print_info "gir1.2-appindicator3-0.1 not available, trying gir1.2-ayatanaappindicator3-0.1..." if ! sudo apt install -y gir1.2-ayatanaappindicator3-0.1; then print_error "Failed to install appindicator package (tried both gir1.2-appindicator3-0.1 and gir1.2-ayatanaappindicator3-0.1)" exit 1 fi print_info "Successfully installed gir1.2-ayatanaappindicator3-0.1 (modern replacement)" fi if [ -n "$FILTERED_PACKAGES" ]; then sudo apt install -y $FILTERED_PACKAGES || { print_error "Failed to install dependencies"; exit 1; } fi else sudo apt install -y $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; } fi else print_info "All required packages are already installed." fi ;; fedora) # For Fedora/RHEL-based systems if command_exists dnf; then INSTALL_CMD="sudo dnf install -y" UPDATE_CMD="sudo dnf check-update" elif command_exists yum; then INSTALL_CMD="sudo yum install -y" UPDATE_CMD="sudo yum check-update" else print_error "No supported package manager found (dnf/yum)" exit 1 fi # Check for missing packages for pkg in $DNF_PACKAGES; do if ! dnf_package_installed "$pkg"; then MISSING_PACKAGES="$MISSING_PACKAGES $pkg" fi done if [ -n "$MISSING_PACKAGES" ]; then print_info "Installing missing packages:$MISSING_PACKAGES" $UPDATE_CMD $INSTALL_CMD $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; } else print_info "All required packages are already installed." fi ;; arch) # For Arch-based systems if ! command_exists pacman; then print_error "Pacman package manager not found" exit 1 fi # Check for missing packages for pkg in $PACMAN_PACKAGES; do if ! pacman_package_installed "$pkg"; then MISSING_PACKAGES="$MISSING_PACKAGES $pkg" fi done if [ -n "$MISSING_PACKAGES" ]; then print_info "Installing missing packages:$MISSING_PACKAGES" sudo pacman -Sy sudo pacman -S --noconfirm $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; } else print_info "All required packages are already installed." fi ;; suse) # For openSUSE if ! command_exists zypper; then print_error "Zypper package manager not found" exit 1 fi print_info "Updating package lists and installing dependencies..." sudo zypper refresh sudo zypper install -y $ZYPPER_PACKAGES || { print_error "Failed to install dependencies"; exit 1; } ;; gentoo) # For Gentoo Linux if ! command_exists emerge; then print_error "Emerge package manager not found" exit 1 fi print_info "Gentoo detected. Installing dependencies..." print_warning "Gentoo uses emerge. This may take longer as packages are compiled from source." # Check for missing packages MISSING_PACKAGES="" for pkg in $EMERGE_PACKAGES; do # Gentoo uses qlist to check if packages are installed if ! qlist -I "$pkg" >/dev/null 2>&1; then MISSING_PACKAGES="$MISSING_PACKAGES $pkg" fi done if [ -n "$MISSING_PACKAGES" ]; then print_info "Installing packages:$MISSING_PACKAGES" # Update Portage tree first sudo emerge --sync || { print_error "Failed to sync Portage tree"; exit 1; } # Install missing packages sudo emerge $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; } else print_info "All required packages are already installed." fi ;; alpine) # For Alpine Linux if ! command_exists apk; then print_error "Apk package manager not found" exit 1 fi print_info "Alpine Linux detected." print_warning "Alpine uses musl libc. Some Python packages may not have pre-built wheels." # Check for missing packages MISSING_PACKAGES="" for pkg in $APK_PACKAGES; do if ! apk info -e "$pkg" >/dev/null 2>&1; then MISSING_PACKAGES="$MISSING_PACKAGES $pkg" fi done if [ -n "$MISSING_PACKAGES" ]; then print_info "Installing packages:$MISSING_PACKAGES" sudo apk update || { print_error "Failed to update package indexes"; exit 1; } sudo apk add $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; } else print_info "All required packages are already installed." fi ;; void) # For Void Linux if ! command_exists xbps; then print_error "Xbps package manager not found" exit 1 fi print_info "Void Linux detected." # Check for missing packages MISSING_PACKAGES="" for pkg in $XBPS_PACKAGES; do if ! xbps-query -S "$pkg" >/dev/null 2>&1; then MISSING_PACKAGES="$MISSING_PACKAGES $pkg" fi done if [ -n "$MISSING_PACKAGES" ]; then print_info "Installing packages:$MISSING_PACKAGES" sudo xbps-install -Sy $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; } else print_info "All required packages are already installed." fi ;; solus) # For Solus if ! command_exists eopkg; then print_error "Eopkg package manager not found" exit 1 fi print_info "Solus detected." # Check for missing packages MISSING_PACKAGES="" for pkg in $EOPKG_PACKAGES; do if ! eopkg info "$pkg" >/dev/null 2>&1; then MISSING_PACKAGES="$MISSING_PACKAGES $pkg" fi done if [ -n "$MISSING_PACKAGES" ]; then print_info "Installing packages:$MISSING_PACKAGES" sudo eopkg install $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; } else print_info "All required packages are already installed." fi ;; mageia) # For Mageia if command_exists dnf; then INSTALL_CMD="sudo dnf install -y" UPDATE_CMD="sudo dnf check-update" elif command_exists urpmi; then INSTALL_CMD="sudo urpmi --force" UPDATE_CMD="sudo urpmi.update -a" else print_error "No supported package manager found (dnf/urpmi)" exit 1 fi # Use similar packages to Fedora/RHEL for pkg in $DNF_PACKAGES; do # Mageia uses rpm like Fedora if ! rpm -q "$pkg" >/dev/null 2>&1; then MISSING_PACKAGES="$MISSING_PACKAGES $pkg" fi done if [ -n "$MISSING_PACKAGES" ]; then print_info "Installing missing packages:$MISSING_PACKAGES" $UPDATE_CMD 2>/dev/null || true $INSTALL_CMD $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; } else print_info "All required packages are already installed." fi ;; *) print_error "Unsupported distribution family: $DISTRO_FAMILY" print_warning "Please install the following dependencies manually:" print_warning "- Python 3.8 or newer with pip" print_warning "- PyGObject (GTK3 bindings for Python)" print_warning "- GTK3 development libraries" print_warning "- AppIndicator3 support" print_warning "- PortAudio development libraries" print_warning "- Python virtual environment support" if [[ "$NON_INTERACTIVE" != "yes" ]]; then read -p "Press Enter to continue once dependencies are installed, or Ctrl+C to cancel..." else print_info "Non-interactive mode: continuing (dependencies may be missing)..." fi ;; esac } # Install system dependencies install_system_dependencies # Define XDG directories CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/vocalinux" DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/vocalinux" DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications" ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/scalable/apps" # Function to detect and install text input tools install_text_input_tools() { # Detect session type more robustly local SESSION_TYPE="unknown" # Check XDG_SESSION_TYPE first if [ -n "$XDG_SESSION_TYPE" ]; then SESSION_TYPE="$XDG_SESSION_TYPE" # Check for Wayland-specific environment variables elif [ -n "$WAYLAND_DISPLAY" ]; then SESSION_TYPE="wayland" # Check if X server is running elif [ -n "$DISPLAY" ] && command_exists xset && xset q &>/dev/null; then SESSION_TYPE="x11" # Check loginctl if available elif command_exists loginctl; then SESSION_TYPE=$(loginctl show-session $(loginctl | grep $(whoami) | awk '{print $1}') -p Type | cut -d= -f2) fi print_info "Detected session type: $SESSION_TYPE" # Install appropriate tools based on session type and distribution case "$SESSION_TYPE" in wayland) print_info "Installing Wayland text input tools..." case "$DISTRO_FAMILY" in ubuntu|debian) if ! apt_package_installed "wtype"; then sudo apt install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } else print_info "wtype is already installed." fi ;; fedora) if command_exists dnf && ! dnf_package_installed "wtype"; then sudo dnf install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } elif command_exists yum && ! rpm -q wtype &>/dev/null; then sudo yum install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } else print_info "wtype is already installed." fi ;; arch) if ! pacman_package_installed "wtype"; then sudo pacman -S --noconfirm wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } else print_info "wtype is already installed." fi ;; suse) sudo zypper install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } ;; gentoo) if ! qlist -I wtype >/dev/null 2>&1; then sudo emerge wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } else print_info "wtype is already installed." fi ;; alpine) if ! apk info -e wtype >/dev/null 2>&1; then sudo apk add wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } else print_info "wtype is already installed." fi ;; void) if ! xbps-query -S wtype >/dev/null 2>&1; then sudo xbps-install -Sy wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } else print_info "wtype is already installed." fi ;; solus) if ! eopkg info wtype >/dev/null 2>&1; then sudo eopkg install wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } else print_info "wtype is already installed." fi ;; mageia) if command_exists dnf && ! rpm -q wtype >/dev/null 2>&1; then sudo dnf install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } elif command_exists urpmi && ! rpm -q wtype >/dev/null 2>&1; then sudo urpmi -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } else print_info "wtype is already installed." fi ;; *) print_warning "Unsupported distribution for Wayland text input tools." print_warning "Please install 'wtype' manually for Wayland text input support." ;; esac # Try to install ydotool as additional fallback for Wayland # ydotool works better with some compositors (like GNOME) where wtype may fail print_info "Attempting to install ydotool for better Wayland compatibility..." case "$DISTRO_FAMILY" in ubuntu|debian) if ! apt_package_installed "ydotool"; then sudo apt install -y ydotool 2>/dev/null || print_info "ydotool not available in repos (optional)" fi ;; fedora) if command_exists dnf; then sudo dnf install -y ydotool 2>/dev/null || print_info "ydotool not available in repos (optional)" fi ;; arch) if ! pacman_package_installed "ydotool"; then sudo pacman -S --noconfirm ydotool 2>/dev/null || print_info "ydotool not available in repos (optional)" fi ;; esac # Add user to input group for ydotool/dotool support if ! groups | grep -q '\binput\b'; then print_info "Adding $USER to 'input' group for text injection..." sudo usermod -aG input "$USER" || print_warning "Failed to add user to input group" print_warning "You will need to LOG OUT and back in for text injection to work with ydotool/dotool" fi # Install udev rule for ydotool/dotool if [ ! -f /etc/udev/rules.d/80-dotool.rules ]; then print_info "Installing udev rule for input device access..." echo 'KERNEL=="uinput", GROUP="input", MODE="0620", OPTIONS+="static_node=uinput"' \ | sudo tee /etc/udev/rules.d/80-dotool.rules >/dev/null 2>&1 || print_warning "Failed to install udev rule" sudo udevadm control --reload 2>/dev/null || true sudo udevadm trigger 2>/dev/null || true fi ;; x11|"") print_info "Installing X11 text input tools..." case "$DISTRO_FAMILY" in ubuntu|debian) if ! apt_package_installed "xdotool"; then sudo apt install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } else print_info "xdotool is already installed." fi ;; fedora) if command_exists dnf && ! dnf_package_installed "xdotool"; then sudo dnf install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } elif command_exists yum && ! rpm -q xdotool &>/dev/null; then sudo yum install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } else print_info "xdotool is already installed." fi ;; arch) if ! pacman_package_installed "xdotool"; then sudo pacman -S --noconfirm xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } else print_info "xdotool is already installed." fi ;; suse) sudo zypper install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } ;; gentoo) if ! qlist -I xdotool >/dev/null 2>&1; then sudo emerge xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } else print_info "xdotool is already installed." fi ;; alpine) if ! apk info -e xdotool >/dev/null 2>&1; then sudo apk add xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } else print_info "xdotool is already installed." fi ;; void) if ! xbps-query -S xdotool >/dev/null 2>&1; then sudo xbps-install -Sy xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } else print_info "xdotool is already installed." fi ;; solus) if ! eopkg info xdotool >/dev/null 2>&1; then sudo eopkg install xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } else print_info "xdotool is already installed." fi ;; mageia) if command_exists dnf && ! rpm -q xdotool >/dev/null 2>&1; then sudo dnf install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } elif command_exists urpmi && ! rpm -q xdotool >/dev/null 2>&1; then sudo urpmi -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } else print_info "xdotool is already installed." fi ;; *) print_warning "Unsupported distribution for X11 text input tools." print_warning "Please install 'xdotool' manually for X11 text input support." ;; esac ;; *) print_warning "Unknown session type: $SESSION_TYPE" print_warning "Installing both Wayland and X11 text input tools for compatibility..." # Install both tools based on distribution case "$DISTRO_FAMILY" in ubuntu|debian) sudo apt install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; } ;; fedora|mageia) if command_exists dnf; then sudo dnf install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; } elif command_exists yum; then sudo yum install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; } fi # Mageia also supports urpmi if [[ "$DISTRO_FAMILY" == "mageia" ]] && command_exists urpmi; then sudo urpmi -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; } fi ;; arch) sudo pacman -S --noconfirm xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; } ;; suse) sudo zypper install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; } ;; gentoo) sudo emerge xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; } ;; alpine) sudo apk add xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; } ;; void) sudo xbps-install -Sy xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; } ;; solus) sudo eopkg install xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; } ;; *) print_warning "Unsupported distribution for text input tools." print_warning "Please install 'xdotool' and 'wtype' manually for text input support." ;; esac ;; esac } # Install text input tools based on session type install_text_input_tools # Create necessary directories print_info "Creating application directories..." mkdir -p "$CONFIG_DIR" mkdir -p "$DATA_DIR/models" mkdir -p "$DESKTOP_DIR" mkdir -p "$ICON_DIR" # Check Python version check_python_version() { local MIN_VERSION="3.8" local PYTHON_CMD="python3" # Check if python3 command exists if ! command_exists python3; then print_error "Python 3 is not installed or not in PATH" return 1 fi # Get Python version local PY_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") print_info "Detected Python version: $PY_VERSION" # Compare versions if [[ $(echo -e "$PY_VERSION\n$MIN_VERSION" | sort -V | head -n1) == "$MIN_VERSION" || "$PY_VERSION" == "$MIN_VERSION" ]]; then return 0 else print_error "This application requires Python $MIN_VERSION or newer. Detected: $PY_VERSION" return 1 fi } # Set up virtual environment with error handling setup_virtual_environment() { print_info "Setting up Python virtual environment in $VENV_DIR..." # Check if virtual environment already exists if [ -d "$VENV_DIR" ] && [ -f "$VENV_DIR/bin/activate" ]; then print_warning "Virtual environment already exists in $VENV_DIR" if [[ "$NON_INTERACTIVE" == "yes" ]]; then # In non-interactive mode, reuse existing venv print_info "Non-interactive mode: using existing virtual environment." source "$VENV_DIR/bin/activate" || { print_error "Failed to activate virtual environment"; exit 1; } return 0 else read -p "Do you want to recreate it? (y/n) " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then print_info "Removing existing virtual environment..." rm -rf "$VENV_DIR" else print_info "Using existing virtual environment." source "$VENV_DIR/bin/activate" || { print_error "Failed to activate virtual environment"; exit 1; } return 0 fi fi fi # Create virtual environment # Use --system-site-packages to access pre-compiled system packages like PyGObject # This avoids build failures with Python 3.13+ where PyGObject may not build from source python3 -m venv --system-site-packages "$VENV_DIR" || { print_error "Failed to create virtual environment. Please check your Python installation." exit 1 } # Activate virtual environment source "$VENV_DIR/bin/activate" || { print_error "Failed to activate virtual environment"; exit 1; } # Update pip and setuptools print_info "Updating pip, setuptools, and wheel..." pip install --upgrade pip setuptools wheel || { print_error "Failed to update pip, setuptools, and wheel"; exit 1; } print_info "Virtual environment activated successfully." } # Check Python version if ! check_python_version; then print_warning "Continuing with unsupported Python version. Some features may not work correctly." fi # Set up virtual environment setup_virtual_environment # Create activation script for users # Put it in ~/.local/bin when running remotely, or current dir when running locally if [[ "$CLEANUP_ON_EXIT" == "yes" ]]; then ACTIVATION_SCRIPT_DIR="$HOME/.local/bin" mkdir -p "$ACTIVATION_SCRIPT_DIR" else ACTIVATION_SCRIPT_DIR="." fi ACTIVATION_SCRIPT="$ACTIVATION_SCRIPT_DIR/activate-vocalinux.sh" cat > "$ACTIVATION_SCRIPT" << EOF #!/bin/bash # This script activates the Vocalinux virtual environment source "$VENV_DIR/bin/activate" echo "Vocalinux virtual environment activated." echo "To start the application, run: vocalinux" EOF chmod +x "$ACTIVATION_SCRIPT" print_info "Created activation script: $ACTIVATION_SCRIPT" # Function to install Python package with error handling and verification install_python_package() { # Create a temporary directory for pip logs local PIP_LOG_DIR=$(mktemp -d) local PIP_LOG_FILE="$PIP_LOG_DIR/pip_log.txt" # Function to verify package installation verify_package_installed() { local PKG_NAME="vocalinux" # 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 $? } if [[ "$DEV_MODE" == "yes" ]]; then print_info "Installing Vocalinux in development mode..." # Install in development mode with logging pip install -e . --log "$PIP_LOG_FILE" || { print_error "Failed to install Vocalinux in development mode." print_error "Check the pip log for details: $PIP_LOG_FILE" return 1 } # Install test dependencies print_info "Installing test dependencies..." pip install pytest pytest-mock pytest-cov --log "$PIP_LOG_FILE" || { print_warning "Failed to install some test dependencies. Tests may not run correctly." } # Install all optional dependencies for development print_info "Installing all optional dependencies for development..." pip install ".[whisper,dev]" --log "$PIP_LOG_FILE" || { print_warning "Failed to install some optional dependencies." print_warning "Some features may not work correctly." } else print_info "Installing Vocalinux..." # Install the package with logging pip install . --log "$PIP_LOG_FILE" || { print_error "Failed to install Vocalinux." print_error "Check the pip log for details: $PIP_LOG_FILE" return 1 } # Whisper installation logic: # - Default (non-interactive via curl|bash): Install Whisper # - --no-whisper flag: Skip Whisper, create config with VOSK as default # - Interactive mode: Prompt user if [ "$NO_WHISPER_EXPLICIT" = "yes" ]; then # User explicitly requested --no-whisper (for low-RAM systems) print_info "Skipping Whisper installation (--no-whisper flag)." print_info "VOSK will be used as the default speech recognition engine." # Create config file with VOSK as default engine local CONFIG_FILE="$CONFIG_DIR/config.json" if [ ! -f "$CONFIG_FILE" ]; then print_info "Creating config with VOSK as default engine..." cat > "$CONFIG_FILE" << 'VOSK_CONFIG' { "speech_recognition": { "engine": "vosk", "model_size": "small", "vosk_model_size": "small", "whisper_model_size": "tiny", "vad_sensitivity": 3, "silence_timeout": 2.0 }, "audio": { "device_index": null, "device_name": null }, "shortcuts": { "toggle_recognition": "ctrl+ctrl" }, "ui": { "start_minimized": false, "show_notifications": true }, "advanced": { "debug_logging": false, "wayland_mode": false } } VOSK_CONFIG fi elif [ "$WHISPER_CPU" = "yes" ]; then # CPU-only Whisper installation (smaller download, works on low-RAM systems) print_info "Installing Whisper AI with CPU-only PyTorch (~200MB instead of ~2.3GB)..." print_info "This is ideal for systems without NVIDIA GPU or with limited RAM." # Install CPU-only PyTorch first pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu --log "$PIP_LOG_FILE" || { print_warning "Failed to install CPU-only PyTorch." print_warning "Voice recognition will fall back to VOSK." } # Install openai-whisper (without its torch dependency since we already installed it) pip install openai-whisper --log "$PIP_LOG_FILE" || { print_warning "Failed to install Whisper." print_warning "Voice recognition will fall back to VOSK." } elif [ "$WITH_WHISPER" = "yes" ]; then # Install Whisper with GPU support (set by --with-whisper flag or auto-detected in non-interactive mode) print_info "Installing Whisper AI support with GPU/CUDA (~2.3GB download, ~5-10 min)..." print_info "This enables high-accuracy speech recognition with GPU acceleration." print_info "Tip: Use --whisper-cpu for a smaller download if you don't have an NVIDIA GPU." pip install ".[whisper]" --log "$PIP_LOG_FILE" || { print_warning "Failed to install Whisper support." print_warning "Voice recognition will fall back to VOSK." } else # Interactive mode: Prompt user echo "" print_info "Choose installation type:" print_info " 1) Full install - GPU (~2.3GB) - Whisper + VOSK, best accuracy" print_info " 2) Full install - CPU (~200MB) - Whisper + VOSK, no GPU needed" print_info " 3) Minimal install (~40MB) - VOSK only, lightweight" read -p "Choose option [1/2/3]: " -n 1 -r echo if [[ $REPLY == "1" ]]; then WITH_WHISPER="yes" print_info "Installing full (Whisper + VOSK) with GPU support..." pip install ".[whisper]" --log "$PIP_LOG_FILE" || { print_warning "Failed to install Whisper support." print_warning "Voice recognition will fall back to VOSK." } elif [[ $REPLY == "2" ]]; then WITH_WHISPER="yes" WHISPER_CPU="yes" print_info "Installing full (Whisper + VOSK) with CPU-only PyTorch..." pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu --log "$PIP_LOG_FILE" || { print_warning "Failed to install CPU-only PyTorch." } pip install openai-whisper --log "$PIP_LOG_FILE" || { print_warning "Failed to install Whisper." print_warning "Voice recognition will fall back to VOSK." } elif [[ $REPLY == "3" ]]; then # User selected minimal install (VOSK only) NO_WHISPER_EXPLICIT="yes" print_info "Installing minimal (VOSK only)..." # Create config file with VOSK as default engine local CONFIG_FILE_PATH="$CONFIG_DIR/config.json" if [ ! -f "$CONFIG_FILE_PATH" ]; then print_info "Creating config with VOSK as default engine..." cat > "$CONFIG_FILE_PATH" << 'VOSK_INTERACTIVE_CONFIG' { "speech_recognition": { "engine": "vosk", "model_size": "small", "vosk_model_size": "small", "whisper_model_size": "tiny", "vad_sensitivity": 3, "silence_timeout": 2.0 }, "audio": { "device_index": null, "device_name": null }, "shortcuts": { "toggle_recognition": "ctrl+ctrl" }, "ui": { "start_minimized": false, "show_notifications": true }, "advanced": { "debug_logging": false, "wayland_mode": false } } VOSK_INTERACTIVE_CONFIG fi fi fi fi # Verify installation if verify_package_installed; then print_success "Vocalinux package installed successfully!" # Clean up log file if installation was successful rm -rf "$PIP_LOG_DIR" # Detect GI_TYPELIB_PATH for cross-distro compatibility GI_TYPELIB_DETECTED=$(detect_typelib_path) print_info "Detected GI_TYPELIB_PATH: $GI_TYPELIB_DETECTED" # Create wrapper scripts in ~/.local/bin for easy access mkdir -p "$HOME/.local/bin" # Create vocalinux wrapper script # Uses 'sg input' to run with input group for keyboard shortcuts on Wayland # This allows shortcuts to work without logging out after installation cat > "$HOME/.local/bin/vocalinux" << WRAPPER_EOF #!/bin/bash # Wrapper script for Vocalinux that sets required environment variables # and applies the 'input' group for keyboard shortcuts on Wayland export GI_TYPELIB_PATH=$GI_TYPELIB_DETECTED # Check if user is in input group but current session doesn't have it if grep -q "^input:.*\b\$(whoami)\b" /etc/group 2>/dev/null && ! groups | grep -q '\binput\b'; then # Use sg to run with input group without requiring logout exec sg input -c "$VENV_DIR/bin/vocalinux \$*" else exec "$VENV_DIR/bin/vocalinux" "\$@" fi 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 # and applies the 'input' group for keyboard shortcuts on Wayland export GI_TYPELIB_PATH=$GI_TYPELIB_DETECTED # Check if user is in input group but current session doesn't have it if grep -q "^input:.*\b\$(whoami)\b" /etc/group 2>/dev/null && ! groups | grep -q '\binput\b'; then # Use sg to run with input group without requiring logout exec sg input -c "$VENV_DIR/bin/vocalinux-gui \$*" else exec "$VENV_DIR/bin/vocalinux-gui" "\$@" fi 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 else print_error "Vocalinux package installation verification failed." print_error "Check the pip log for details: $PIP_LOG_FILE" return 1 fi } # Install Python package if ! install_python_package; then print_error "Failed to install Vocalinux package. Installation cannot continue." exit 1 fi # Function to download and install Whisper tiny model install_whisper_model() { print_info "Installing Whisper tiny model (~75MB)..." # Create whisper models directory local WHISPER_DIR="$DATA_DIR/models/whisper" mkdir -p "$WHISPER_DIR" # Whisper tiny model URL and path local TINY_MODEL_URL="https://openaipublic.azureedge.net/main/whisper/models/65147644a518d12f04e32d6f3b26facc3f8dd46e5390956a9424a650c0ce22b9/tiny.pt" local TINY_MODEL_PATH="$WHISPER_DIR/tiny.pt" # Check if model already exists if [ -f "$TINY_MODEL_PATH" ]; then print_info "Whisper tiny model already exists at $TINY_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 Whisper model." print_warning "Model 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 "Whisper model will be downloaded on first application run." return 1 fi print_info "Downloading Whisper tiny model..." print_info "This may take a few minutes depending on your internet connection." local TEMP_FILE="$TINY_MODEL_PATH.tmp" # Download the model if command -v wget >/dev/null 2>&1; then if ! wget --progress=bar:force:noscroll -O "$TEMP_FILE" "$TINY_MODEL_URL" 2>&1; then print_error "Failed to download Whisper model with wget" rm -f "$TEMP_FILE" return 1 fi elif command -v curl >/dev/null 2>&1; then if ! curl -L --progress-bar -o "$TEMP_FILE" "$TINY_MODEL_URL"; then print_error "Failed to download Whisper model with curl" rm -f "$TEMP_FILE" return 1 fi fi # Verify download if [ ! -f "$TEMP_FILE" ] || [ ! -s "$TEMP_FILE" ]; then print_error "Downloaded model file is empty or missing" rm -f "$TEMP_FILE" return 1 fi # Move to final location mv "$TEMP_FILE" "$TINY_MODEL_PATH" # Verify the model file if [ -f "$TINY_MODEL_PATH" ]; then local MODEL_SIZE=$(du -h "$TINY_MODEL_PATH" | cut -f1) print_success "Whisper tiny model installed successfully ($MODEL_SIZE)" # Create a marker file to indicate this model was pre-installed echo "$(date)" > "$WHISPER_DIR/.vocalinux_preinstalled" return 0 else print_error "Whisper model installation failed" return 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..." # Check if desktop entry file exists if [ ! -f "vocalinux.desktop" ]; then print_error "Desktop entry file not found: vocalinux.desktop" return 1 fi # Create desktop directory if it doesn't exist mkdir -p "$DESKTOP_DIR" || { print_error "Failed to create desktop directory: $DESKTOP_DIR" return 1 } # Copy desktop entry cp vocalinux.desktop "$DESKTOP_DIR/" || { print_error "Failed to copy desktop entry to $DESKTOP_DIR" return 1 } # 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 # 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 wrapper script with GI_TYPELIB_PATH" fi # Make desktop entry executable chmod +x "$DESKTOP_DIR/vocalinux.desktop" || { print_warning "Failed to make desktop entry executable" } return 0 } # Function to install icons with error handling install_icons() { print_info "Installing application icons..." # Create icon directory if it doesn't exist mkdir -p "$ICON_DIR" || { print_error "Failed to create icon directory: $ICON_DIR" return 1 } # Check if icons directory exists if [ ! -d "resources/icons/scalable" ]; then print_warning "Custom icons not found in resources/icons/scalable directory" return 1 fi # List of icons to install local ICONS=( "vocalinux.svg" "vocalinux-microphone.svg" "vocalinux-microphone-off.svg" "vocalinux-microphone-process.svg" ) # Install each icon local INSTALLED_COUNT=0 for icon in "${ICONS[@]}"; do if [ -f "resources/icons/scalable/$icon" ]; then cp "resources/icons/scalable/$icon" "$ICON_DIR/" || { print_warning "Failed to copy icon: $icon" continue } ((INSTALLED_COUNT++)) else print_warning "Icon not found: resources/icons/scalable/$icon" fi done if [ "$INSTALLED_COUNT" -eq "${#ICONS[@]}" ]; then print_success "Installed all custom Vocalinux icons" return 0 elif [ "$INSTALLED_COUNT" -gt 0 ]; then print_warning "Installed $INSTALLED_COUNT/${#ICONS[@]} custom Vocalinux icons" return 0 else print_error "Failed to install any icons" return 1 fi } # Function to update icon cache and desktop database update_icon_cache() { print_info "Updating icon cache..." # Check if gtk-update-icon-cache command exists if command_exists gtk-update-icon-cache; then gtk-update-icon-cache -f -t "${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" 2>/dev/null || { print_warning "Failed to update icon cache" } else print_warning "gtk-update-icon-cache command not found, skipping icon cache update" fi # Update desktop database so the app appears in application menus immediately print_info "Updating desktop database..." if command_exists update-desktop-database; then update-desktop-database "${XDG_DATA_HOME:-$HOME/.local/share}/applications" 2>/dev/null || { print_warning "Failed to update desktop database" } else print_warning "update-desktop-database command not found - app may not appear in menu until next login" fi } # Install desktop entry install_desktop_entry || print_warning "Desktop entry installation failed" # Install icons install_icons || print_warning "Icon installation failed" # Install Whisper tiny model if Whisper is available # This is important because Whisper is the default engine in config_manager.py # We check if Whisper was actually installed (importable) rather than relying on flags if [ "$SKIP_MODELS" = "no" ]; then if "$VENV_DIR/bin/python" -c "import whisper" 2>/dev/null; then print_info "Whisper is installed - downloading tiny model (default engine)..." install_whisper_model || print_warning "Whisper model download failed - model will be downloaded on first run" elif [ "$NO_WHISPER_EXPLICIT" != "yes" ]; then # Whisper is not installed but wasn't explicitly disabled # This shouldn't happen in normal flow, but warn the user print_warning "Whisper not available but is the default engine." print_warning "The app will try to download the model on first run." else print_info "Whisper not installed (VOSK-only mode), skipping Whisper model download" fi else print_info "Skipping Whisper model download (--skip-models specified)" print_info "Model will be downloaded automatically on first application run" fi # Install VOSK models (always useful as fallback, and required for VOSK-only mode) 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 # Function to run tests with better error handling run_tests() { print_info "Running tests..." # Check if pytest is installed in the virtual environment if ! "$VENV_DIR/bin/python" -c "import pytest" &>/dev/null; then print_info "Installing pytest and related packages..." pip install pytest pytest-mock pytest-cov || { print_error "Failed to install pytest. Cannot run tests." return 1 } fi # Create a directory for test results local TEST_RESULTS_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/vocalinux/test_results" mkdir -p "$TEST_RESULTS_DIR" local TEST_RESULTS_FILE="$TEST_RESULTS_DIR/pytest_$(date +%Y%m%d_%H%M%S).xml" print_info "Running tests with pytest..." print_info "This may take a few minutes..." # Run the tests with pytest and capture output local TEST_OUTPUT_FILE=$(mktemp) if pytest -v --junitxml="$TEST_RESULTS_FILE" | tee "$TEST_OUTPUT_FILE"; then print_success "All tests passed!" print_info "Test results saved to: $TEST_RESULTS_FILE" rm -f "$TEST_OUTPUT_FILE" return 0 else local FAILED_COUNT=$(grep -c "FAILED" "$TEST_OUTPUT_FILE") print_error "$FAILED_COUNT tests failed!" print_info "Test results saved to: $TEST_RESULTS_FILE" print_info "Check the test output for details." rm -f "$TEST_OUTPUT_FILE" return 1 fi } # Run tests if requested if [[ "$RUN_TESTS" == "yes" ]]; then if run_tests; then print_success "Test suite completed successfully." else print_warning "Test suite completed with failures." print_warning "You can still use the application, but some features might not work as expected." fi fi # Function to verify the installation verify_installation() { print_info "Verifying installation..." local ISSUES=0 # Check if virtual environment exists and is activated if [ ! -d "$VENV_DIR" ] || [ ! -f "$VENV_DIR/bin/activate" ]; then print_error "Virtual environment not found or incomplete." ((ISSUES++)) fi # Check if vocalinux command is available if ! command -v vocalinux &>/dev/null && [ ! -f "$VENV_DIR/bin/vocalinux" ]; then print_error "Vocalinux command not found." ((ISSUES++)) fi # Check if desktop entry is installed if [ ! -f "$DESKTOP_DIR/vocalinux.desktop" ]; then print_warning "Desktop entry not found. Application may not appear in application menu." ((ISSUES++)) fi # Check if icons are installed local ICON_COUNT=0 for icon in vocalinux.svg vocalinux-microphone.svg vocalinux-microphone-off.svg vocalinux-microphone-process.svg; do if [ -f "$ICON_DIR/$icon" ]; then ((ICON_COUNT++)) fi done if [ "$ICON_COUNT" -lt 4 ]; then print_warning "Some icons are missing. Application may not display correctly." ((ISSUES++)) fi # Check if Python package is importable using venv python if ! "$VENV_DIR/bin/python" -c "import vocalinux" &>/dev/null; then print_error "Vocalinux Python package cannot be imported." ((ISSUES++)) fi # Return the number of issues found return $ISSUES } # Function to print beautiful welcome message print_welcome_message() { local ISSUES=$1 clear_screen # ASCII art header cat << "EOF" ▗▖ ▗▖ ▗▄▖ ▗▄▄▖ ▗▄▖ ▗▖ ▗▄▄▄▖▗▖ ▗▖▗▖ ▗▖▗▖ ▗▖ ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌ ▐▌▐▌ █ ▐▛▚▖▐▌▐▌ ▐▌ ▝▚▞▘ ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▛▀▜▌▐▌ █ ▐▌ ▝▜▌▐▌ ▐▌ ▐▌ ▝▚▞▘ ▝▚▄▞▘▝▚▄▄▖▐▌ ▐▌▐▙▄▄▖▗▄█▄▖▐▌ ▐▌▝▚▄▞▘▗▞▘▝▚▖ ✓ Installation Complete! EOF # Success or warning message if [ "$ISSUES" -eq 0 ]; then print_success "Vocalinux has been installed successfully!" else print_warning "Installation complete with $ISSUES minor issue(s)" print_warning "The application should still work normally." fi echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " Getting Started" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "1. Launch Vocalinux from your app menu" echo " Look for 'Vocalinux' in your application launcher." echo "" echo -e " Or from terminal: \e[36mvocalinux\e[0m" echo "" echo "2. Find Vocalinux in your system tray (top bar)" echo " • Click the icon to access settings" echo " • Right-click for options (Quit, Settings, etc.)" echo "" echo "3. Start dictating!" echo -e " \e[1mDouble-tap Ctrl\e[0m anywhere to start/stop dictation" echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " First Run Tips" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "• Speak clearly and at a natural pace" echo "• Use voice commands: 'period', 'comma', 'new line', 'delete that'" echo "• Both engines are 100% offline - your privacy is protected" echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " Need Help?" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "• Report issues: https://github.com/jatinkrmalik/vocalinux/issues" echo "• Documentation: https://github.com/jatinkrmalik/vocalinux" echo "• Star us on GitHub: ⭐ https://github.com/jatinkrmalik/vocalinux" echo "" # Installation details (optional, for debugging) if [[ "$VERBOSE" == "yes" ]]; then echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " Installation Details" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Virtual environment: $VENV_DIR" echo "Desktop entry: $DESKTOP_DIR/vocalinux.desktop" echo "Configuration: $CONFIG_DIR" echo "Data directory: $DATA_DIR" echo "" fi } # Verify the installation verify_installation INSTALL_ISSUES=$? # Print welcome message print_welcome_message $INSTALL_ISSUES print_success "Installation process completed!"