[CmdletBinding()] param( [string]$TargetRoot = "" ) $ErrorActionPreference = "Stop" if (-not $TargetRoot) { $TargetRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path } if (!(Test-Path $TargetRoot)) { throw "Target root does not exist: $TargetRoot" } $TargetRoot = (Resolve-Path $TargetRoot).Path $assetConfigPath = Join-Path $TargetRoot "config\voice-assets.json" if (!(Test-Path $assetConfigPath)) { throw "Voice asset configuration is missing: $assetConfigPath" } $assetConfig = Get-Content -Raw $assetConfigPath | ConvertFrom-Json $assets = $assetConfig.windows_x64 $shared = $assetConfig.shared $cache = Join-Path $TargetRoot ".jackailocal-builder\voice-cache" $whisperDir = Join-Path $TargetRoot "backends\whisper.cpp\windows" $piperDir = Join-Path $TargetRoot "backends\piper\windows" $whisperModelDir = Join-Path $TargetRoot "models\whisper" $piperModelDir = Join-Path $TargetRoot "models\piper" New-Item -ItemType Directory -Force -Path $cache,$whisperDir,$piperDir,$whisperModelDir,$piperModelDir | Out-Null function Download-Verified([string]$Url, [string]$Sha256, [string]$Destination) { if (Test-Path $Destination) { $existing = (Get-FileHash -Algorithm SHA256 -Path $Destination).Hash.ToLowerInvariant() if ($existing -eq $Sha256.ToLowerInvariant()) { return } Remove-Item -LiteralPath $Destination -Force } Write-Host "Downloading $Url" -ForegroundColor Cyan Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing $actual = (Get-FileHash -Algorithm SHA256 -Path $Destination).Hash.ToLowerInvariant() if ($actual -ne $Sha256.ToLowerInvariant()) { Remove-Item -LiteralPath $Destination -Force throw "SHA256 verification failed for $Url. Expected $Sha256, got $actual." } } function Copy-ArchiveContents([string]$Archive, [string]$Destination, [string]$RequiredFile) { $extract = Join-Path $cache ([IO.Path]::GetFileNameWithoutExtension($Archive)) if (Test-Path $extract) { Remove-Item -LiteralPath $extract -Recurse -Force } New-Item -ItemType Directory -Force -Path $extract | Out-Null Expand-Archive -Path $Archive -DestinationPath $extract -Force $required = Get-ChildItem -Path $extract -Recurse -File -Filter $RequiredFile | Select-Object -First 1 if (-not $required) { throw "Required file $RequiredFile was not found in $Archive." } $sourceDir = Split-Path $required.FullName -Parent Copy-Item -Path (Join-Path $sourceDir "*") -Destination $Destination -Recurse -Force } $whisperArchive = Join-Path $cache "whisper-bin-x64.zip" $piperArchive = Join-Path $cache "piper-windows-amd64.zip" $whisperModel = Join-Path $whisperModelDir "ggml-base.bin" $piperVoice = Join-Path $piperModelDir "en_US-libritts_r-medium.onnx" $piperVoiceConfig = Join-Path $piperModelDir "en_US-libritts_r-medium.onnx.json" if (!(Test-Path (Join-Path $whisperDir "whisper-cli.exe"))) { Download-Verified $assets.whisper.archive_url $assets.whisper.archive_sha256 $whisperArchive Copy-ArchiveContents $whisperArchive $whisperDir "whisper-cli.exe" } if (!(Test-Path (Join-Path $piperDir "piper.exe"))) { Download-Verified $assets.piper.archive_url $assets.piper.archive_sha256 $piperArchive Copy-ArchiveContents $piperArchive $piperDir "piper.exe" } Download-Verified $shared.whisper.model_url $shared.whisper.model_sha256 $whisperModel Download-Verified $shared.piper.voice_url $shared.piper.voice_sha256 $piperVoice Download-Verified $shared.piper.voice_config_url $shared.piper.voice_config_sha256 $piperVoiceConfig Write-Host "Voice assets installed and verified under $TargetRoot" -ForegroundColor Green