install.sh
Rama main · réplica de lectura del repositorio original.
#!/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" <<PLISTEOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>$PLIST_LABEL</string>
<key>ProgramArguments</key>
<array>
<string>$PYTHON_BIN</string>
<string>$INSTALL_DIR/speech_to_text.py</string>
</array>
<key>WorkingDirectory</key>
<string>$INSTALL_DIR</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>PYTHONUNBUFFERED</key>
<string>1</string>
</dict>
<key>StandardOutPath</key>
<string>$INSTALL_DIR/server.log</string>
<key>StandardErrorPath</key>
<string>$INSTALL_DIR/server.log</string>
</dict>
</plist>
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 <<SYSEOF
[Unit]
Description=STT Speech-to-Text Server
After=network.target
[Service]
Type=simple
# The installing user, not root: a root service writes root-owned files into this
# user's checkout and ~/.stt, after which the in-app updater (running as the user)
# cannot touch them. The capability is the one thing root was for: binding port 80.
User=$USER
Group=$(id -gn)
AmbientCapabilities=CAP_NET_BIND_SERVICE
WorkingDirectory=$INSTALL_DIR
Environment="PYTHONUNBUFFERED=1"
Environment="HOME=$HOME"
ExecStart=$INSTALL_DIR/.venv/bin/python3 $INSTALL_DIR/speech_to_text.py
# No ExecStop needed: systemd SIGTERMs the whole cgroup (server, worker,
# ffmpeg) on stop — the server exits cleanly on SIGTERM — and SIGKILLs any
# straggler after TimeoutStopSec. The old pkill chain killed its own control
# process (logged as 'code=killed, status=9/KILL' on every stop).
TimeoutStopSec=15
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=$SERVICE_NAME
[Install]
WantedBy=multi-user.target
SYSEOF
# Reload systemd and enable service
sudo systemctl daemon-reload
sudo systemctl enable "$SERVICE_NAME"
install_restart_sudoers
print_success "Systemd service created and enabled"
print_status "Service commands:"
print_status " sudo systemctl start $SERVICE_NAME - Start service"
print_status " sudo systemctl stop $SERVICE_NAME - Stop service"
print_status " sudo systemctl status $SERVICE_NAME - Check status"
print_status " sudo journalctl -u $SERVICE_NAME -f - View logs"
}
# Function to set up the watchdog service (replaces the bare stt-server service)
setup_watchdog_service() {
if [ "$OS" = "macos" ]; then
setup_watchdog_launchd
else
setup_watchdog_systemd
fi
}
setup_watchdog_launchd() {
local PLIST_LABEL="com.stt.watchdog"
local PLIST_SRC="$INSTALL_DIR/deploy/com.stt.watchdog.plist"
local PLIST_DST="$HOME/Library/LaunchAgents/$PLIST_LABEL.plist"
local PYTHON_BIN="$INSTALL_DIR/.venv/bin/python3"
print_status "Would you like to set up the STT Watchdog as a LaunchAgent?"
print_status "(Watchdog provides crash recovery and daily auto-updates)"
read -p "Setup Watchdog LaunchAgent? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
return
fi
mkdir -p "$HOME/Library/LaunchAgents"
# Substitute the plist template's placeholders. launchd will not create the log
# directory, and a job whose StandardOutPath cannot be opened never starts.
local LOG_DIR="$HOME/.stt/logs"
mkdir -p "$LOG_DIR"
sed -e "s|INSTALL_DIR|$INSTALL_DIR|g" -e "s|LOG_DIR|$LOG_DIR|g" "$PLIST_SRC" > "$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 <<SYSEOF
[Unit]
Description=STT Watchdog (manages speech_to_text.py with crash recovery and auto-updates)
After=network.target
[Service]
Type=simple
# The installing user, not root: a root service writes root-owned files into this
# user's checkout and ~/.stt, after which the in-app updater (running as the user)
# cannot touch them. The capability is the one thing root was for: binding port 80.
User=$USER
Group=$(id -gn)
AmbientCapabilities=CAP_NET_BIND_SERVICE
WorkingDirectory=$INSTALL_DIR
Environment="PYTHONUNBUFFERED=1"
Environment="HOME=$HOME"
ExecStart=$INSTALL_DIR/.venv/bin/python3 $INSTALL_DIR/stt/watchdog.py --headless
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=$WATCHDOG_SERVICE
[Install]
WantedBy=multi-user.target
SYSEOF
sudo systemctl daemon-reload
sudo systemctl enable "$WATCHDOG_SERVICE"
install_restart_sudoers
# Always retire the old bare stt-server unit: running it alongside the
# watchdog means two supervisors both launching a server, which is never
# correct. (The 57338 server lock keeps only one server alive regardless,
# but a disabled unit avoids the losing one flapping under Restart=always.)
if systemctl list-unit-files --no-legend 'stt-server.service' 2>/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 "$@"