#!/bin/bash # Speech-to-Text Installation Script # Works on bare Ubuntu/Debian installations set -e # Exit on error # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Configuration INSTALL_DIR="$(pwd)" SERVICE_NAME="stt-server" PYTHON_BIN="" # Function to print colored output print_status() { echo -e "${BLUE}[INFO]${NC} $1" } print_success() { echo -e "${GREEN}[OK]${NC} $1" } print_error() { echo -e "${RED}[ERROR]${NC} $1" } print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } # Function to check if running as root check_not_root() { if [ "$EUID" -eq 0 ]; then print_error "This script should not be run as root for security reasons." print_error "Please run as a regular user with sudo access." exit 1 fi } # Function to detect OS detect_os() { local uname_s uname_s=$(uname -s) case $uname_s in Darwin) OS=macos ARCH=$(uname -m) # PyTorch published its last macOS x86_64 wheels with 2.2.2, so the # pinned torch in requirements.txt cannot resolve on an Intel Mac. # Stop here rather than after installing Python, ffmpeg and a venv. if [ "$ARCH" != "arm64" ]; then print_error "This Mac has an Intel processor. STT needs PyTorch, which no longer publishes Intel-Mac builds, so its dependencies cannot be installed here." print_error "Run STT on an Apple Silicon Mac (M1 or newer), or on a Windows or Linux PC." exit 1 fi # scipy and scikit-learn (pulled in by librosa) have never # published an arm64 wheel tagged below macosx_12_0 for Python # 3.11, so on Big Sur there is nothing to install and nothing to # fall back to — uv builds from source and fails hours later. MACOS_MAJOR=$(sw_vers -productVersion 2>/dev/null | cut -d. -f1) if [ -n "$MACOS_MAJOR" ] && [ "$MACOS_MAJOR" -lt 12 ] 2>/dev/null; then print_error "This Mac runs macOS $MACOS_MAJOR, and STT needs macOS Monterey (12) or later — the audio libraries it depends on stopped publishing builds for older versions." print_error "The update is free for every Apple Silicon Mac: open System Preferences -> Software Update, install it, then run this installer again." exit 1 fi print_success "Detected macOS ($ARCH)" ;; Linux) if [ -f /etc/os-release ]; then . /etc/os-release OS=$ID VERSION_ID=$VERSION_ID else print_warning "Cannot read /etc/os-release. Assuming generic Linux." OS=linux fi case $OS in ubuntu|debian) print_success "Detected $OS $VERSION_ID" ;; *) print_warning "Untested Linux distro: $OS. Script designed for Debian/Ubuntu." read -p "Continue anyway? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi ;; esac ;; *) print_error "Unsupported OS: $uname_s. This script supports Linux (Debian/Ubuntu) and macOS." exit 1 ;; esac } # Function to check and install system packages install_system_deps() { print_status "Checking system packages..." if [ "$OS" = "macos" ]; then # macOS: use Homebrew if ! command -v brew &> /dev/null; then print_error "Homebrew not found. Install it from https://brew.sh/ then re-run this script." exit 1 fi print_status "Installing system packages via Homebrew..." brew install python3 ffmpeg else # Linux: use apt-get print_status "Updating package list..." sudo apt-get update PACKAGES=( "python3" "python3-dev" "python3-tk" "build-essential" "git" "curl" "wget" "ffmpeg" "libasound2-dev" "alsa-utils" "psmisc" ) print_status "Installing system packages: ${PACKAGES[*]}" sudo apt-get install -y "${PACKAGES[@]}" fi # Verify installations if ! command -v python3 &> /dev/null; then print_error "Python3 installation failed" exit 1 fi if ! command -v ffmpeg &> /dev/null; then print_error "FFmpeg installation failed" exit 1 fi print_success "System packages installed successfully" } # Function to add user to audio group (Linux only) setup_audio_permissions() { if [ "$OS" = "macos" ]; then print_status "Skipping audio group setup (not needed on macOS)" return fi print_status "Setting up audio permissions..." # Check if user is already in audio group if groups "$USER" | grep -q '\baudio\b'; then print_success "User already in audio group" else print_status "Adding user to audio group..." sudo usermod -a -G audio "$USER" print_warning "User added to audio group. You may need to log out and back in for changes to take effect." fi # Check if pulseaudio or pipewire is running if pgrep -x "pulseaudio" > /dev/null || pgrep -x "pipewire" > /dev/null; then print_success "Audio server detected" else print_warning "No audio server detected. For desktop audio, consider installing pulseaudio or pipewire." fi } # Function to install uv (fast Python package installer) install_uv() { print_status "Installing uv package manager..." if command -v uv &> /dev/null; then print_success "uv already installed: $(uv --version)" return fi curl -LsSf https://astral.sh/uv/install.sh | sh # Add uv to PATH for current session export PATH="$HOME/.local/bin:$PATH" if command -v uv &> /dev/null; then print_success "uv installed: $(uv --version)" else print_error "uv installation failed" exit 1 fi } # Function to create Python virtual environment create_venv() { print_status "Creating Python virtual environment..." # --clear replaces an existing .venv without the interactive y/n prompt # (unattended/headless installs would otherwise hang on a keypress) uv venv --clear "$INSTALL_DIR/.venv" export VIRTUAL_ENV="$INSTALL_DIR/.venv" PYTHON_BIN="$INSTALL_DIR/.venv/bin/python3" print_success "Virtual environment created at $INSTALL_DIR/.venv" } # Function to detect GPU and configure PyTorch detect_gpu() { print_status "Detecting GPU..." if [ "$OS" = "macos" ]; then if [ "$(uname -m)" = "arm64" ]; then print_success "Apple Silicon detected — MPS (Metal) GPU acceleration available" return 0 fi # Unreachable via main() — detect_os exits on an Intel Mac. Kept so this # stays a check rather than a claim if the call order ever changes. print_error "This Mac has an Intel processor — unsupported (see detect_os)" return 1 else if command -v nvidia-smi &> /dev/null; then print_success "NVIDIA GPU detected:" nvidia-smi --query-gpu=name,memory.total --format=csv,noheader # Check CUDA version CUDA_VERSION=$(nvidia-smi | grep "CUDA Version" | awk '{print $9}') if [ -n "$CUDA_VERSION" ]; then print_status "CUDA Version: $CUDA_VERSION" fi return 0 else print_warning "No NVIDIA GPU detected or drivers not installed" print_warning "Will install CPU-only PyTorch" return 1 fi fi } # Pick the torch wheel index this GPU can actually run. # # cu128 dropped the pre-Volta architectures, so on a Pascal card (GTX 10-series, # sm_61) a cu128 install reports cuda.is_available() True and then fails every kernel # launch with "no kernel image is available for execution on the device". The index is # therefore chosen by compute capability, not by the presence of nvidia-smi. # # Mirrors stt/cuda_index.py, which carries the tests; keep the two in step. Prints # nothing when the capability cannot be read, so the caller installs the default wheel # rather than guessing: a CPU build is slow and obvious, a CUDA build with no kernels # for the card looks perfect until the first service. torch_index_url() { local caps lowest caps=$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null) || return 0 # Lowest capability wins: the wheels have to run on every card in the box. lowest=$(printf '%s\n' "$caps" | grep -E '^[0-9]+\.[0-9]+$' | sort -g | head -1) [ -z "$lowest" ] && return 0 if [ "$(printf '%s\n7.0\n' "$lowest" | sort -g | head -1)" = "7.0" ]; then echo "https://download.pytorch.org/whl/cu128" else echo "https://download.pytorch.org/whl/cu126" fi } # Compiled packages that must arrive as wheels or not at all. uv otherwise falls # back to building from source when no wheel matches the platform, which needs a # compiler this machine may not have; refusing the fallback makes uv pick a # release that does ship a wheel here. Mirrors stt/wheel_policy.py, which is the # source of truth and carries the tests — keep the two in step. ONLY_BINARY="llvmlite,numba,numpy,scikit-learn,scipy,soxr" # Function to install Python packages install_python_deps() { print_status "Installing Python dependencies from requirements.txt..." if [ "$OS" = "macos" ]; then # macOS: standard PyPI PyTorch includes MPS support; no CUDA index needed detect_gpu # Just for informational output print_status "Installing packages (MPS/CPU)..." uv pip install -r requirements.txt --only-binary "$ONLY_BINARY" else # Linux: install CUDA PyTorch if NVIDIA GPU present, otherwise CPU if detect_gpu; then index_url=$(torch_index_url) if [ -n "$index_url" ]; then print_status "Installing GPU-enabled packages ($index_url)..." uv pip install -r requirements.txt --only-binary "$ONLY_BINARY" --extra-index-url "$index_url" else print_status "Installing default packages..." uv pip install -r requirements.txt --only-binary "$ONLY_BINARY" fi else print_status "Installing CPU-only packages..." uv pip install -r requirements.txt --only-binary "$ONLY_BINARY" fi fi print_success "Python dependencies installed" } # Function to verify Python installation verify_python_deps() { print_status "Verifying Python installation..." "$PYTHON_BIN" -c " import sys try: import torch print(f'PyTorch {torch.__version__} installed') if torch.cuda.is_available(): print(f'CUDA available: {torch.cuda.get_device_name(0)}') elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available(): print('MPS (Apple Silicon GPU) available') else: print('GPU not available (CPU mode)') import speech_recognition print('SpeechRecognition installed') import numpy print('NumPy installed') import flask print('Flask installed') print('\nAll critical packages verified!') except ImportError as e: print(f'Import error: {e}') sys.exit(1) " print_success "Python verification passed" } # Function to list audio devices list_audio_devices() { print_status "Listing available audio devices..." "$PYTHON_BIN" -c " from audio_capture import list_audio_devices devices = list_audio_devices() print('\\nAvailable audio devices:') print('-' * 60) for i, device in enumerate(devices): print(f'{i}: {device[\"name\"]}') print(f' Display: {device.get(\"display_name\", device[\"name\"])}') if device.get('is_default'): print(' [DEFAULT]') print() " } # Function to test audio capture test_audio_capture() { print_status "Would you like to test audio capture?" read -p "Test audio capture? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then return fi print_status "Testing audio capture for 3 seconds..." "$PYTHON_BIN" << 'PYEOF' import sys sys.path.insert(0, '.') from audio_capture import FFmpegAudioCapture import time try: capture = FFmpegAudioCapture(sample_rate=16000, chunk_duration=1.0, device_name=None) capture.ts_enabled = False # Don't create backup for test print("Starting capture...") capture.start() time.sleep(3) capture.stop() print("\nAudio capture test completed successfully!") except Exception as e: import platform print(f"\nAudio capture test failed: {e}") print("\nTroubleshooting:") print("1. Check if microphone is connected and permitted") if platform.system() == "Darwin": print("2. macOS: Grant microphone access in System Settings > Privacy & Security > Microphone") print("3. List devices: ffmpeg -f avfoundation -list_devices true -i \"\"") print("4. Try device ':0' or ':1' as default_microphone in ~/.stt/config/config.json") else: print("2. Check if user is in 'audio' group (log out and back in if added)") print("3. Try: arecord -l (to list recording devices)") print("4. Check ALSA: cat /proc/asound/cards") PYEOF } # Function to create systemd service (Linux) or LaunchAgent (macOS) setup_systemd_service() { if [ "$OS" = "macos" ]; then setup_launchd_agent else setup_systemd_service_linux fi } setup_launchd_agent() { local PLIST_LABEL="com.stt.server" local PLIST_PATH="$HOME/Library/LaunchAgents/$PLIST_LABEL.plist" local PYTHON_BIN="$INSTALL_DIR/.venv/bin/python3" print_status "Would you like to set up a LaunchAgent for auto-start on login?" read -p "Setup LaunchAgent? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then return fi mkdir -p "$HOME/Library/LaunchAgents" cat > "$PLIST_PATH" < Label $PLIST_LABEL ProgramArguments $PYTHON_BIN $INSTALL_DIR/speech_to_text.py WorkingDirectory $INSTALL_DIR RunAtLoad KeepAlive EnvironmentVariables PYTHONUNBUFFERED 1 StandardOutPath $INSTALL_DIR/server.log StandardErrorPath $INSTALL_DIR/server.log PLISTEOF launchctl load "$PLIST_PATH" print_success "LaunchAgent created and loaded" print_status "Service commands:" print_status " launchctl start $PLIST_LABEL - Start service" print_status " launchctl stop $PLIST_LABEL - Stop service" print_status " launchctl unload $PLIST_PATH - Remove service" print_status " tail -f $INSTALL_DIR/server.log - View logs" } # Allow the installing user (and an unprivileged service user) to restart the # STT units without a password. The in-app auto-updater tries plain systemctl, # then `sudo -n systemctl restart`; without this rule a unit running as a # regular user can only fall back to an in-place execv restart. install_restart_sudoers() { local SYSTEMCTL_BIN SUDOERS_FILE SYSTEMCTL_BIN=$(command -v systemctl) || return 0 SUDOERS_FILE="/etc/sudoers.d/stt-restart" printf '%s ALL=(root) NOPASSWD: %s restart stt-server, %s restart stt-watchdog\n' \ "$USER" "$SYSTEMCTL_BIN" "$SYSTEMCTL_BIN" | sudo tee "$SUDOERS_FILE" > /dev/null sudo chmod 440 "$SUDOERS_FILE" if sudo visudo -cf "$SUDOERS_FILE" > /dev/null 2>&1; then print_success "Sudoers rule installed: $USER may restart STT services without a password" else sudo rm -f "$SUDOERS_FILE" print_error "Sudoers rule failed validation and was removed (auto-update will use in-place restart instead)" fi } setup_systemd_service_linux() { print_status "Would you like to set up a systemd service for auto-start?" read -p "Setup systemd service? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then return fi print_status "Creating systemd service..." # Create service file sudo tee "/etc/systemd/system/$SERVICE_NAME.service" > /dev/null < "$PLIST_DST" # Retire the old bare-server LaunchAgent so two supervisors don't both start # a server. Unloading alone isn't enough — the plist auto-loads again at the # next login — so remove the file too. (The 57338 server lock would keep only # one server alive regardless, but this avoids the losing one flapping.) local OLD_PLIST="$HOME/Library/LaunchAgents/com.stt.server.plist" if [ -f "$OLD_PLIST" ]; then launchctl unload "$OLD_PLIST" 2>/dev/null || true rm -f "$OLD_PLIST" print_status "Retired old com.stt.server LaunchAgent (watchdog now manages the server)" fi launchctl load "$PLIST_DST" print_success "Watchdog LaunchAgent installed and loaded" print_status "Service commands:" print_status " launchctl start $PLIST_LABEL - Start watchdog + STT" print_status " launchctl stop $PLIST_LABEL - Stop watchdog + STT" print_status " launchctl unload $PLIST_DST - Remove watchdog service" print_status " tail -f $LOG_DIR/watchdog.log - Watchdog log" print_status " tail -f $LOG_DIR/stt.log - STT app log" } setup_watchdog_systemd() { local WATCHDOG_SERVICE="stt-watchdog" local SERVICE_FILE="/etc/systemd/system/$WATCHDOG_SERVICE.service" print_status "Would you like to set up the STT Watchdog as a systemd service?" print_status "(Watchdog provides crash recovery and daily auto-updates)" read -p "Setup Watchdog systemd service? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then return fi print_status "Installing $WATCHDOG_SERVICE.service..." sudo tee "$SERVICE_FILE" > /dev/null </dev/null | grep -q '^stt-server\.service'; then print_status "Retiring old stt-server.service (watchdog now manages the server)..." sudo systemctl stop stt-server 2>/dev/null || true sudo systemctl disable stt-server 2>/dev/null || true print_success "stt-server stopped and disabled" fi print_success "Watchdog systemd service installed and enabled" print_status "Service commands:" print_status " sudo systemctl start $WATCHDOG_SERVICE - Start watchdog + STT" print_status " sudo systemctl stop $WATCHDOG_SERVICE - Stop watchdog + STT" print_status " sudo systemctl status $WATCHDOG_SERVICE - Check status" print_status " sudo journalctl -u $WATCHDOG_SERVICE -f - Watchdog log" print_status " tail -f $INSTALL_DIR/logs/stt.log - STT app log" } # Function to display final instructions show_final_instructions() { echo echo "========================================" echo " Installation Complete!" echo "========================================" echo print_success "Speech-to-Text has been installed successfully" echo echo "Quick Start:" if [ "$OS" = "macos" ]; then echo " 1. Start with watchdog: sudo python3 stt/watchdog.py --headless" echo " (or GUI mode: python3 stt/watchdog.py --gui)" echo " (the shipped port is 8080, which needs no sudo; port 80 does)" echo " 2. Grant microphone access if prompted by macOS" echo " 3. Open browser: http://localhost:8080" echo " 4. Set microphone in \$HOME/.stt/config/config.json: default_microphone: ':0'" echo " (run: ffmpeg -f avfoundation -list_devices true -i \"\" to find device index)" else echo " 1. Start with watchdog: sudo ./start_watchdog.sh" echo " (or GUI mode: python3 stt/watchdog.py --gui)" echo " 2. Open browser: http://localhost:8080" fi echo " - Go to /model-manager to download and select a model" echo " - Go to /live-settings to configure audio" echo echo "Configuration:" # Not $INSTALL_DIR/config: that holds the shipped template. The server seeds and # reads its own copy in the data dir, so editing the checkout does nothing at all. echo " - Main config: \$HOME/.stt/config/config.json" echo " (created on first run; set STT_DATA_DIR to put the data dir elsewhere)" echo echo "Useful commands:" echo if [ "$OS" != "macos" ]; then if ! groups "$USER" | grep -q '\baudio\b'; then echo -e "${YELLOW}IMPORTANT:${NC} You were added to the 'audio' group." echo -e "${YELLOW}Please log out and log back in for audio permissions to take effect.${NC}" echo fi if [ -f "/etc/systemd/system/stt-watchdog.service" ]; then echo "Watchdog service installed (crash recovery + auto-updates):" echo " sudo systemctl start stt-watchdog - Start" echo " sudo systemctl stop stt-watchdog - Stop" echo " sudo systemctl status stt-watchdog - Status" echo " tail -f $INSTALL_DIR/logs/stt.log - STT log" echo elif [ -f "/etc/systemd/system/$SERVICE_NAME.service" ]; then echo "Systemd service installed:" echo " sudo systemctl start $SERVICE_NAME - Start" echo " sudo systemctl stop $SERVICE_NAME - Stop" echo " sudo systemctl status $SERVICE_NAME - Status" echo fi fi echo "Manual start (with watchdog):" echo " ./start_watchdog.sh - Headless (server/production)" echo " python3 stt/watchdog.py --gui - Desktop GUI (start/stop + settings)" echo " python3 stt/watchdog.py --check-update - Check for updates now" echo } # Main installation function main() { echo "========================================" echo " Speech-to-Text Installation Script" echo "========================================" echo # Pre-flight checks check_not_root detect_os # Installation steps install_system_deps install_uv create_venv setup_audio_permissions install_python_deps verify_python_deps # Optional setups list_audio_devices test_audio_capture setup_watchdog_service # Final instructions show_final_instructions } # Handle script interruption trap 'print_error "Installation interrupted"; exit 1' INT TERM # Run main function main "$@"