param( [string]$ServerUrl, [string]$PairCode, [string]$DeviceName = $env:COMPUTERNAME, [string]$PackageUrl, [string]$RuntimeUrl, [switch]$RepairRuntime, [switch]$NoAutoStart ) $ErrorActionPreference = 'Stop' $WorkerHome = Join-Path $env:LOCALAPPDATA 'KeoVideoTrungQuoc' $ProjectRoot = Join-Path $WorkerHome 'app' $CredentialFile = Join-Path $WorkerHome 'worker-agent.json' $LogFile = Join-Path $WorkerHome 'worker.log' $Launcher = Join-Path $WorkerHome 'run-worker.ps1' New-Item -ItemType Directory -Path $WorkerHome -Force | Out-Null if (-not [Environment]::Is64BitOperatingSystem) { throw 'Vietsub Studio chi ho tro Windows 64-bit.' } $SystemDrive = Get-PSDrive -Name ([IO.Path]::GetPathRoot($env:LOCALAPPDATA).TrimEnd(':\\')) -ErrorAction SilentlyContinue if ($SystemDrive -and $SystemDrive.Free -lt 10GB) { throw 'O dia he thong con duoi 10 GB. Hay giai phong dung luong roi chay lai bo cai.' } $MemoryGB = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 1) if ($MemoryGB -lt 8) { throw "May chi co $MemoryGB GB RAM. Qwen 7B can toi thieu 8 GB RAM." } Write-Host '[1/7] Da kiem tra Windows, RAM va dung luong.' -ForegroundColor Cyan $ExistingCredential = if (Test-Path -LiteralPath $CredentialFile) { Get-Content -Raw -LiteralPath $CredentialFile | ConvertFrom-Json } else { $null } if (-not $ServerUrl -and $ExistingCredential.serverUrl) { $ServerUrl = $ExistingCredential.serverUrl } if (-not $ServerUrl) { $ServerUrl = Read-Host 'Dia chi web app (mac dinh https://vietsub.hocmate.com)'; if (-not $ServerUrl) { $ServerUrl = 'https://vietsub.hocmate.com' } } $ServerUrl = $ServerUrl.TrimEnd('/') $Manifest = $null try { $Manifest = Invoke-RestMethod -Uri "$ServerUrl/downloads/manifest.json" -UseBasicParsing -TimeoutSec 10 } catch {} function Assert-PackageHash([string]$FilePath, [string]$FileName) { if (-not $Manifest -or -not $Manifest.files) { return } $Entry = $Manifest.files.$FileName if (-not $Entry -or -not $Entry.sha256) { return } $Actual = (Get-FileHash -LiteralPath $FilePath -Algorithm SHA256).Hash.ToLowerInvariant() $Expected = ([string]$Entry.sha256).ToLowerInvariant() if ($Actual -ne $Expected) { throw "Kiem tra SHA-256 cua $FileName khong khop (Expected: $Expected, Actual: $Actual)." } } if (-not $PackageUrl) { $PackageUrl = "$ServerUrl/downloads/vietsub-worker-windows.zip" } $Archive = Join-Path $env:TEMP 'vietsub-worker-windows.zip' Write-Host '[2/7] Dang tai chuong trinh Worker...' -ForegroundColor Cyan Invoke-WebRequest -Uri $PackageUrl -OutFile $Archive -UseBasicParsing Assert-PackageHash -FilePath $Archive -FileName 'vietsub-worker-windows.zip' New-Item -ItemType Directory -Path $ProjectRoot -Force | Out-Null Expand-Archive -LiteralPath $Archive -DestinationPath $ProjectRoot -Force Remove-Item -LiteralPath $Archive -Force -ErrorAction SilentlyContinue if (-not (Test-Path -LiteralPath (Join-Path $ProjectRoot 'src\worker-agent.js'))) { throw 'Goi worker tai ve khong hop le. Hay tai lai tu trang Cai dat may.' } $Ffmpeg = Get-ChildItem -LiteralPath (Join-Path $ProjectRoot 'tools\ffmpeg') -Filter ffmpeg.exe -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 $Whisper = Join-Path $ProjectRoot 'tools\whispercpp\Release\whisper-cli.exe' $Python = Join-Path $ProjectRoot 'tools\venv\Scripts\python.exe' if ($RepairRuntime -or -not $Ffmpeg -or -not (Test-Path -LiteralPath $Whisper) -or -not (Test-Path -LiteralPath $Python)) { if (-not $RuntimeUrl) { $RuntimeUrl = "$ServerUrl/downloads/vietsub-runtime-minimal-windows.zip" } $RuntimeArchive = Join-Path $env:TEMP 'vietsub-runtime-minimal-windows.zip' $RuntimeStage = Join-Path $env:TEMP 'vietsub-runtime-stage' Write-Host '[3/7] Dang tai bo xu ly video va nhan dang giong noi (khoang 490 MB)...' -ForegroundColor Cyan Invoke-WebRequest -Uri $RuntimeUrl -OutFile $RuntimeArchive -UseBasicParsing Assert-PackageHash -FilePath $RuntimeArchive -FileName 'vietsub-runtime-minimal-windows.zip' Remove-Item -LiteralPath $RuntimeStage -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path $RuntimeStage -Force | Out-Null Expand-Archive -LiteralPath $RuntimeArchive -DestinationPath $RuntimeStage -Force New-Item -ItemType Directory -Path (Join-Path $ProjectRoot 'tools\ffmpeg'),(Join-Path $ProjectRoot 'tools\whispercpp'),(Join-Path $ProjectRoot 'tools\uv') -Force | Out-Null Expand-Archive -LiteralPath (Join-Path $RuntimeStage 'ffmpeg.zip') -DestinationPath (Join-Path $ProjectRoot 'tools\ffmpeg') -Force Expand-Archive -LiteralPath (Join-Path $RuntimeStage 'whisper-bin-x64.zip') -DestinationPath (Join-Path $ProjectRoot 'tools\whispercpp') -Force Copy-Item -LiteralPath (Join-Path $RuntimeStage 'ggml-tiny.bin') -Destination (Join-Path $ProjectRoot 'tools\whispercpp\ggml-tiny.bin') -Force if (Test-Path -LiteralPath (Join-Path $RuntimeStage 'ggml-small.bin')) { Copy-Item -LiteralPath (Join-Path $RuntimeStage 'ggml-small.bin') -Destination (Join-Path $ProjectRoot 'tools\whispercpp\ggml-small.bin') -Force } Expand-Archive -LiteralPath (Join-Path $RuntimeStage 'uv.zip') -DestinationPath (Join-Path $ProjectRoot 'tools\uv') -Force if (Test-Path -LiteralPath (Join-Path $RuntimeStage 'vieneu-cache')) { Copy-Item -LiteralPath (Join-Path $RuntimeStage 'vieneu-cache') -Destination (Join-Path $ProjectRoot 'tools\vieneu-cache') -Recurse -Force } $Uv = Join-Path $ProjectRoot 'tools\uv\uv.exe' & $Uv venv --python 3.12 (Join-Path $ProjectRoot 'tools\venv') if ($LASTEXITCODE -ne 0) { throw 'Khong tao duoc moi truong Python cho worker.' } & $Uv pip install --python (Join-Path $ProjectRoot 'tools\venv\Scripts\python.exe') -r (Join-Path $ProjectRoot 'requirements-worker.txt') if ($LASTEXITCODE -ne 0) { throw 'Khong cai duoc yt-dlp, Edge TTS va bo dich.' } Remove-Item -LiteralPath $RuntimeArchive -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $RuntimeStage -Recurse -Force -ErrorAction SilentlyContinue } else { Write-Host '[3/7] Bo xu ly video da san sang, dang cap nhat thu vien Python...' -ForegroundColor Green $Uv = Join-Path $ProjectRoot 'tools\uv\uv.exe' if (Test-Path -LiteralPath $Uv) { & $Uv pip install --python $Python -r (Join-Path $ProjectRoot 'requirements-worker.txt') | Out-Null } } $NodeCommand = Get-Command node.exe -ErrorAction SilentlyContinue $Node = if ($NodeCommand) { $NodeCommand.Source } else { $null } $Major = if ($Node) { [int]((& $Node --version).TrimStart('v').Split('.')[0]) } else { 0 } if ($Major -lt 20) { Write-Host '[4/7] Dang cai Node.js portable...' -ForegroundColor Cyan $RuntimeHome = Join-Path $WorkerHome 'runtime' $Index = Invoke-RestMethod -Uri 'https://nodejs.org/dist/index.json' -UseBasicParsing $Release = $Index | Where-Object { $_.lts -and ($_.files -contains 'win-x64-zip') -and ([int]($_.version.TrimStart('v').Split('.')[0]) -ge 20) } | Select-Object -First 1 if (-not $Release) { throw 'Khong tim thay ban Node.js LTS phu hop tu nodejs.org.' } $ArchiveName = "node-$($Release.version)-win-x64.zip" $DownloadUrl = "https://nodejs.org/dist/$($Release.version)/$ArchiveName" $Archive = Join-Path $env:TEMP $ArchiveName $ChecksumFile = Join-Path $env:TEMP "$ArchiveName.sha256.txt" Invoke-WebRequest -Uri $DownloadUrl -OutFile $Archive -UseBasicParsing Invoke-WebRequest -Uri "https://nodejs.org/dist/$($Release.version)/SHASUMS256.txt" -OutFile $ChecksumFile -UseBasicParsing $Expected = ((Get-Content -LiteralPath $ChecksumFile | Where-Object { $_ -match "\s+$([regex]::Escape($ArchiveName))$" } | Select-Object -First 1) -split '\s+')[0] $Actual = (Get-FileHash -LiteralPath $Archive -Algorithm SHA256).Hash.ToLowerInvariant() if (-not $Expected -or $Actual -ne $Expected.ToLowerInvariant()) { throw 'Kiem tra SHA-256 cua Node.js khong thanh cong.' } Remove-Item -LiteralPath $RuntimeHome -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path $RuntimeHome -Force | Out-Null Expand-Archive -LiteralPath $Archive -DestinationPath $RuntimeHome -Force $Node = (Get-ChildItem -LiteralPath $RuntimeHome -Filter node.exe -Recurse | Select-Object -First 1).FullName Remove-Item -LiteralPath $Archive,$ChecksumFile -Force -ErrorAction SilentlyContinue } else { Write-Host '[4/7] Node.js da san sang.' -ForegroundColor Green } if (-not $Node -or -not (Test-Path -LiteralPath $Node)) { throw 'Khong khoi tao duoc Node.js portable.' } $OllamaCandidates = @( (Join-Path $env:LOCALAPPDATA 'Programs\Ollama\ollama.exe'), (Join-Path $env:ProgramFiles 'Ollama\ollama.exe') ) $OllamaCommand = Get-Command ollama.exe -ErrorAction SilentlyContinue $Ollama = if ($OllamaCommand) { $OllamaCommand.Source } else { $OllamaCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 } if (-not $Ollama) { $Winget = Get-Command winget.exe -ErrorAction SilentlyContinue if (-not $Winget) { throw 'Thieu Ollama va Windows Package Manager. Hay cai Ollama tu https://ollama.com/download/windows roi chay lai bo cai.' } Write-Host 'Dang cai Ollama de dich Qwen hoan toan tren may...' -ForegroundColor Cyan & $Winget.Source install --id Ollama.Ollama --exact --silent --accept-package-agreements --accept-source-agreements if ($LASTEXITCODE -ne 0) { throw 'Khong cai duoc Ollama.' } $Ollama = $OllamaCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 } if (-not $Ollama) { throw 'Da cai Ollama nhung khong tim thay ollama.exe.' } $OllamaReady = $false try { $null = Invoke-RestMethod -Uri 'http://127.0.0.1:11434/api/tags' -TimeoutSec 2 $OllamaReady = $true } catch {} if (-not $OllamaReady) { Start-Process -FilePath $Ollama -ArgumentList 'serve' -WindowStyle Hidden for ($i = 0; $i -lt 20; $i++) { Start-Sleep -Milliseconds 750 try { $null = Invoke-RestMethod -Uri 'http://127.0.0.1:11434/api/tags' -TimeoutSec 2 $OllamaReady = $true break } catch {} } } Write-Host '[5/7] Dang kiem tra AI Qwen2.5 7B (tai mot lan khoang 4.7 GB neu chua co)...' -ForegroundColor Cyan & $Ollama pull qwen2.5:7b if ($LASTEXITCODE -ne 0) { throw 'Khong tai duoc model qwen2.5:7b.' } if (-not $ExistingCredential -and -not $PairCode) { $PairCode = Read-Host 'Ma ghep cap hien thi tren web' } if (-not $ServerUrl -or (-not $ExistingCredential -and -not $PairCode)) { throw 'Thieu dia chi web app hoac ma ghep cap.' } $env:KVTQ_WORKER_CONFIG = $CredentialFile Push-Location $ProjectRoot try { if ($PairCode) { Write-Host '[6/7] Dang ghep may voi Vietsub Studio...' -ForegroundColor Cyan & $Node (Join-Path $ProjectRoot 'src\worker-agent.js') pair $ServerUrl $PairCode $DeviceName if ($LASTEXITCODE -ne 0) { throw 'Ghep may khong thanh cong.' } } } finally { Pop-Location } $LauncherContent = @" `$ErrorActionPreference = 'Continue' `$env:KVTQ_WORKER_CONFIG = '$($CredentialFile.Replace("'", "''"))' Set-Location -LiteralPath '$($ProjectRoot.Replace("'", "''"))' while (`$true) { try { `$null = Invoke-RestMethod -Uri 'http://127.0.0.1:11434/api/tags' -TimeoutSec 2 } catch { if (Test-Path -LiteralPath '$($Ollama.Replace("'", "''"))') { Start-Process -FilePath '$($Ollama.Replace("'", "''"))' -ArgumentList 'serve' -WindowStyle Hidden Start-Sleep -Seconds 2 } } & '$($Node.Replace("'", "''"))' '$((Join-Path $ProjectRoot 'src\worker-agent.js').Replace("'", "''"))' run *>> '$($LogFile.Replace("'", "''"))' Start-Sleep -Seconds 3 } "@ Set-Content -LiteralPath $Launcher -Value $LauncherContent -Encoding UTF8 if (-not $NoAutoStart) { $Action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$Launcher`"" $Trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME $Settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Days 3650) -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries Register-ScheduledTask -TaskName 'Keo Video Trung Quoc Worker' -Action $Action -Trigger $Trigger -Settings $Settings -Description 'Worker local cho Keo Video Trung Quoc' -Force | Out-Null } Get-CimInstance Win32_Process -Filter "Name = 'node.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -like '*worker-agent.js*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } Write-Host '[7/7] Dang khoi dong Worker va kiem tra ket noi...' -ForegroundColor Cyan Start-Process powershell.exe -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-WindowStyle','Hidden','-File',"`"$Launcher`"" -WindowStyle Hidden Write-Host '' Write-Host 'Da cai va khoi dong worker.' -ForegroundColor Green Write-Host "Cau hinh: $CredentialFile" Write-Host "Video thanh pham: $env:USERPROFILE\Videos\Keo Video Trung Quoc" Write-Host "Nhat ky: $LogFile"