feat(speech): add whisper.cpp with Vulkan support for GPU acceleration (#179)
* feat(speech): add whisper.cpp with Vulkan support for GPU acceleration Add whisper.cpp as a new speech recognition engine with the following features: - Vulkan GPU acceleration support (AMD, Intel, NVIDIA) - Fallback to CUDA for NVIDIA GPUs - CPU-only mode with automatic thread optimization - Support for all model sizes: tiny, base, small, medium, large - Hardware detection to recommend best model/backend - Models downloaded from Hugging Face (ggml format) - Updated UI to show compute backend info in settings - pywhispercpp as the Python binding library - whisper_cpp as the new default engine Changes: - New whispercpp_model_info.py with model metadata and hardware detection - Updated recognition_manager.py with whisper.cpp init and transcription - Updated settings_dialog.py with whisper.cpp UI support - Updated config_manager.py with whisper_cpp_model_size config - Updated main.py with whisper_cpp CLI option - Updated pyproject.toml with pywhispercpp dependency - Updated install.sh with vulkan-tools for GPU detection Closes #104 * style: fix black and isort formatting issues * test: update config manager test for whisper_cpp default engine * feat(install): interactive mode with 3 engine choices and whisper.cpp as default Major changes to install.sh: - Interactive mode is now the DEFAULT (not --auto) - Added --auto flag for non-interactive automatic installation - New --engine flag to specify engine directly - Interactive mode shows 3 engine options with visual boxes: 1. whisper.cpp (recommended) - Vulkan/CUDA/CPU support 2. whisper (OpenAI) - PyTorch-based, NVIDIA-only 3. VOSK - lightweight option - Hardware detection with recommendations: - Detects Vulkan support (AMD/Intel/NVIDIA) - Detects NVIDIA CUDA - Checks available RAM - Recommends best engine based on hardware - Updated help text with new options and examples - Removed old --with-whisper, --whisper-cpu, --no-whisper flags - whisper.cpp is now the default engine for automatic mode * feat(uninstall): kill running vocalinux processes before uninstall * fix(whisper.cpp): auto-download model, use all CPU cores, hide unavailable engines Three fixes for whisper.cpp implementation: 1. Auto-download model during install - Added install_whispercpp_model() function to install.sh - Downloads ggml-tiny.bin (~39MB) from Hugging Face - Runs automatically when whisper.cpp engine is selected 2. Use all CPU cores for transcription - Added n_threads parameter to Model initialization - Uses multiprocessing.cpu_count() to get all cores - Significantly improves transcription speed 3. Hide unavailable engines from dropdown - Added get_available_engines() function to detect installed engines - Settings dialog now only shows engines that are actually installed - Falls back to first available engine if current not available Fixes user-reported issues with whisper.cpp integration. * ui(settings): display whisper.cpp correctly and increase dialog height - Changed engine display name from 'whisper_cpp' to 'whisper.cpp' - Added format_engine_display_name() helper function - Dialog now uses 75% of screen height for better visibility - More options visible by default without scrolling * revert(settings): remove display name change, keep dialog height increase - Reverted engine display name change (was breaking engine selection) - Kept dialog height at 75% of screen for better visibility * style: fix black and isort formatting for whisper.cpp logging * style: fix black formatting in settings_dialog.py * ci: fix Fedora 39 container package installation Add dnf cache update and fallback with --nogpgcheck for Fedora containers that may have stale or missing package metadata.
This commit is contained in:
@@ -35,7 +35,13 @@ jobs:
|
||||
apt-get update
|
||||
apt-get install -y bash curl python3 git
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y bash curl python3 git
|
||||
# Fedora needs cache update first in container environment
|
||||
dnf clean all || true
|
||||
dnf makecache || true
|
||||
dnf install -y bash curl python3 git || {
|
||||
echo "⚠ dnf install failed, trying with --nogpgcheck..."
|
||||
dnf install -y --nogpgcheck bash curl python3 git
|
||||
}
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
|
||||
+384
-237
@@ -31,14 +31,20 @@ WITH_WHISPER="no"
|
||||
WHISPER_CPU="no"
|
||||
NO_WHISPER_EXPLICIT="no"
|
||||
NON_INTERACTIVE="no"
|
||||
INTERACTIVE_MODE="no"
|
||||
INTERACTIVE_MODE="yes" # Default to interactive mode
|
||||
AUTO_MODE="no"
|
||||
HAS_NVIDIA_GPU="unknown"
|
||||
GPU_NAME=""
|
||||
GPU_MEMORY=""
|
||||
HAS_VULKAN="no"
|
||||
VULKAN_DEVICE=""
|
||||
|
||||
# Detect if running non-interactively (e.g., via curl | bash)
|
||||
# Interactive mode is now default, so only auto-detect non-interactive for curl pipes
|
||||
if [ ! -t 0 ]; then
|
||||
NON_INTERACTIVE="yes"
|
||||
# When piped via curl, we'll still try to be interactive if possible
|
||||
# User can force non-interactive with --auto
|
||||
INTERACTIVE_MODE="ask"
|
||||
fi
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -59,19 +65,8 @@ while [[ $# -gt 0 ]]; do
|
||||
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
|
||||
--engine=*)
|
||||
SELECTED_ENGINE="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--interactive|-i)
|
||||
@@ -82,25 +77,38 @@ while [[ $# -gt 0 ]]; do
|
||||
INSTALL_TAG="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
-y|--yes)
|
||||
--auto)
|
||||
AUTO_MODE="yes"
|
||||
INTERACTIVE_MODE="no"
|
||||
NON_INTERACTIVE="yes"
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
echo "Vocalinux Installer"
|
||||
echo ""
|
||||
echo "Usage: $0 [options]"
|
||||
echo ""
|
||||
echo "Installation Modes:"
|
||||
echo " (no flags) Interactive mode - guided setup with recommendations"
|
||||
echo " --auto Automatic mode - install with defaults (whisper.cpp)"
|
||||
echo " --auto --engine=whisper Auto mode with specific engine"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --interactive, -i Guided interactive installation with recommendations"
|
||||
echo " --interactive, -i Force interactive mode (default)"
|
||||
echo " --auto Non-interactive automatic installation"
|
||||
echo " --engine=NAME Speech engine: whisper_cpp (default), whisper, vosk"
|
||||
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 " --venv-dir=PATH Specify custom virtual environment directory"
|
||||
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"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Interactive mode (recommended)"
|
||||
echo " $0 --auto # Auto-install with whisper.cpp"
|
||||
echo " $0 --auto --engine=vosk # Auto-install VOSK only"
|
||||
echo " $0 --dev --test # Dev mode with tests"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
@@ -255,6 +263,48 @@ detect_nvidia_gpu() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect Vulkan support for whisper.cpp
|
||||
detect_vulkan() {
|
||||
# Check for vulkaninfo command
|
||||
if command -v vulkaninfo >/dev/null 2>&1; then
|
||||
local vulkan_output=$(vulkaninfo --summary 2>/dev/null | head -20)
|
||||
if [ -n "$vulkan_output" ]; then
|
||||
HAS_VULKAN="yes"
|
||||
# Try to extract GPU name
|
||||
VULKAN_DEVICE=$(echo "$vulkan_output" | grep -i "deviceName" | head -1 | cut -d'=' -f2 | xargs)
|
||||
if [ -z "$VULKAN_DEVICE" ]; then
|
||||
VULKAN_DEVICE="Vulkan-compatible GPU"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
HAS_VULKAN="no"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Detect hardware and recommend best engine
|
||||
get_engine_recommendation() {
|
||||
detect_nvidia_gpu || true
|
||||
detect_vulkan || true
|
||||
|
||||
# Get RAM info
|
||||
local TOTAL_RAM_GB=$(free -g 2>/dev/null | awk '/^Mem:/{print $2}' || echo "0")
|
||||
|
||||
if [[ "$HAS_NVIDIA_GPU" == "yes" ]]; then
|
||||
# NVIDIA GPU detected - whisper.cpp can use CUDA
|
||||
echo "whisper_cpp:✓:NVIDIA GPU detected ($GPU_NAME) - Best performance with whisper.cpp"
|
||||
elif [[ "$HAS_VULKAN" == "yes" ]]; then
|
||||
# Non-NVIDIA GPU with Vulkan support
|
||||
echo "whisper_cpp:✓:$VULKAN_DEVICE detected - Great performance with whisper.cpp Vulkan"
|
||||
elif [ "$TOTAL_RAM_GB" -ge 8 ]; then
|
||||
# No GPU but decent RAM
|
||||
echo "whisper_cpp:✓:No GPU detected, but ${TOTAL_RAM_GB}GB RAM - whisper.cpp CPU mode"
|
||||
else
|
||||
# Low RAM, no GPU
|
||||
echo "vosk:⚠:Low RAM (${TOTAL_RAM_GB}GB) and no GPU - VOSK recommended for best performance"
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect GI_TYPELIB_PATH for cross-distro compatibility
|
||||
detect_typelib_path() {
|
||||
# Try pkg-config first (most reliable)
|
||||
@@ -318,86 +368,103 @@ 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 "All 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 || true
|
||||
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
|
||||
|
||||
# Get hardware recommendation
|
||||
local RECOMMENDATION=$(get_engine_recommendation)
|
||||
local RECOMMENDED_ENGINE=$(echo "$RECOMMENDATION" | cut -d':' -f1)
|
||||
local RECOMMENDED_ICON=$(echo "$RECOMMENDATION" | cut -d':' -f2)
|
||||
local RECOMMENDED_REASON=$(echo "$RECOMMENDATION" | cut -d':' -f3-)
|
||||
|
||||
echo "Hardware: $RECOMMENDED_REASON"
|
||||
echo ""
|
||||
|
||||
# Step 2: Choose installation type
|
||||
print_header "Step 2: Installation Type"
|
||||
echo "Choose what to install:"
|
||||
# Step 2: Choose speech recognition engine
|
||||
print_header "Step 2: Choose Speech Recognition Engine"
|
||||
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 " │ 1. WHISPER.CPP ★ RECOMMENDED │"
|
||||
echo " │ • Fastest, most accurate, works with any GPU │"
|
||||
echo " │ • Supports NVIDIA (CUDA), AMD, Intel (Vulkan) │"
|
||||
echo " │ • CPU-only mode available for older systems │"
|
||||
echo " │ • Models: tiny (39MB) to large (1.5GB) │"
|
||||
echo " │ • 99+ languages with auto-detection │"
|
||||
echo " └─────────────────────────────────────────────────────────────┘"
|
||||
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 " ┌─────────────────────────────────────────────────────────────┐"
|
||||
echo " │ 2. WHISPER (OpenAI) │"
|
||||
echo " │ • PyTorch-based, high accuracy │"
|
||||
echo " │ • Only supports NVIDIA GPUs (CUDA) │"
|
||||
echo " │ • Larger download (~2GB with CUDA) │"
|
||||
echo " │ • Good for development/research │"
|
||||
echo " └─────────────────────────────────────────────────────────────┘"
|
||||
echo ""
|
||||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
||||
echo " │ 3. VOSK │"
|
||||
echo " │ • Lightweight and fast │"
|
||||
echo " │ • Works on older/low-RAM systems │"
|
||||
echo " │ • ~40MB download │"
|
||||
echo " │ • Good for basic dictation needs │"
|
||||
echo " └─────────────────────────────────────────────────────────────┘"
|
||||
echo ""
|
||||
|
||||
# Show recommendation
|
||||
case "$RECOMMENDED_ENGINE" in
|
||||
whisper_cpp)
|
||||
echo " → Recommendation: whisper.cpp (best performance for your hardware)"
|
||||
DEFAULT_CHOICE="1"
|
||||
;;
|
||||
vosk)
|
||||
echo " → Recommendation: VOSK (lightweight option for your system)"
|
||||
DEFAULT_CHOICE="3"
|
||||
;;
|
||||
*)
|
||||
echo " → Recommendation: whisper.cpp (best overall experience)"
|
||||
DEFAULT_CHOICE="1"
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
|
||||
read -p "Choose installation type [1-2] (default: 1): " ENGINE_CHOICE
|
||||
ENGINE_CHOICE=${ENGINE_CHOICE:-1}
|
||||
read -p "Choose engine [1-3] (default: $DEFAULT_CHOICE): " ENGINE_CHOICE
|
||||
ENGINE_CHOICE=${ENGINE_CHOICE:-$DEFAULT_CHOICE}
|
||||
|
||||
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)"
|
||||
case "$ENGINE_CHOICE" in
|
||||
1)
|
||||
SELECTED_ENGINE="whisper_cpp"
|
||||
ENGINE_DISPLAY="Whisper.cpp (Recommended)"
|
||||
;;
|
||||
2)
|
||||
SELECTED_ENGINE="whisper"
|
||||
ENGINE_DISPLAY="Whisper (OpenAI)"
|
||||
;;
|
||||
3)
|
||||
SELECTED_ENGINE="vosk"
|
||||
ENGINE_DISPLAY="VOSK (Lightweight)"
|
||||
;;
|
||||
*)
|
||||
SELECTED_ENGINE="whisper_cpp"
|
||||
ENGINE_DISPLAY="Whisper.cpp (Recommended)"
|
||||
;;
|
||||
esac
|
||||
|
||||
# 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."
|
||||
# Step 3: Model download preference
|
||||
print_header "Step 3: Model Download"
|
||||
echo ""
|
||||
echo " 1. Download now (recommended) - Faster first run"
|
||||
echo " 2. Download later - Smaller install, models download on first use"
|
||||
echo "Speech recognition models can be downloaded now or later."
|
||||
echo ""
|
||||
echo " 1. Download now (recommended)"
|
||||
echo " • Faster first run - ready to use immediately"
|
||||
echo " • Offline capable right after install"
|
||||
echo ""
|
||||
echo " 2. Download later"
|
||||
echo " • Smaller initial install"
|
||||
echo " • Models download automatically on first use"
|
||||
echo ""
|
||||
|
||||
read -p "Download models now? [1-2] (default: 1): " MODELS_CHOICE
|
||||
@@ -405,21 +472,20 @@ EOF
|
||||
|
||||
if [[ "$MODELS_CHOICE" == "2" ]]; then
|
||||
SKIP_MODELS="yes"
|
||||
MODELS_DISPLAY="Download on first use"
|
||||
else
|
||||
MODELS_DISPLAY="Download now (recommended)"
|
||||
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 " Speech Engine: $ENGINE_DISPLAY"
|
||||
echo " Models: $MODELS_DISPLAY"
|
||||
echo " Install Location: ${INSTALL_DIR:-\$HOME/.local/share/vocalinux}"
|
||||
echo ""
|
||||
read -p "Press Enter to continue with installation, or Ctrl+C to cancel..."
|
||||
echo ""
|
||||
|
||||
# Set non-interactive to skip the old prompt
|
||||
NON_INTERACTIVE="yes"
|
||||
}
|
||||
|
||||
# Detect distribution
|
||||
@@ -453,13 +519,35 @@ else
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if interactive mode is requested
|
||||
# Handle installation mode selection
|
||||
if [[ "$INTERACTIVE_MODE" == "ask" ]]; then
|
||||
# Running via curl pipe but we have a terminal - ask user preference
|
||||
echo ""
|
||||
echo "Installation Mode:"
|
||||
echo " 1. Interactive (recommended) - guided setup with recommendations"
|
||||
echo " 2. Automatic - quick install with defaults (whisper.cpp)"
|
||||
echo ""
|
||||
read -p "Choose mode [1-2] (default: 1): " MODE_CHOICE
|
||||
MODE_CHOICE=${MODE_CHOICE:-1}
|
||||
|
||||
if [[ "$MODE_CHOICE" == "2" ]]; then
|
||||
AUTO_MODE="yes"
|
||||
INTERACTIVE_MODE="no"
|
||||
NON_INTERACTIVE="yes"
|
||||
else
|
||||
INTERACTIVE_MODE="yes"
|
||||
NON_INTERACTIVE="no"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Run interactive installation if selected
|
||||
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"
|
||||
print_error "Please run from a terminal, or use automatic mode:"
|
||||
print_error " curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh | bash -s -- --auto"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -467,20 +555,12 @@ if [[ "$INTERACTIVE_MODE" == "yes" ]]; then
|
||||
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 || true
|
||||
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
|
||||
# Set default engine for auto/non-interactive mode
|
||||
if [[ "$NON_INTERACTIVE" == "yes" ]] && [[ -z "$SELECTED_ENGINE" ]]; then
|
||||
# Default to whisper.cpp for best performance
|
||||
SELECTED_ENGINE="whisper_cpp"
|
||||
print_info "Automatic mode: Installing with whisper.cpp (default engine)"
|
||||
print_info "For other engines, use: --engine=whisper or --engine=vosk"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
@@ -524,13 +604,13 @@ install_system_dependencies() {
|
||||
fi
|
||||
|
||||
# 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 build-essential portaudio19-dev python3-venv pkg-config wget curl unzip"
|
||||
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 build-essential portaudio19-dev python3-venv pkg-config wget curl unzip vulkan-tools"
|
||||
local APT_PACKAGES_DEBIAN_BASE="python3-pip python3-gi python3-gi-cairo gir1.2-gtk-3.0 libcairo2-dev python3-dev build-essential portaudio19-dev python3-venv pkg-config 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 pkg-config wget curl unzip"
|
||||
local PACMAN_PACKAGES="python-pip python-gobject gtk3 libappindicator-gtk3 gobject-introspection python-cairo portaudio python-virtualenv pkg-config wget curl unzip base-devel"
|
||||
local ZYPPER_PACKAGES="python3-pip python3-gobject python3-gobject-cairo gtk3 libappindicator-gtk3 gobject-introspection-devel python3-devel portaudio-devel python3-virtualenv pkg-config wget curl unzip"
|
||||
local DNF_PACKAGES="python3-pip python3-gobject gtk3 libappindicator-gtk3 gobject-introspection-devel python3-devel portaudio-devel python3-virtualenv pkg-config wget curl unzip vulkan-tools"
|
||||
local PACMAN_PACKAGES="python-pip python-gobject gtk3 libappindicator-gtk3 gobject-introspection python-cairo portaudio python-virtualenv pkg-config wget curl unzip base-devel vulkan-tools"
|
||||
local ZYPPER_PACKAGES="python3-pip python3-gobject python3-gobject-cairo gtk3 libappindicator-gtk3 gobject-introspection-devel python3-devel portaudio-devel python3-virtualenv pkg-config wget curl unzip vulkan-tools"
|
||||
# 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 pkgconf"
|
||||
# Alpine Linux uses apk and has musl libc
|
||||
@@ -1241,33 +1321,111 @@ install_python_package() {
|
||||
else
|
||||
print_info "Installing Vocalinux..."
|
||||
|
||||
# Install the package with logging
|
||||
# Install the package with logging (includes pywhispercpp by default)
|
||||
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'
|
||||
# Engine installation logic:
|
||||
# - SELECTED_ENGINE is set by interactive mode or --engine flag
|
||||
# - Default is whisper_cpp for best performance
|
||||
case "${SELECTED_ENGINE:-whisper_cpp}" in
|
||||
whisper_cpp)
|
||||
print_info ""
|
||||
print_info "╔════════════════════════════════════════════════════════╗"
|
||||
print_info "║ Installing WHISPER.CPP (Recommended) ║"
|
||||
print_info "╠════════════════════════════════════════════════════════╣"
|
||||
print_info "║ • Fastest speech recognition ║"
|
||||
print_info "║ • Works with any GPU: NVIDIA, AMD, Intel ║"
|
||||
print_info "║ • Uses Vulkan for GPU acceleration ║"
|
||||
print_info "║ • CPU-only mode available ║"
|
||||
print_info "╚════════════════════════════════════════════════════════╝"
|
||||
print_info ""
|
||||
print_info "whisper.cpp is included by default and ready to use!"
|
||||
print_info ""
|
||||
|
||||
# Check for GPU acceleration support
|
||||
detect_vulkan || true
|
||||
if [[ "$HAS_VULKAN" == "yes" ]]; then
|
||||
print_info "✓ Vulkan detected: $VULKAN_DEVICE"
|
||||
print_info " GPU acceleration will be used automatically"
|
||||
elif [[ "$HAS_NVIDIA_GPU" == "yes" ]]; then
|
||||
print_info "✓ NVIDIA GPU detected: $GPU_NAME"
|
||||
print_info " CUDA acceleration will be used automatically"
|
||||
else
|
||||
print_info "ℹ No GPU detected - whisper.cpp will use CPU mode"
|
||||
print_info " CPU mode is still very fast!"
|
||||
fi
|
||||
echo ""
|
||||
;;
|
||||
|
||||
whisper)
|
||||
print_info "Installing Whisper (OpenAI) with PyTorch..."
|
||||
print_info "Note: This engine requires NVIDIA GPU for acceleration"
|
||||
print_info " For AMD/Intel GPUs, whisper.cpp is recommended"
|
||||
|
||||
# Install PyTorch and whisper
|
||||
pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu --log "$PIP_LOG_FILE" || {
|
||||
print_warning "Failed to install PyTorch."
|
||||
}
|
||||
pip install openai-whisper --log "$PIP_LOG_FILE" || {
|
||||
print_warning "Failed to install Whisper."
|
||||
print_warning "Falling back to VOSK."
|
||||
}
|
||||
|
||||
# Create config with whisper as default
|
||||
local WHISPER_CONFIG="$CONFIG_DIR/config.json"
|
||||
if [ ! -f "$WHISPER_CONFIG" ]; then
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
cat > "$WHISPER_CONFIG" << 'WHISPER_CONFIG'
|
||||
{
|
||||
"speech_recognition": {
|
||||
"engine": "whisper",
|
||||
"model_size": "tiny",
|
||||
"vosk_model_size": "small",
|
||||
"whisper_model_size": "tiny",
|
||||
"whisper_cpp_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
|
||||
}
|
||||
}
|
||||
WHISPER_CONFIG
|
||||
fi
|
||||
;;
|
||||
|
||||
vosk)
|
||||
print_info "Installing VOSK (lightweight option)..."
|
||||
print_info "VOSK is fast and works well on older systems."
|
||||
|
||||
# Create config with vosk as default
|
||||
local VOSK_CONFIG_FILE="$CONFIG_DIR/config.json"
|
||||
if [ ! -f "$VOSK_CONFIG_FILE" ]; then
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
cat > "$VOSK_CONFIG_FILE" << 'VOSK_CONFIG'
|
||||
{
|
||||
"speech_recognition": {
|
||||
"engine": "vosk",
|
||||
"model_size": "small",
|
||||
"vosk_model_size": "small",
|
||||
"whisper_model_size": "tiny",
|
||||
"whisper_cpp_model_size": "tiny",
|
||||
"vad_sensitivity": 3,
|
||||
"silence_timeout": 2.0
|
||||
},
|
||||
@@ -1288,98 +1446,9 @@ install_python_package() {
|
||||
}
|
||||
}
|
||||
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
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Verify installation
|
||||
@@ -1624,6 +1693,83 @@ install_vosk_models() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to download and install whisper.cpp tiny model
|
||||
install_whispercpp_model() {
|
||||
print_info "Installing whisper.cpp tiny model (~39MB)..."
|
||||
|
||||
# Create whisper.cpp models directory
|
||||
local WHISPERCPP_DIR="$DATA_DIR/models/whispercpp"
|
||||
mkdir -p "$WHISPERCPP_DIR"
|
||||
|
||||
# whisper.cpp tiny model URL and path
|
||||
local TINY_MODEL_URL="https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin"
|
||||
local TINY_MODEL_PATH="$WHISPERCPP_DIR/ggml-tiny.bin"
|
||||
|
||||
# Check if model already exists
|
||||
if [ -f "$TINY_MODEL_PATH" ]; then
|
||||
print_info "whisper.cpp 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.cpp 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.cpp model will be downloaded on first application run."
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_info "Downloading whisper.cpp 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.cpp 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.cpp 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.cpp tiny model installed successfully ($MODEL_SIZE)"
|
||||
|
||||
# Create a marker file to indicate this model was pre-installed
|
||||
echo "$(date)" > "$WHISPERCPP_DIR/.vocalinux_preinstalled"
|
||||
|
||||
return 0
|
||||
else
|
||||
print_error "whisper.cpp model installation failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to install desktop entry with error handling
|
||||
install_desktop_entry() {
|
||||
print_info "Installing desktop entry..."
|
||||
@@ -1748,27 +1894,28 @@ 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
|
||||
# Install models based on selected engine
|
||||
# whisper.cpp is now the default engine
|
||||
if [ "$SKIP_MODELS" = "no" ]; then
|
||||
# Check which engines are installed and download appropriate models
|
||||
|
||||
# Install whisper.cpp model (default engine)
|
||||
if "$VENV_DIR/bin/python" -c "from pywhispercpp.model import Model" 2>/dev/null; then
|
||||
print_info "whisper.cpp is installed - downloading tiny model (default engine)..."
|
||||
install_whispercpp_model || print_warning "whisper.cpp model download failed - model will be downloaded on first run"
|
||||
fi
|
||||
|
||||
# Install OpenAI Whisper model if whisper engine is installed
|
||||
if "$VENV_DIR/bin/python" -c "import whisper" 2>/dev/null; then
|
||||
print_info "Whisper is installed - downloading tiny model (default engine)..."
|
||||
print_info "Whisper (OpenAI) is installed - downloading tiny model..."
|
||||
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"
|
||||
print_info "Skipping model downloads (--skip-models specified)"
|
||||
print_info "Models will be downloaded automatically on first application run"
|
||||
fi
|
||||
|
||||
# Install VOSK models (always useful as fallback, and required for VOSK-only mode)
|
||||
# Install VOSK models (always useful as fallback)
|
||||
if [ "$SKIP_MODELS" = "no" ]; then
|
||||
install_vosk_models || print_warning "VOSK model installation failed - models will be downloaded on first run"
|
||||
else
|
||||
|
||||
@@ -39,6 +39,7 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"vosk>=0.3.45",
|
||||
"pywhispercpp>=1.2.0",
|
||||
"pydub>=0.25.1",
|
||||
"pynput>=1.7.6",
|
||||
"evdev>=1.6.0; sys_platform == 'linux'",
|
||||
|
||||
@@ -57,8 +57,8 @@ def parse_arguments():
|
||||
parser.add_argument(
|
||||
"--engine",
|
||||
type=str,
|
||||
choices=["vosk", "whisper"],
|
||||
help="Speech recognition engine to use",
|
||||
choices=["vosk", "whisper", "whisper_cpp"],
|
||||
help="Speech recognition engine to use (whisper_cpp recommended for best performance)",
|
||||
)
|
||||
parser.add_argument("--wayland", action="store_true", help="Force Wayland compatibility mode")
|
||||
return parser.parse_args()
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
Speech recognition manager module for Vocalinux.
|
||||
|
||||
This module provides a unified interface to different speech recognition engines,
|
||||
currently supporting VOSK and Whisper.
|
||||
currently supporting VOSK, Whisper, and whisper.cpp.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable, List, Optional
|
||||
@@ -16,6 +17,11 @@ from typing import Callable, List, Optional
|
||||
from ..common_types import RecognitionState
|
||||
from ..ui.audio_feedback import play_error_sound, play_start_sound, play_stop_sound
|
||||
from ..utils.vosk_model_info import VOSK_MODEL_INFO
|
||||
from ..utils.whispercpp_model_info import (
|
||||
WHISPERCPP_MODEL_INFO,
|
||||
get_model_path,
|
||||
is_model_downloaded,
|
||||
)
|
||||
from .command_processor import CommandProcessor
|
||||
|
||||
|
||||
@@ -368,6 +374,8 @@ class SpeechRecognitionManager:
|
||||
self._init_vosk()
|
||||
elif engine == "whisper":
|
||||
self._init_whisper()
|
||||
elif engine == "whisper_cpp":
|
||||
self._init_whispercpp()
|
||||
else:
|
||||
raise ValueError(f"Unsupported speech recognition engine: {engine}")
|
||||
|
||||
@@ -554,6 +562,273 @@ class SpeechRecognitionManager:
|
||||
logger.error(f"Error in Whisper transcription: {e}", exc_info=True)
|
||||
return ""
|
||||
|
||||
def _init_whispercpp(self):
|
||||
"""Initialize the whisper.cpp speech recognition engine."""
|
||||
import time
|
||||
|
||||
try:
|
||||
from pywhispercpp.model import Model
|
||||
|
||||
# Validate model size for whisper.cpp
|
||||
valid_models = list(WHISPERCPP_MODEL_INFO.keys())
|
||||
if self.model_size not in valid_models:
|
||||
logger.warning(
|
||||
f"Model size '{self.model_size}' not valid for whisper.cpp. "
|
||||
f"Valid options: {valid_models}. Using 'tiny' instead."
|
||||
)
|
||||
self.model_size = "tiny"
|
||||
|
||||
# Check if model is downloaded
|
||||
model_path = get_model_path(self.model_size)
|
||||
|
||||
if not os.path.exists(model_path):
|
||||
if self._defer_download:
|
||||
logger.info(
|
||||
f"whisper.cpp model '{self.model_size}' not found at {model_path}. "
|
||||
"Will download when needed."
|
||||
)
|
||||
self._model_initialized = False
|
||||
return # Don't block startup
|
||||
else:
|
||||
logger.info(f"Downloading whisper.cpp '{self.model_size}' model...")
|
||||
self._download_whispercpp_model()
|
||||
|
||||
# Detect and log compute backend
|
||||
from ..utils.whispercpp_model_info import (
|
||||
ComputeBackend,
|
||||
detect_compute_backend,
|
||||
get_backend_display_name,
|
||||
)
|
||||
|
||||
backend, backend_info = detect_compute_backend()
|
||||
logger.info(f"whisper.cpp backend selection priority: Vulkan -> CUDA -> CPU")
|
||||
logger.info(
|
||||
f"whisper.cpp using {get_backend_display_name(backend)} backend: {backend_info}"
|
||||
)
|
||||
|
||||
# Log hardware summary
|
||||
import psutil
|
||||
|
||||
total_ram_gb = psutil.virtual_memory().total // (1024**3)
|
||||
logger.info(f"whisper.cpp hardware: {backend} | {backend_info} | RAM: {total_ram_gb}GB")
|
||||
|
||||
# Validate model file exists and get size
|
||||
if os.path.exists(model_path):
|
||||
model_size_mb = os.path.getsize(model_path) / (1024 * 1024)
|
||||
logger.info(f"whisper.cpp model file: {model_path} ({model_size_mb:.1f} MB)")
|
||||
else:
|
||||
logger.error(f"whisper.cpp model file not found: {model_path}")
|
||||
raise FileNotFoundError(f"Model file not found: {model_path}")
|
||||
|
||||
logger.info(f"Loading whisper.cpp '{self.model_size}' model...")
|
||||
# Ensure previous model is released if re-initializing
|
||||
self.model = None
|
||||
|
||||
# Load model with pywhispercpp
|
||||
# It auto-detects the best backend (Vulkan, CUDA, or CPU)
|
||||
# Use all available CPU cores for best performance
|
||||
import multiprocessing
|
||||
|
||||
n_threads = multiprocessing.cpu_count()
|
||||
cpu_count = multiprocessing.cpu_count()
|
||||
|
||||
load_start_time = time.time()
|
||||
self.model = Model(model_path, n_threads=n_threads)
|
||||
load_duration = time.time() - load_start_time
|
||||
|
||||
logger.info(
|
||||
f"whisper.cpp configured with n_threads={n_threads} (detected {cpu_count} CPUs)"
|
||||
)
|
||||
logger.info(f"whisper.cpp model loaded in {load_duration:.2f}s ({backend} backend)")
|
||||
|
||||
self._model_initialized = True
|
||||
logger.info("whisper.cpp engine initialized successfully.")
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import pywhispercpp: {e}")
|
||||
logger.error(f"Python path: {sys.path}")
|
||||
logger.error("Please install with: pip install pywhispercpp")
|
||||
self.state = RecognitionState.ERROR
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize whisper.cpp engine: {e}", exc_info=True)
|
||||
self.state = RecognitionState.ERROR
|
||||
raise
|
||||
|
||||
def _transcribe_with_whispercpp(self, audio_buffer: List[bytes]) -> str:
|
||||
"""
|
||||
Transcribe audio buffer using whisper.cpp.
|
||||
|
||||
Args:
|
||||
audio_buffer: List of audio data chunks (16-bit PCM at 16kHz)
|
||||
|
||||
Returns:
|
||||
Transcribed text
|
||||
"""
|
||||
import time
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
|
||||
if not audio_buffer:
|
||||
return ""
|
||||
|
||||
# Convert audio buffer to numpy array
|
||||
audio_data = np.frombuffer(b"".join(audio_buffer), dtype=np.int16)
|
||||
|
||||
# Convert to float32 and normalize to [-1, 1]
|
||||
audio_float = audio_data.astype(np.float32) / 32768.0
|
||||
|
||||
duration = len(audio_float) / 16000.0 # 16kHz sample rate
|
||||
num_chunks = len(audio_buffer)
|
||||
logger.debug(
|
||||
f"whisper.cpp audio preprocessing: {len(audio_float)} samples, {duration:.2f}s, {num_chunks} chunks"
|
||||
)
|
||||
|
||||
# Prepare language parameter
|
||||
lang = self.language
|
||||
if self.language == "en-us":
|
||||
lang = "en"
|
||||
elif self.language == "auto":
|
||||
lang = None # Auto-detect
|
||||
|
||||
logger.debug(f"whisper.cpp using language: {lang or 'auto-detect'}")
|
||||
|
||||
# Transcribe with whisper.cpp
|
||||
# pywhispercpp expects audio as numpy array
|
||||
transcribe_start = time.time()
|
||||
segments = self.model.transcribe(audio_float, language=lang)
|
||||
transcribe_duration = time.time() - transcribe_start
|
||||
|
||||
# Extract text from segments
|
||||
text_parts = []
|
||||
for segment in segments:
|
||||
if hasattr(segment, "text") and segment.text:
|
||||
text_parts.append(segment.text.strip())
|
||||
|
||||
text = " ".join(text_parts).strip()
|
||||
num_segments = len(text_parts)
|
||||
|
||||
# Calculate RTF (Real-Time Factor)
|
||||
rtf = transcribe_duration / duration if duration > 0 else 0
|
||||
|
||||
if text:
|
||||
logger.info(f"whisper.cpp transcribed: '{text}'")
|
||||
logger.info(
|
||||
f"whisper.cpp transcription completed in {transcribe_duration:.3f}s for {duration:.2f}s audio (RTF: {rtf:.2f}x) - {num_segments} segments"
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"whisper.cpp returned empty transcription ({transcribe_duration:.3f}s)"
|
||||
)
|
||||
|
||||
return text
|
||||
|
||||
except Exception as e:
|
||||
audio_info = (
|
||||
f"audio buffer: {len(audio_buffer)} chunks"
|
||||
if audio_buffer
|
||||
else "empty audio buffer"
|
||||
)
|
||||
logger.error(f"Error in whisper.cpp transcription: {e} ({audio_info})", exc_info=True)
|
||||
return ""
|
||||
|
||||
def _download_whispercpp_model(self):
|
||||
"""Download a whisper.cpp model with progress tracking."""
|
||||
import requests
|
||||
|
||||
self._download_cancelled = False
|
||||
|
||||
model_info = WHISPERCPP_MODEL_INFO.get(self.model_size)
|
||||
if not model_info:
|
||||
raise ValueError(f"Unknown whisper.cpp model size: {self.model_size}")
|
||||
|
||||
url = model_info["url"]
|
||||
model_path = get_model_path(self.model_size)
|
||||
temp_file = model_path + ".tmp"
|
||||
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(model_path), exist_ok=True)
|
||||
|
||||
logger.info(f"Downloading whisper.cpp {self.model_size} model to {model_path}")
|
||||
logger.info(f"Downloading from {url}")
|
||||
|
||||
try:
|
||||
response = requests.get(url, stream=True)
|
||||
response.raise_for_status()
|
||||
|
||||
total_size = int(response.headers.get("content-length", 0))
|
||||
downloaded_size = 0
|
||||
start_time = time.time()
|
||||
last_update_time = start_time
|
||||
chunk_size = 8192 # 8KB chunks
|
||||
|
||||
with open(temp_file, "wb") as f:
|
||||
for data in response.iter_content(chunk_size=chunk_size):
|
||||
if self._download_cancelled:
|
||||
logger.info("Download cancelled by user")
|
||||
f.close()
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
raise RuntimeError("Download cancelled")
|
||||
|
||||
f.write(data)
|
||||
downloaded_size += len(data)
|
||||
|
||||
# Update progress callback
|
||||
current_time = time.time()
|
||||
if (
|
||||
self._download_progress_callback
|
||||
and (current_time - last_update_time) >= 0.1
|
||||
):
|
||||
elapsed = current_time - start_time
|
||||
if elapsed > 0:
|
||||
speed_mbps = (downloaded_size / (1024 * 1024)) / elapsed
|
||||
else:
|
||||
speed_mbps = 0
|
||||
|
||||
if total_size > 0:
|
||||
progress = downloaded_size / total_size
|
||||
remaining_mb = (total_size - downloaded_size) / (1024 * 1024)
|
||||
if speed_mbps > 0:
|
||||
eta_seconds = remaining_mb / speed_mbps
|
||||
eta_str = (
|
||||
f"{int(eta_seconds)}s"
|
||||
if eta_seconds < 60
|
||||
else f"{int(eta_seconds / 60)}m {int(eta_seconds % 60)}s"
|
||||
)
|
||||
else:
|
||||
eta_str = "--"
|
||||
status = f"{downloaded_size / (1024 * 1024):.1f} / {total_size / (1024 * 1024):.1f} MB • {speed_mbps:.1f} MB/s • ETA: {eta_str}"
|
||||
else:
|
||||
progress = 0
|
||||
status = (
|
||||
f"{downloaded_size / (1024 * 1024):.1f} MB • {speed_mbps:.1f} MB/s"
|
||||
)
|
||||
|
||||
self._download_progress_callback(progress, speed_mbps, status)
|
||||
last_update_time = current_time
|
||||
|
||||
logger.info(f"Download progress: {progress * 100:.1f}% - {status}")
|
||||
|
||||
# Rename temp file to final
|
||||
os.rename(temp_file, model_path)
|
||||
logger.info("whisper.cpp model downloaded successfully")
|
||||
|
||||
if self._download_progress_callback:
|
||||
self._download_progress_callback(1.0, 0, "Complete!")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Failed to download whisper.cpp model from {url}: {e}")
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
raise RuntimeError(f"Failed to download whisper.cpp model: {e}") from e
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred during whisper.cpp model download: {e}")
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
raise
|
||||
|
||||
def _get_vosk_model_path(self) -> str:
|
||||
"""Get the path to the VOSK model based on the selected size and language."""
|
||||
model_name = self.vosk_model_map.get(self.model_size, self.vosk_model_map["small"])
|
||||
@@ -1264,6 +1539,9 @@ class SpeechRecognitionManager:
|
||||
elif self.engine == "whisper":
|
||||
text = self._transcribe_with_whisper(self.audio_buffer)
|
||||
|
||||
elif self.engine == "whisper_cpp":
|
||||
text = self._transcribe_with_whispercpp(self.audio_buffer)
|
||||
|
||||
else:
|
||||
logger.error(f"Unknown engine: {self.engine}")
|
||||
return
|
||||
@@ -1357,6 +1635,8 @@ class SpeechRecognitionManager:
|
||||
self._init_vosk()
|
||||
elif self.engine == "whisper":
|
||||
self._init_whisper()
|
||||
elif self.engine == "whisper_cpp":
|
||||
self._init_whispercpp()
|
||||
else:
|
||||
raise ValueError(f"Unsupported engine during reconfigure: {self.engine}")
|
||||
logger.info("Speech engine re-initialized successfully.")
|
||||
|
||||
@@ -18,11 +18,12 @@ CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
|
||||
# Default configuration
|
||||
DEFAULT_CONFIG = {
|
||||
"speech_recognition": { # Changed section name
|
||||
"engine": "whisper", # "vosk" or "whisper" - whisper is default for better accuracy
|
||||
"language": "auto", # Auto-detect language (Whisper only)
|
||||
"engine": "whisper_cpp", # "vosk", "whisper", or "whisper_cpp" - whisper_cpp is default for best performance
|
||||
"language": "auto", # Auto-detect language (Whisper/whisper.cpp 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
|
||||
"whisper_cpp_model_size": "tiny", # Default model for whisper.cpp engine
|
||||
"vad_sensitivity": 3, # Voice Activity Detection sensitivity (1-5)
|
||||
"silence_timeout": 2.0, # Seconds of silence before stopping
|
||||
},
|
||||
@@ -194,7 +195,7 @@ class ConfigManager:
|
||||
"""Get the saved model size for a specific engine.
|
||||
|
||||
Args:
|
||||
engine: The engine name ("vosk" or "whisper")
|
||||
engine: The engine name ("vosk", "whisper", or "whisper_cpp")
|
||||
|
||||
Returns:
|
||||
The model size for the engine, or the default if not found
|
||||
|
||||
@@ -29,6 +29,13 @@ from gi.repository import Gdk, GLib, Gtk, Pango # noqa: E402
|
||||
|
||||
from ..common_types import RecognitionState # noqa: E402
|
||||
from ..utils.vosk_model_info import SUPPORTED_LANGUAGES, VOSK_MODEL_INFO # noqa: E402
|
||||
from ..utils.whispercpp_model_info import (
|
||||
WHISPERCPP_MODEL_INFO,
|
||||
detect_compute_backend,
|
||||
get_backend_display_name,
|
||||
)
|
||||
from ..utils.whispercpp_model_info import get_recommended_model as get_recommended_whispercpp_model
|
||||
from ..utils.whispercpp_model_info import is_model_downloaded as is_whispercpp_model_downloaded
|
||||
from .keyboard_backends import SHORTCUT_DISPLAY_NAMES, SUPPORTED_SHORTCUTS # noqa: E402
|
||||
|
||||
# Avoid circular imports for type checking
|
||||
@@ -52,6 +59,13 @@ ENGINE_MODELS = {
|
||||
"medium",
|
||||
"large",
|
||||
], # Add more whisper sizes if needed
|
||||
"whisper_cpp": [
|
||||
"tiny",
|
||||
"base",
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
], # whisper.cpp models (ggml format)
|
||||
}
|
||||
|
||||
# Whisper model metadata for display
|
||||
@@ -63,6 +77,42 @@ WHISPER_MODEL_INFO = {
|
||||
"large": {"size_mb": 2900, "desc": "Highest accuracy, slowest", "params": "1550M"},
|
||||
}
|
||||
|
||||
|
||||
def get_available_engines():
|
||||
"""
|
||||
Detect which speech recognition engines are available/installed.
|
||||
Returns a dictionary of engine_name -> availability (bool).
|
||||
"""
|
||||
engines = {"vosk": False, "whisper": False, "whisper_cpp": False}
|
||||
|
||||
# Check VOSK
|
||||
try:
|
||||
import vosk
|
||||
|
||||
engines["vosk"] = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Check OpenAI Whisper
|
||||
try:
|
||||
import whisper
|
||||
|
||||
engines["whisper"] = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Check whisper.cpp (pywhispercpp)
|
||||
try:
|
||||
from pywhispercpp.model import Model
|
||||
|
||||
engines["whisper_cpp"] = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
logger.debug(f"Available engines: {engines}")
|
||||
return engines
|
||||
|
||||
|
||||
# Models directory
|
||||
MODELS_DIR = os.path.expanduser("~/.local/share/vocalinux/models")
|
||||
SYSTEM_MODELS_DIRS = [
|
||||
@@ -635,7 +685,25 @@ class SettingsDialog(Gtk.Dialog):
|
||||
|
||||
# Dialog configuration - Close button only (instant-apply pattern)
|
||||
self.add_button("_Close", Gtk.ResponseType.CLOSE)
|
||||
self.set_default_size(520, 680)
|
||||
# Calculate dialog size - 75% of screen height for better visibility
|
||||
display = Gdk.Display.get_default()
|
||||
if display:
|
||||
monitor = display.get_primary_monitor()
|
||||
if not monitor and display.get_n_monitors() > 0:
|
||||
monitor = display.get_monitor(0)
|
||||
if monitor:
|
||||
geometry = monitor.get_geometry()
|
||||
screen_height = geometry.height
|
||||
screen_width = geometry.width
|
||||
else:
|
||||
screen_height = 1080 # Default fallback
|
||||
screen_width = 1920
|
||||
else:
|
||||
screen_height = 1080 # Default fallback
|
||||
screen_width = 1920
|
||||
dialog_height = int(screen_height * 0.75)
|
||||
dialog_width = min(520, int(screen_width * 0.8))
|
||||
self.set_default_size(dialog_width, dialog_height)
|
||||
self.get_style_context().add_class("settings-dialog")
|
||||
|
||||
# Main scrolled window for content
|
||||
@@ -1041,20 +1109,50 @@ class SettingsDialog(Gtk.Dialog):
|
||||
f"Starting dialog with settings: engine={self.current_engine}, model={self.current_model_size}"
|
||||
)
|
||||
|
||||
# Populate engine combo
|
||||
# Populate engine combo with only available engines
|
||||
available_engines = get_available_engines()
|
||||
available_count = 0
|
||||
|
||||
for engine in ENGINE_MODELS.keys():
|
||||
capitalized_engine = engine.capitalize()
|
||||
self.engine_combo.append(capitalized_engine, capitalized_engine)
|
||||
if available_engines.get(engine, False):
|
||||
capitalized_engine = engine.capitalize()
|
||||
self.engine_combo.append(capitalized_engine, capitalized_engine)
|
||||
available_count += 1
|
||||
|
||||
if available_count == 0:
|
||||
logger.error("No speech recognition engines available!")
|
||||
# Still add them so the UI works, but log the error
|
||||
for engine in ENGINE_MODELS.keys():
|
||||
capitalized_engine = engine.capitalize()
|
||||
self.engine_combo.append(capitalized_engine, capitalized_engine)
|
||||
else:
|
||||
logger.info(f"Populated {available_count} available engines: {available_engines}")
|
||||
|
||||
# Set engine active - check if current engine is available
|
||||
if not available_engines.get(self.current_engine, False):
|
||||
logger.warning(
|
||||
f"Current engine '{self.current_engine}' is not available, selecting first available"
|
||||
)
|
||||
# Find first available engine
|
||||
for engine in ENGINE_MODELS.keys():
|
||||
if available_engines.get(engine, False):
|
||||
self.current_engine = engine
|
||||
break
|
||||
|
||||
# Set engine active
|
||||
engine_text = self.current_engine.capitalize()
|
||||
logger.info(f"Setting active engine to: {engine_text}")
|
||||
if not self.engine_combo.set_active_id(engine_text):
|
||||
logger.warning("Could not set engine by ID, trying by index")
|
||||
if self.current_engine == "vosk":
|
||||
self.engine_combo.set_active(0)
|
||||
elif self.current_engine == "whisper":
|
||||
self.engine_combo.set_active(1)
|
||||
# Find index of current engine
|
||||
model = self.engine_combo.get_model()
|
||||
for i, row in enumerate(model):
|
||||
if row[0].lower() == self.current_engine.lower():
|
||||
self.engine_combo.set_active(i)
|
||||
break
|
||||
else:
|
||||
# Fallback to first available
|
||||
if self.engine_combo.get_model():
|
||||
self.engine_combo.set_active(0)
|
||||
|
||||
# Populate model options for the selected engine
|
||||
self._populate_model_options()
|
||||
@@ -1115,17 +1213,21 @@ class SettingsDialog(Gtk.Dialog):
|
||||
|
||||
downloaded_models = []
|
||||
smallest_model = None
|
||||
recommended_model, _ = (
|
||||
_get_recommended_whisper_model()
|
||||
if engine == "whisper"
|
||||
else _get_recommended_vosk_model()
|
||||
)
|
||||
if engine == "whisper":
|
||||
recommended_model, _ = _get_recommended_whisper_model()
|
||||
elif engine == "whisper_cpp":
|
||||
recommended_model, _ = get_recommended_whispercpp_model()
|
||||
else:
|
||||
recommended_model, _ = _get_recommended_vosk_model()
|
||||
|
||||
if engine in ENGINE_MODELS:
|
||||
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)
|
||||
elif engine == "whisper_cpp" and size in WHISPERCPP_MODEL_INFO:
|
||||
info = WHISPERCPP_MODEL_INFO[size]
|
||||
is_downloaded = is_whispercpp_model_downloaded(size)
|
||||
elif engine == "vosk" and size in VOSK_MODEL_INFO:
|
||||
info = VOSK_MODEL_INFO[size]
|
||||
is_downloaded = _is_vosk_model_downloaded(size, self.language)
|
||||
@@ -1186,7 +1288,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
current_lang == "auto" or not SUPPORTED_LANGUAGES.get(current_lang, {}).get("vosk")
|
||||
):
|
||||
self.language = "en-us"
|
||||
elif engine == "whisper" and not current_lang:
|
||||
elif engine in ["whisper", "whisper_cpp"] and not current_lang:
|
||||
self.language = "auto"
|
||||
|
||||
self._populate_model_options()
|
||||
@@ -1229,7 +1331,8 @@ class SettingsDialog(Gtk.Dialog):
|
||||
continue
|
||||
is_downloaded = _is_vosk_model_downloaded("small", lang_code)
|
||||
display_text += " ✓" if is_downloaded else " ↓"
|
||||
elif engine == "whisper":
|
||||
elif engine in ["whisper", "whisper_cpp"]:
|
||||
# Both Whisper and whisper.cpp support auto-detect
|
||||
if lang_code == "auto":
|
||||
display_text += " ⚠"
|
||||
else:
|
||||
@@ -1297,6 +1400,17 @@ class SettingsDialog(Gtk.Dialog):
|
||||
is_downloaded = _is_whisper_model_downloaded(model_name)
|
||||
recommended, reason = _get_recommended_whisper_model()
|
||||
extra_info = f"Parameters: {info['params']}"
|
||||
elif engine == "whisper_cpp":
|
||||
if model_name not in WHISPERCPP_MODEL_INFO:
|
||||
self.model_info_card.hide()
|
||||
return
|
||||
info = WHISPERCPP_MODEL_INFO[model_name]
|
||||
is_downloaded = is_whispercpp_model_downloaded(model_name)
|
||||
recommended, reason = get_recommended_whispercpp_model()
|
||||
backend, backend_info = detect_compute_backend()
|
||||
extra_info = (
|
||||
f"Parameters: {info['params']} • Backend: {get_backend_display_name(backend)}"
|
||||
)
|
||||
elif engine == "vosk":
|
||||
if model_name not in VOSK_MODEL_INFO:
|
||||
self.model_info_card.hide()
|
||||
@@ -1357,6 +1471,9 @@ class SettingsDialog(Gtk.Dialog):
|
||||
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 == "whisper_cpp" and not is_whispercpp_model_downloaded(model_name):
|
||||
needs_download = True
|
||||
model_info = WHISPERCPP_MODEL_INFO.get(model_name, {"size_mb": 39})
|
||||
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})
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
Whisper.cpp model information and hardware detection for Vocalinux.
|
||||
|
||||
This module provides model metadata and hardware acceleration detection
|
||||
for whisper.cpp, supporting Vulkan, CUDA, and CPU backends.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Whisper.cpp model information
|
||||
# Models are downloaded from Hugging Face (ggml format)
|
||||
WHISPERCPP_MODEL_INFO = {
|
||||
"tiny": {
|
||||
"size_mb": 39,
|
||||
"params": "39M",
|
||||
"desc": "Fastest, lowest accuracy",
|
||||
"url": "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin",
|
||||
},
|
||||
"base": {
|
||||
"size_mb": 74,
|
||||
"params": "74M",
|
||||
"desc": "Fast, good for basic use",
|
||||
"url": "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.bin",
|
||||
},
|
||||
"small": {
|
||||
"size_mb": 244,
|
||||
"params": "244M",
|
||||
"desc": "Balanced speed/accuracy",
|
||||
"url": "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin",
|
||||
},
|
||||
"medium": {
|
||||
"size_mb": 769,
|
||||
"params": "769M",
|
||||
"desc": "High accuracy, slower",
|
||||
"url": "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-medium.bin",
|
||||
},
|
||||
"large": {
|
||||
"size_mb": 1550,
|
||||
"params": "1550M",
|
||||
"desc": "Highest accuracy, slowest",
|
||||
"url": "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3.bin",
|
||||
},
|
||||
}
|
||||
|
||||
# Available models list
|
||||
AVAILABLE_MODELS = list(WHISPERCPP_MODEL_INFO.keys())
|
||||
|
||||
|
||||
# Compute backend types
|
||||
class ComputeBackend:
|
||||
"""Compute backend options for whisper.cpp."""
|
||||
|
||||
VULKAN = "vulkan"
|
||||
CUDA = "cuda"
|
||||
CPU = "cpu"
|
||||
|
||||
|
||||
def detect_vulkan_support() -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Detect if Vulkan is available and get device info.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_available, device_name)
|
||||
"""
|
||||
try:
|
||||
# Check for vulkaninfo command
|
||||
result = subprocess.run(
|
||||
["vulkaninfo", "--summary"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
# Try to extract GPU name from output
|
||||
for line in result.stdout.split("\n"):
|
||||
if "deviceName" in line or "GPU" in line:
|
||||
device_name = line.split(":")[-1].strip()
|
||||
if device_name:
|
||||
logger.info(f"Vulkan support detected: {device_name}")
|
||||
return True, device_name
|
||||
logger.info("Vulkan support detected")
|
||||
return True, "Vulkan GPU"
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, Exception) as e:
|
||||
logger.debug(f"Vulkan detection failed: {e}")
|
||||
|
||||
return False, None
|
||||
|
||||
|
||||
def detect_cuda_support() -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Detect if NVIDIA CUDA is available and get device info.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_available, device_info)
|
||||
"""
|
||||
try:
|
||||
# Check for nvidia-smi
|
||||
result = subprocess.run(
|
||||
["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv,noheader"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
gpu_info = result.stdout.strip().split(",")
|
||||
if gpu_info:
|
||||
gpu_name = gpu_info[0].strip()
|
||||
gpu_memory = gpu_info[1].strip() if len(gpu_info) > 1 else "unknown"
|
||||
logger.info(f"CUDA support detected: {gpu_name} ({gpu_memory})")
|
||||
return True, f"{gpu_name} ({gpu_memory})"
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, Exception) as e:
|
||||
logger.debug(f"CUDA detection failed: {e}")
|
||||
|
||||
return False, None
|
||||
|
||||
|
||||
def detect_compute_backend() -> Tuple[str, str]:
|
||||
"""
|
||||
Detect the best available compute backend.
|
||||
|
||||
Priority order: Vulkan > CUDA > CPU
|
||||
|
||||
Returns:
|
||||
Tuple of (backend_type, backend_info)
|
||||
"""
|
||||
# Try Vulkan first (supports AMD, Intel, NVIDIA)
|
||||
has_vulkan, vulkan_info = detect_vulkan_support()
|
||||
if has_vulkan and vulkan_info:
|
||||
return ComputeBackend.VULKAN, vulkan_info
|
||||
|
||||
# Try CUDA next (NVIDIA only)
|
||||
has_cuda, cuda_info = detect_cuda_support()
|
||||
if has_cuda and cuda_info:
|
||||
return ComputeBackend.CUDA, cuda_info
|
||||
|
||||
# Fall back to CPU
|
||||
cpu_info = detect_cpu_info()
|
||||
return ComputeBackend.CPU, cpu_info
|
||||
|
||||
|
||||
def detect_cpu_info() -> str:
|
||||
"""
|
||||
Detect CPU information for CPU backend.
|
||||
|
||||
Returns:
|
||||
CPU info string
|
||||
"""
|
||||
try:
|
||||
# Try to get CPU model from /proc/cpuinfo
|
||||
with open("/proc/cpuinfo", "r") as f:
|
||||
for line in f:
|
||||
if "model name" in line:
|
||||
cpu_name = line.split(":")[1].strip()
|
||||
return cpu_name
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not read CPU info: {e}")
|
||||
|
||||
# Fallback to nproc
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["nproc"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
cpu_count = result.stdout.strip()
|
||||
return f"{cpu_count} cores"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return "CPU"
|
||||
|
||||
|
||||
def get_recommended_model() -> Tuple[str, str]:
|
||||
"""
|
||||
Get the recommended whisper.cpp model based on system configuration.
|
||||
|
||||
Returns:
|
||||
Tuple of (model_name, reason)
|
||||
"""
|
||||
try:
|
||||
import psutil
|
||||
|
||||
ram_gb = psutil.virtual_memory().total // (1024**3)
|
||||
|
||||
# Detect available compute backends
|
||||
backend, backend_info = detect_compute_backend()
|
||||
|
||||
if backend == ComputeBackend.VULKAN:
|
||||
# Vulkan can handle larger models efficiently
|
||||
if ram_gb >= 8:
|
||||
return "small", f"Vulkan GPU with {ram_gb}GB RAM"
|
||||
else:
|
||||
return "base", f"Vulkan GPU with {ram_gb}GB RAM"
|
||||
elif backend == ComputeBackend.CUDA:
|
||||
# CUDA has more VRAM typically
|
||||
if "GB" in backend_info:
|
||||
try:
|
||||
vram_gb = int(backend_info.split("GB")[0].split("(")[-1].strip())
|
||||
if vram_gb >= 8:
|
||||
return "medium", f"CUDA GPU with {vram_gb}GB VRAM"
|
||||
elif vram_gb >= 4:
|
||||
return "small", f"CUDA GPU with {vram_gb}GB VRAM"
|
||||
else:
|
||||
return "base", f"CUDA GPU with limited VRAM"
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
return "small", f"CUDA GPU detected"
|
||||
else:
|
||||
# CPU-only recommendations based on RAM
|
||||
if ram_gb >= 16:
|
||||
return "base", f"{ram_gb}GB RAM - CPU inference"
|
||||
elif ram_gb >= 8:
|
||||
return "tiny", f"{ram_gb}GB RAM - optimized for speed"
|
||||
else:
|
||||
return "tiny", f"Limited RAM ({ram_gb}GB) - fastest model"
|
||||
|
||||
except ImportError:
|
||||
logger.debug("psutil not available for system detection")
|
||||
|
||||
# Default recommendation
|
||||
return "tiny", "Default recommendation"
|
||||
|
||||
|
||||
def get_model_path(model_name: str) -> str:
|
||||
"""
|
||||
Get the path where a model should be stored.
|
||||
|
||||
Args:
|
||||
model_name: Name of the model (tiny, base, small, medium, large)
|
||||
|
||||
Returns:
|
||||
Path to the model file
|
||||
"""
|
||||
models_dir = os.path.expanduser("~/.local/share/vocalinux/models/whispercpp")
|
||||
os.makedirs(models_dir, exist_ok=True)
|
||||
|
||||
if model_name == "large":
|
||||
# Large model uses v3 variant
|
||||
return os.path.join(models_dir, "ggml-large-v3.bin")
|
||||
else:
|
||||
return os.path.join(models_dir, f"ggml-{model_name}.bin")
|
||||
|
||||
|
||||
def is_model_downloaded(model_name: str) -> bool:
|
||||
"""
|
||||
Check if a whisper.cpp model is downloaded.
|
||||
|
||||
Args:
|
||||
model_name: Name of the model
|
||||
|
||||
Returns:
|
||||
True if model exists, False otherwise
|
||||
"""
|
||||
model_path = get_model_path(model_name)
|
||||
return os.path.exists(model_path)
|
||||
|
||||
|
||||
def get_backend_display_name(backend: str) -> str:
|
||||
"""
|
||||
Get a user-friendly display name for a compute backend.
|
||||
|
||||
Args:
|
||||
backend: Backend type (vulkan, cuda, cpu)
|
||||
|
||||
Returns:
|
||||
Display name string
|
||||
"""
|
||||
names = {
|
||||
ComputeBackend.VULKAN: "Vulkan GPU",
|
||||
ComputeBackend.CUDA: "NVIDIA CUDA",
|
||||
ComputeBackend.CPU: "CPU",
|
||||
}
|
||||
return names.get(backend, backend.upper())
|
||||
@@ -149,13 +149,13 @@ class TestConfigManager(unittest.TestCase):
|
||||
|
||||
importlib.reload(cm)
|
||||
self.assertEqual(
|
||||
cm.DEFAULT_CONFIG["speech_recognition"]["engine"], "whisper"
|
||||
) # Default is whisper for better accuracy
|
||||
cm.DEFAULT_CONFIG["speech_recognition"]["engine"], "whisper_cpp"
|
||||
) # Default is whisper_cpp for best performance with Vulkan support
|
||||
|
||||
# Also test the get method works - it should return a valid engine value
|
||||
config_manager = ConfigManager()
|
||||
value = config_manager.get("speech_recognition", "engine")
|
||||
self.assertIn(value, ["vosk", "whisper"]) # Should be one of valid engines
|
||||
self.assertIn(value, ["vosk", "whisper", "whisper_cpp"]) # Should be one of valid engines
|
||||
|
||||
def test_get_nonexistent_value(self):
|
||||
"""Test getting a nonexistent configuration value."""
|
||||
|
||||
@@ -47,6 +47,45 @@ safe_remove() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to kill running Vocalinux processes
|
||||
kill_vocalinux_processes() {
|
||||
print_info "Checking for running Vocalinux processes..."
|
||||
|
||||
# Find and kill vocalinux processes
|
||||
local PIDS=$(pgrep -f "vocalinux" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$PIDS" ]; then
|
||||
print_warning "Found running Vocalinux process(es): $PIDS"
|
||||
print_info "Stopping Vocalinux..."
|
||||
|
||||
# Try graceful termination first
|
||||
echo "$PIDS" | xargs -r kill -TERM 2>/dev/null || true
|
||||
|
||||
# Wait a moment for processes to terminate
|
||||
sleep 2
|
||||
|
||||
# Check if any processes are still running
|
||||
local REMAINING_PIDS=$(pgrep -f "vocalinux" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$REMAINING_PIDS" ]; then
|
||||
print_warning "Some processes still running, forcing termination..."
|
||||
echo "$REMAINING_PIDS" | xargs -r kill -KILL 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# Final check
|
||||
local FINAL_PIDS=$(pgrep -f "vocalinux" 2>/dev/null || true)
|
||||
if [ -n "$FINAL_PIDS" ]; then
|
||||
print_error "Warning: Could not terminate all Vocalinux processes: $FINAL_PIDS"
|
||||
print_error "You may need to manually kill these processes before continuing."
|
||||
else
|
||||
print_success "All Vocalinux processes stopped"
|
||||
fi
|
||||
else
|
||||
print_info "No running Vocalinux processes found"
|
||||
fi
|
||||
}
|
||||
|
||||
print_info "Vocalinux Uninstaller"
|
||||
print_info "=============================="
|
||||
echo ""
|
||||
@@ -355,6 +394,9 @@ print_uninstallation_summary() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Kill any running Vocalinux processes first
|
||||
kill_vocalinux_processes
|
||||
|
||||
# Perform uninstallation steps
|
||||
remove_virtual_environment
|
||||
remove_curl_install_files
|
||||
|
||||
Reference in New Issue
Block a user