109 lines
4.3 KiB
PowerShell
109 lines
4.3 KiB
PowerShell
|
|
#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>-<name>-<quality>"
|
||
|
|
}
|
||
|
|
$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 }
|
||
|
|
}
|