[CmdletBinding()] param( [switch]$SkipInstall, [switch]$FullTts, [switch]$Lite, [switch]$NoAdmin ) $ErrorActionPreference = "Stop" [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) $OutputEncoding = [System.Text.UTF8Encoding]::new($false) $Root = Resolve-Path (Join-Path $PSScriptRoot "..") $AdminRoot = Join-Path $Root "frontend\admin" $DistRoot = Join-Path $Root "dist" $AppDist = Join-Path $DistRoot "LiveStreaming" $BuildRoot = Join-Path $Root "build" $PyBuild = Join-Path $BuildRoot "pyinstaller" function Write-Step([string]$Message) { Write-Host "" Write-Host "==> $Message" -ForegroundColor Cyan } function Invoke-Checked([string]$File, [string[]]$Arguments) { & $File @Arguments if ($LASTEXITCODE -ne 0) { throw "$File $($Arguments -join ' ') failed with exit code $LASTEXITCODE." } } function Get-ProjectPython { $candidates = @( $env:LIVE_STREAMING_PYTHON, (Join-Path $Root ".venv-tts\Scripts\python.exe"), (Join-Path $Root ".venv\Scripts\python.exe"), "E:\Programs\Anaconda3\envs\Live-streaming\python.exe", "python" ) | Where-Object { $_ } foreach ($candidate in $candidates) { try { $version = & $candidate -c "import sys; print(sys.version.split()[0])" 2>$null if ($LASTEXITCODE -eq 0 -and $version) { Write-Host "Python: $candidate ($version)" return $candidate } } catch { } } throw "No usable Python found. Create .venv, install Python 3.10+, or set LIVE_STREAMING_PYTHON." } $Python = Get-ProjectPython $PythonPrefix = (& $Python -c "import sys; print(sys.prefix)").Trim() if ($LASTEXITCODE -ne 0 -or -not $PythonPrefix) { throw "Unable to resolve Python prefix from $Python." } $CondaBin = Join-Path $PythonPrefix "Library\bin" if (Test-Path $CondaBin) { $env:PATH = "$CondaBin;$env:PATH" } if (-not $SkipInstall) { Write-Step "Install Python runtime/build dependencies" Invoke-Checked $Python @("-m", "pip", "install", "-r", (Join-Path $Root "requirements.txt")) } Write-Step "Build Vue admin dist" if (-not (Test-Path (Join-Path $AdminRoot "node_modules"))) { Invoke-Checked "npm" @("--prefix", $AdminRoot, "install") } Invoke-Checked "npm" @("--prefix", $AdminRoot, "run", "build") Write-Step "Prepare PyInstaller output" # 确保没有残留进程占用 dist 目录 $killed = Get-Process -Name "LiveStreaming" -ErrorAction SilentlyContinue if ($killed) { $killed | Stop-Process -Force -ErrorAction SilentlyContinue $killed | Wait-Process -Timeout 5 -ErrorAction SilentlyContinue Start-Sleep -Seconds 2 Write-Host " Stopped LiveStreaming.exe before build" } function Remove-WithRetry([string]$Path) { if (-not (Test-Path $Path)) { return } $retries = 3 while ($retries -gt 0) { try { Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop return } catch { $retries-- if ($retries -eq 0) { Write-Host " WARNING: Cannot remove $Path, skipping" -ForegroundColor Yellow } else { Write-Host " Retry remove $Path ($retries left)..." -ForegroundColor Yellow Start-Sleep -Seconds 2 } } } } Remove-WithRetry $AppDist Remove-WithRetry $PyBuild New-Item -ItemType Directory -Force -Path $DistRoot | Out-Null New-Item -ItemType Directory -Force -Path $PyBuild | Out-Null $excludeModules = @( "tkinter", "pytest", "IPython", "jupyter", "notebook", "matplotlib.tests", "numpy.tests", "pandas.tests", "scipy.tests", "torch.testing" ) if ($Lite) { $excludeModules += @( "torch", "torchaudio", "torchvision", "transformers", "faster_qwen3_tts", "dots_tts" ) } else { $excludeModules += @( "torch.distributed", "torch.utils.tensorboard", "torchvision", "torchaudio._internal", "torch.utils.benchmark", "torch.testing", "torch.onnx", "tensorboard", "tensorboardX" ) } $pyiArgs = @( "--noconfirm", "--onedir", "--name", "LiveStreaming", "--distpath", $DistRoot, "--workpath", $PyBuild, "--specpath", $BuildRoot, "--paths", (Join-Path $Root "app"), "--hidden-import", "danmu_queue", "--hidden-import", "music_monitor", "--hidden-import", "tts_monitor", "--hidden-import", "core.runtime_paths" ) foreach ($module in $excludeModules) { $pyiArgs += @("--exclude-module", $module) } $condaDlls = @("ffi.dll", "liblzma.dll", "libbz2.dll", "libexpat.dll") if (Test-Path $CondaBin) { foreach ($dll in $condaDlls) { $dllPath = Join-Path $CondaBin $dll if (Test-Path $dllPath) { $pyiArgs += @("--add-binary", "$dllPath;.") } } } if (-not $NoAdmin) { $pyiArgs += "--uac-admin" } $pyiArgs += (Join-Path $Root "app\main.py") Write-Step "Package LiveStreaming.exe" # PyInstaller 隔离模式与 torch DLL 冲突,设置环境变量跳过 CUDA 加载 $env:CUDA_VISIBLE_DEVICES = "" $env:PYTORCH_NVFUSER_DISABLE = "1" $SpecFile = Join-Path $BuildRoot "LiveStreaming.spec" if (Test-Path $SpecFile) { Write-Host " Reusing spec (incremental) ..." Invoke-Checked $Python @("-m", "PyInstaller", "--noconfirm", $SpecFile) } else { Invoke-Checked $Python (@("-m", "PyInstaller") + $pyiArgs) } Write-Step "Create runtime directories" $ExePath = Join-Path $AppDist "LiveStreaming.exe" if (-not (Test-Path $ExePath)) { throw "PyInstaller finished without creating $ExePath." } New-Item -ItemType Directory -Force -Path (Join-Path $AppDist "data") | Out-Null New-Item -ItemType Directory -Force -Path (Join-Path $AppDist "logs") | Out-Null Copy-Item -LiteralPath (Join-Path $Root "web") -Destination (Join-Path $AppDist "web") -Recurse -Force Copy-Item -LiteralPath (Join-Path $Root "config") -Destination (Join-Path $AppDist "config") -Recurse -Force Copy-Item -LiteralPath (Join-Path $Root "integrations") -Destination (Join-Path $AppDist "integrations") -Recurse -Force Copy-Item -LiteralPath (Join-Path $Root "docs") -Destination (Join-Path $AppDist "docs") -Recurse -Force Copy-Item -LiteralPath (Join-Path $Root "vendor") -Destination (Join-Path $AppDist "vendor") -Recurse -Force Copy-Item -LiteralPath (Join-Path $Root "README.md") -Destination (Join-Path $AppDist "README.md") -Force Write-Host "" Write-Host "Build complete: $AppDist\LiveStreaming.exe" -ForegroundColor Green if ($Lite) { Write-Host "Lite build: TTS/GPU dependencies excluded." } else { Write-Host "Full build with TTS support." }