#requires -Version 5.1 <# .SYNOPSIS Download Piper TTS engine + Russian voice into tools/piper/. .DESCRIPTION Fetches the latest piper_windows_amd64.zip from rhasspy/piper-binary and the Irina (ru_RU) voice .onnx + .json from huggingface rhasspy/piper-voices. Idempotent — skips if files already exist. Pass -Force to re-download. .PARAMETER Voice Voice name to install. Default "ru_RU-irina-medium". Other options: ru_RU-denis-medium, ru_RU-dmitri-medium, ru_RU-ruslan-medium, en_US-amy-medium, ... .PARAMETER Force Re-download even if files exist. .EXAMPLE pwsh -File install.ps1 pwsh -File install.ps1 -Voice ru_RU-denis-medium pwsh -File install.ps1 -Force #> param( [string]$Voice = "ru_RU-irina-medium", [switch]$Force ) $ErrorActionPreference = "Stop" $InformationPreference = "Continue" $Root = Split-Path -Parent $MyInvocation.MyCommand.Path $BinPath = Join-Path $Root "piper.exe" $VoiceDir = Join-Path $Root "voices" $VoiceFile = Join-Path $VoiceDir "$Voice.onnx" $VoiceJson = Join-Path $VoiceDir "$Voice.onnx.json" # ─── Piper binary ────────────────────────────────────────────────────────── if ((Test-Path $BinPath) -and -not $Force) { Write-Information "[skip] piper.exe already present at $BinPath" } else { Write-Information "[fetch] piper_windows_amd64.zip..." $zip = Join-Path $env:TEMP "piper_windows_amd64.zip" $url = "https://github.com/rhasspy/piper/releases/latest/download/piper_windows_amd64.zip" Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing Write-Information "[unzip] -> $Root" Expand-Archive -Path $zip -DestinationPath $Root -Force # piper distributes inside a "piper/" subfolder — flatten it $inner = Join-Path $Root "piper" if (Test-Path $inner) { Get-ChildItem -Path $inner -Force | Move-Item -Destination $Root -Force Remove-Item -Path $inner -Recurse -Force } Remove-Item $zip -Force if (-not (Test-Path $BinPath)) { throw "piper.exe not found after extraction" } Write-Information "[ok] piper installed at $BinPath" } # ─── Voice files ─────────────────────────────────────────────────────────── if (-not (Test-Path $VoiceDir)) { New-Item -ItemType Directory -Path $VoiceDir | Out-Null } # Voice naming: ru_RU-irina-medium -> lang=ru_RU, name=irina, quality=medium $parts = $Voice -split "-" if ($parts.Length -lt 3) { throw "Bad voice name '$Voice' — expected --" } $lang = $parts[0] $name = $parts[1] $quality = $parts[2] $langCode = ($lang -split "_")[0] $Base = "https://huggingface.co/rhasspy/piper-voices/resolve/main/$langCode/$lang/$name/$quality" foreach ($pair in @( @{ url = "$Base/$Voice.onnx"; out = $VoiceFile }, @{ url = "$Base/$Voice.onnx.json"; out = $VoiceJson } )) { if ((Test-Path $pair.out) -and -not $Force) { Write-Information "[skip] $(Split-Path -Leaf $pair.out) already present" continue } Write-Information "[fetch] $($pair.url)" Invoke-WebRequest -Uri $pair.url -OutFile $pair.out -UseBasicParsing Write-Information "[ok] $(Split-Path -Leaf $pair.out) -> $($pair.out)" } # ─── Smoke test ──────────────────────────────────────────────────────────── $testWav = Join-Path $env:TEMP "jarvis-piper-test.wav" try { Write-Information "[test] synthesizing 'Привет, Дмитрий'..." "Привет, Дмитрий" | & $BinPath --model $VoiceFile --output_file $testWav 2>$null if ((Test-Path $testWav) -and ((Get-Item $testWav).Length -gt 1000)) { Write-Information "[ok] Piper works! Test wav: $testWav" Write-Information "" Write-Information "Next: set environment variable JARVIS_TTS=piper" Write-Information " (or just leave it unset — auto-detect will pick Piper)" } else { Write-Warning "Test wav empty or missing — synthesis may have failed" } } catch { Write-Warning "Smoke test failed: $_" } finally { if (Test-Path $testWav) { Remove-Item $testWav -Force } }