# Speech-to-Text Installation Script for Windows # Installs Python, ffmpeg, creates venv, and installs all dependencies Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" # Configuration $INSTALL_DIR = $PSScriptRoot $SERVICE_NAME = "stt-server" $PYTHON_BIN = "" $VENV_DIR = Join-Path $INSTALL_DIR ".venv" # --- Output helpers -------------------------------------------------- function Print-Status { param($msg) Write-Host "[INFO] " -ForegroundColor Blue -NoNewline; Write-Host $msg } function Print-Success { param($msg) Write-Host "[OK] " -ForegroundColor Green -NoNewline; Write-Host $msg } function Print-Error { param($msg) Write-Host "[ERROR] " -ForegroundColor Red -NoNewline; Write-Host $msg } function Print-Warning { param($msg) Write-Host "[WARNING] " -ForegroundColor Yellow -NoNewline; Write-Host $msg } # --- Check admin ----------------------------------------------------- function Test-Admin { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($identity) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } # --- Refresh PATH from registry (picks up new installs without restart) -- function Refresh-Path { $machinePath = [Environment]::GetEnvironmentVariable("Path", "Machine") $userPath = [Environment]::GetEnvironmentVariable("Path", "User") $env:Path = "$machinePath;$userPath" } # --- Check if a command exists --------------------------------------- function Test-Command { param($Name) return [bool](Get-Command $Name -ErrorAction SilentlyContinue) } # --- Install winget packages ---------------------------------------- function Install-SystemDeps { Print-Status "Checking system dependencies..." # --- Python --- if (Test-Command "python") { $pyVer = & python --version 2>&1 Print-Success "Python already installed: $pyVer" } else { Print-Status "Installing Python via winget..." if (Test-Command "winget") { winget install --id Python.Python.3.11 --accept-source-agreements --accept-package-agreements Refresh-Path } else { Print-Error "Python not found and winget is not available." Print-Error "Please install Python 3.10+ from https://www.python.org/downloads/" Print-Error "IMPORTANT: Check 'Add Python to PATH' during installation." exit 1 } } # Verify Python if (-not (Test-Command "python")) { Refresh-Path if (-not (Test-Command "python")) { Print-Error "Python not found in PATH after installation." Print-Error "Please install Python manually and ensure 'Add Python to PATH' is checked." exit 1 } } # --- Git --- if (Test-Command "git") { Print-Success "Git already installed" } else { Print-Status "Installing Git via winget..." if (Test-Command "winget") { winget install --id Git.Git --accept-source-agreements --accept-package-agreements Refresh-Path } else { Print-Warning "Git not found. Install from https://git-scm.com/download/win" Print-Warning "Not required to install: auto-updates fall back to archive downloads." } } # --- FFmpeg --- if (Test-Command "ffmpeg") { Print-Success "FFmpeg already installed" } else { Print-Status "Installing FFmpeg via winget..." if (Test-Command "winget") { winget install --id Gyan.FFmpeg --accept-source-agreements --accept-package-agreements Refresh-Path } if (-not (Test-Command "ffmpeg")) { Print-Warning "FFmpeg not found in PATH after install attempt." Print-Warning "You may need to add FFmpeg to your PATH manually or restart your terminal." Print-Warning "Download from: https://www.gyan.dev/ffmpeg/builds/" } } Print-Success "System dependency check complete" } # --- Install uv ------------------------------------------------------ function Install-Uv { Print-Status "Installing uv package manager..." if (Test-Command "uv") { $uvVer = & uv --version 2>&1 Print-Success "uv already installed: $uvVer" return } Print-Status "Downloading uv installer..." Invoke-RestMethod https://astral.sh/uv/install.ps1 | Invoke-Expression Refresh-Path # Also check common install location $uvPath = Join-Path $env:USERPROFILE ".local\bin" if (Test-Path $uvPath) { $env:Path = "$uvPath;$env:Path" } # Also check cargo bin (alternative install location) $cargoPath = Join-Path $env:USERPROFILE ".cargo\bin" if (Test-Path $cargoPath) { $env:Path = "$cargoPath;$env:Path" } if (Test-Command "uv") { $uvVer = & uv --version 2>&1 Print-Success "uv installed: $uvVer" } else { Print-Error "uv installation failed. Falling back to pip." } } # --- Create virtual environment -------------------------------------- function Create-Venv { Print-Status "Creating Python virtual environment..." if (Test-Command "uv") { # --clear replaces an existing venv without the interactive y/n prompt & uv venv --clear $VENV_DIR } else { & python -m venv $VENV_DIR } $script:PYTHON_BIN = Join-Path $VENV_DIR "Scripts\python.exe" if (-not (Test-Path $PYTHON_BIN)) { Print-Error "Failed to create virtual environment" exit 1 } Print-Success "Virtual environment created at $VENV_DIR" } # --- Detect GPU ------------------------------------------------------ function Detect-Gpu { Print-Status "Detecting GPU..." if (Test-Command "nvidia-smi") { Print-Success "NVIDIA GPU detected:" # To the host, not the pipeline: a bare command's output becomes part of the # function's return value, so the name never printed and callers got an array. & nvidia-smi --query-gpu=name,memory.total --format=csv,noheader 2>$null | ForEach-Object { Write-Host " $_" } $cudaLine = & nvidia-smi 2>$null | Select-String "CUDA Version" if ($cudaLine) { $cudaVer = ($cudaLine -split '\s+' | Where-Object { $_ -match '^\d+\.\d+' } | Select-Object -Last 1) Print-Status "CUDA Version: $cudaVer" } return $true } else { Print-Warning "No NVIDIA GPU detected (nvidia-smi not found)" Print-Warning "Will install CPU-only PyTorch" return $false } } # --- Pick the torch wheel index this GPU can actually run ----------- # cu128 dropped the pre-Volta architectures. On a Pascal card (GTX 10-series, sm_61) # a cu128 install still reports cuda.is_available() True and then fails every kernel # launch with "no kernel image is available for execution on the device" -- so the # index is 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. function Get-TorchIndexUrl { $cap = & nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>$null $lowest = $null foreach ($line in @($cap)) { $value = 0.0 if ([double]::TryParse(("" + $line).Trim(), [ref]$value)) { if ($null -eq $lowest -or $value -lt $lowest) { $lowest = $value } } } if ($null -eq $lowest) { # Unreadable capability: install the default wheel rather than guess. A CPU # build is slow and obvious; a CUDA build with no kernels for the card looks # perfect until the first service. Print-Warning "Could not read GPU compute capability -- installing default wheels" return $null } if ($lowest -ge 7.0) { return "https://download.pytorch.org/whl/cu128" } Print-Status "GPU compute capability $lowest is pre-Volta -- using the cu126 wheels" return "https://download.pytorch.org/whl/cu126" } # Compiled packages that must arrive as wheels or not at all. uv/pip otherwise # fall back to building from source when no wheel matches the platform, which # needs a compiler this machine may not have; refusing the fallback makes the # resolver pick a release that does ship a wheel here. Mirrors # stt/wheel_policy.py, which is the source of truth and carries the tests. $ONLY_BINARY = "llvmlite,numba,numpy,scikit-learn,scipy,soxr" # --- Install Python packages ---------------------------------------- function Install-PythonDeps { Print-Status "Installing Python dependencies from requirements.txt..." $reqFile = Join-Path $INSTALL_DIR "requirements.txt" $hasGpu = Detect-Gpu $indexUrl = if ($hasGpu) { Get-TorchIndexUrl } else { $null } if (Test-Command "uv") { if ($indexUrl) { Print-Status "Installing GPU-enabled packages ($indexUrl)..." & uv pip install --python $PYTHON_BIN -r $reqFile --only-binary $ONLY_BINARY --extra-index-url $indexUrl } else { Print-Status "Installing default packages..." & uv pip install --python $PYTHON_BIN -r $reqFile --only-binary $ONLY_BINARY } } else { # Fallback to pip & $PYTHON_BIN -m pip install --upgrade pip if ($indexUrl) { Print-Status "Installing GPU-enabled packages ($indexUrl)..." & $PYTHON_BIN -m pip install -r $reqFile --only-binary $ONLY_BINARY --extra-index-url $indexUrl } else { Print-Status "Installing default packages..." & $PYTHON_BIN -m pip install -r $reqFile --only-binary $ONLY_BINARY } } if ($LASTEXITCODE -ne 0) { Print-Error "Some packages failed to install. Check the output above." Print-Warning "You can try installing manually: .venv\Scripts\pip install -r requirements.txt --only-binary $ONLY_BINARY" } else { Print-Success "Python dependencies installed" } } # --- Verify installation -------------------------------------------- function Verify-PythonDeps { Print-Status "Verifying Python installation..." $verifyScript = @" import sys errors = [] try: import torch print(f' PyTorch {torch.__version__}') if torch.cuda.is_available(): print(f' CUDA available: {torch.cuda.get_device_name(0)}') else: print(' GPU not available (CPU mode)') except ImportError as e: errors.append(str(e)) for mod in ['speech_recognition', 'numpy', 'flask', 'soundfile']: try: __import__(mod) print(f' {mod} OK') except ImportError as e: errors.append(str(e)) if errors: print(f'\nMissing packages: {errors}') sys.exit(1) else: print('\n All critical packages verified!') "@ & $PYTHON_BIN -c $verifyScript if ($LASTEXITCODE -eq 0) { Print-Success "Python verification passed" } else { Print-Warning "Some packages could not be verified (see above)" } } # --- Verify scripts exist -------------------------------------------- function Verify-Scripts { Print-Status "Checking server management scripts..." $scripts = @("start_server.bat", "stop_server.bat", "restart_server.bat") foreach ($s in $scripts) { $path = Join-Path $INSTALL_DIR $s if (Test-Path $path) { Print-Success "$s found" } else { Print-Warning "$s not found -- download it from the repository" } } } # --- Optional: Task Scheduler auto-start ----------------------------- function Setup-TaskScheduler { Write-Host "" $reply = Read-Host "Would you like to set up auto-start on login via Task Scheduler? (y/N)" if ($reply -notmatch '^[Yy]$') { return } if (-not (Test-Admin)) { Print-Warning "Task Scheduler setup requires Administrator privileges." Print-Warning "Please re-run this script as Administrator to set up auto-start." return } $taskAction = New-ScheduledTaskAction ` -Execute (Join-Path $VENV_DIR "Scripts\python.exe") ` -Argument "speech_to_text.py" ` -WorkingDirectory $INSTALL_DIR $taskTrigger = New-ScheduledTaskTrigger -AtLogOn # ExecutionTimeLimit zero = no limit. The default is 72 hours, after which Task # Scheduler kills the task: a server left running over a long weekend just stopped. $taskSettings = New-ScheduledTaskSettingsSet ` -ExecutionTimeLimit ([TimeSpan]::Zero) ` -AllowStartIfOnBatteries ` -DontStopIfGoingOnBatteries ` -RestartCount 3 ` -RestartInterval (New-TimeSpan -Minutes 1) Register-ScheduledTask ` -TaskName $SERVICE_NAME ` -Action $taskAction ` -Trigger $taskTrigger ` -Settings $taskSettings ` -Description "Speech-to-Text Server" ` -RunLevel Highest ` -Force Print-Success "Task Scheduler entry created: $SERVICE_NAME" Print-Status "The server will auto-start when you log in." Print-Status "Manage it in Task Scheduler (taskschd.msc) or with:" Print-Status " schtasks /Run /TN $SERVICE_NAME - Start now" Print-Status " schtasks /End /TN $SERVICE_NAME - Stop" Print-Status " schtasks /Delete /TN $SERVICE_NAME - Remove" } # --- Final instructions --------------------------------------------- function Show-FinalInstructions { Write-Host "" Write-Host "========================================" Write-Host " Installation Complete!" Write-Host "========================================" Write-Host "" Print-Success "Speech-to-Text has been installed successfully" Write-Host "" Write-Host "Quick Start:" Write-Host " 1. Run: start_server.bat" Write-Host " 2. Open browser: http://localhost:8080" Write-Host " 3. Go to /model-manager to download and select a model" Write-Host " 4. Go to /live-settings to configure audio" Write-Host "" Write-Host "Configuration:" # The server seeds and reads its own copy under the data dir. Naming the bundled # template here sent operators to edit a file nothing reads: settings looked # ignored, with no warning, until the startup log line gave the real path away. Write-Host " - Main config: $env:USERPROFILE\.stt\config\config.json" Write-Host " (created on first run; set STT_DATA_DIR to put the data dir elsewhere)" Write-Host "" Write-Host "Manual start (from this directory):" Write-Host " .venv\Scripts\python.exe speech_to_text.py" Write-Host "" if (-not (Test-Command "ffmpeg")) { Print-Warning "FFmpeg was not found in PATH. You may need to:" Print-Warning " 1. Restart your terminal/PC after installation" Print-Warning " 2. Or install manually from https://www.gyan.dev/ffmpeg/builds/" Write-Host "" } } # --- Main ------------------------------------------------------------ function Main { Write-Host "" Write-Host "========================================" Write-Host " Speech-to-Text Windows Installer" Write-Host "========================================" Write-Host "" Push-Location $INSTALL_DIR try { Install-SystemDeps Install-Uv Create-Venv Install-PythonDeps Verify-PythonDeps Verify-Scripts Setup-TaskScheduler Show-FinalInstructions } finally { Pop-Location } } # Run Main