KAZIK/tools/fortune_ai/download_model.ps1
Игорь Федосеев 5dedaf459f fix: restore fortune AI and add model setup
Restore native inference, card-bound readings and paged dialogue.

Provide manual, checksum-verified model setup instead of Git LFS so
source checkouts work without uploading multi-gigabyte weights. The
game stays offline; complete release archives still bundle the model.
2026-10-31 00:37:36 +03:00

88 lines
4.2 KiB
PowerShell

param(
[string]$ManifestPath = '',
[string]$AiDirectory = ''
)
$ErrorActionPreference = 'Stop'
$root = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..'))
if (!$ManifestPath) { $ManifestPath = Join-Path $root 'ai/model.json' }
if (!$AiDirectory) { $AiDirectory = Join-Path $root 'ai' }
$lock = $null
function Test-Model([string]$Path, $Manifest) {
if (!(Test-Path -LiteralPath $Path -PathType Leaf)) { return $false }
if ((Get-Item -LiteralPath $Path).Length -ne [long]$Manifest.bytes) { return $false }
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash -eq $Manifest.sha256
}
try {
$manifest = Get-Content -LiteralPath $ManifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
if ([long]$manifest.bytes -le 0 -or $manifest.sha256 -notmatch '^[a-fA-F0-9]{64}$') {
throw 'Invalid model size or SHA-256 in manifest'
}
$ai = [IO.Path]::GetFullPath($AiDirectory).TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
$model = [IO.Path]::GetFullPath((Join-Path $ai $manifest.file))
if (!$model.StartsWith($ai, [StringComparison]::OrdinalIgnoreCase)) {
throw 'Model destination must be inside the ai directory'
}
if (Test-Model $model $manifest) {
Write-Host 'SUCCESS: model already present; size and SHA-256 verified. Nothing to download.'
exit 0
}
$curl = (Get-Command curl.exe -ErrorAction Stop).Source
New-Item -ItemType Directory -Force -Path ([IO.Path]::GetDirectoryName($model)) | Out-Null
$lock = [IO.File]::Open("$model.download.lock", [IO.FileMode]::OpenOrCreate,
[IO.FileAccess]::ReadWrite, [IO.FileShare]::None)
$part = "$model.part"
$urls = @($manifest.download_urls)
if (!$urls.Count -or !$urls[0]) { throw 'No download URLs in model manifest' }
Write-Host ('Download size: {0:N2} GB. Keep at least 3 GB free.' -f ([long]$manifest.bytes / 1e9))
Write-Host "Destination: $model"
foreach ($url in $urls) {
$uri = [Uri]$url
if ($uri.Scheme -ne 'https' -and !($uri.Scheme -eq 'http' -and $uri.IsLoopback)) {
throw 'Model downloads require HTTPS'
}
Write-Host "Source: $($uri.Host)"
if (Test-Path -LiteralPath $part) {
$length = (Get-Item -LiteralPath $part).Length
if ($length -eq [long]$manifest.bytes -and (Test-Model $part $manifest)) {
Move-Item -LiteralPath $part -Destination $model -Force
Write-Host 'SUCCESS: completed partial file verified and installed.'
exit 0
}
if ($length -ge [long]$manifest.bytes) { Remove-Item -LiteralPath $part }
}
# curl validates range responses before appending. Keep partial data on outages.
$arguments = @('--fail', '--location', '--proto', '=http,https', '--proto-redir', '=http,https',
'--retry', '2', '--retry-delay', '1', '--connect-timeout', '30',
'--speed-limit', '1024', '--speed-time', '60', '--continue-at', '-', '--output', $part, $url)
& $curl @arguments
$result = $LASTEXITCODE
if ($result -eq 33 -or $result -eq 36) {
# Some mirrors ignore Range; retry this source once from the beginning.
if (Test-Path -LiteralPath $part) { Remove-Item -LiteralPath $part }
& $curl @arguments
$result = $LASTEXITCODE
}
if ($result -ne 0) {
Write-Warning "Download interrupted (curl $result). Trying next source; partial data retained."
continue
}
Write-Host 'Verifying downloaded size and SHA-256...'
if (!(Test-Model $part $manifest)) {
Write-Warning 'Downloaded file failed integrity verification. Trying next source.'
if (Test-Path -LiteralPath $part) { Remove-Item -LiteralPath $part }
continue
}
# Never replace an existing destination with unverified data.
Move-Item -LiteralPath $part -Destination $model -Force
Write-Host 'SUCCESS: offline model downloaded, verified and installed.'
exit 0
}
throw 'All model sources failed. Run download_fortune_model.bat again to retry/resume.'
} catch {
Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red
exit 1
} finally {
if ($lock) { $lock.Dispose() }
}