Files
cyber-xiaoya-live/start-component-stack.ps1

241 lines
14 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ============================================================
# 一键拉起:组件版数字人桥(8795) + 弹幕采集转发(reader) + OmniVoice(9877) + 打开舞台页
# ------------------------------------------------------------
# 用法: 双击同目录 启动-组件版全链路.cmd (或)
# powershell -NoProfile -ExecutionPolicy Bypass -File start-component-stack.ps1
# 参数: -NoBrowser 只起服务不开浏览器
# -Restart 强制重启桥(用于"改了代码没生效")
# 注意: 本文件必须保存为 UTF-8 with BOM,否则 PowerShell 5.1 中文会乱码
# ============================================================
param([switch]$NoBrowser, [switch]$Restart)
$ErrorActionPreference = 'Continue'
# ── 运行日志(2026-10-17 新增): 每次双击启动都留一份完整记录, 出问题能回看是哪一步炸的 ──
# 背景: 用户重启电脑后双击快捷方式"出 bug", 但当时没有任何痕迹可查(窗口 8 秒就关了)。
$LogDir = Join-Path $PSScriptRoot 'logs'
try { [void](New-Item -ItemType Directory -Force -Path $LogDir) } catch { }
$LogPath = Join-Path $LogDir 'launcher.log'
try {
Start-Transcript -Path $LogPath -Append -Force | Out-Null
Write-Host ("[launcher] 启动 {0:yyyy-MM-dd HH:mm:ss} 日志 → {1}" -f (Get-Date), $LogPath) -ForegroundColor DarkGray
} catch {
Write-Host ("[launcher] 日志开启失败({0}), 继续执行" -f $_.Exception.Message) -ForegroundColor DarkYellow
}
# 路径自动推导: 脚本所在目录 = 本项目目录; 弹幕采集器和外部语音桥都是可选的, 缺了就跳过
$ViduRoot = $PSScriptRoot
$DanmakuRoot = if ($env:DSH_DANMAKU_ROOT) { $env:DSH_DANMAKU_ROOT } else { Join-Path (Split-Path $PSScriptRoot -Parent) 'multi-danmaku-tts' }
$ViduPy = Join-Path $ViduRoot '.venv\Scripts\python.exe'
$ViduCfg = Join-Path $ViduRoot 'config.json'
$EnginePy = Join-Path $ViduRoot 'src\engine.py'
$OmniCmd = if ($env:DSH_OMNIVOICE_CMD) { $env:DSH_OMNIVOICE_CMD } else { '' } # 可选
$BridgeCfg = if ($env:DSH_VOICE_BRIDGE_CFG) { $env:DSH_VOICE_BRIDGE_CFG } else { '' } # 可选
$StageUrl = 'http://localhost:8795/stage' # 用 localhost 而非字面 127.0.0.1: Chrome 里后者可能被代理扩展的"绕过列表"挡掉(2026-10-17 实测: localhost 能开、127.0.0.1 打不开)
$ApiBase = 'http://127.0.0.1:8795'
function Say($t, $c = 'Gray') { Write-Host $t -ForegroundColor $c }
function Rule() { Say (' ' + ('-' * 60)) 'DarkGray' }
function Test-Port([string]$h, [int]$p, [int]$ms = 2000) {
# ⚠️ 必须带超时(2026-10-14 故障修复): 原来 $c.Connect($h, $p) 无超时, 探"黑洞地址"时
# (本机 WSL 镜像网络下 LAN IP 192.168.x.x 从 Windows 不可达) 会挂几十秒/次;
# Wait-Port 循环 150 次 → 实际卡几小时, 表现就是"重启电脑后快捷方式拉不起来"。
# 这里改用 BeginConnect + WaitOne 兜住, 单次最多 $ms 毫秒。
try {
$c = New-Object Net.Sockets.TcpClient
$iar = $c.BeginConnect($h, $p, $null, $null)
$ok = $iar.AsyncWaitHandle.WaitOne($ms, $false) -and $c.Connected
$c.Close()
return $ok
} catch { return $false }
}
function Wait-Port([string]$h, [int]$p, [int]$sec) {
for ($i = 0; $i -lt $sec; $i++) {
if (Test-Port $h $p) { return $true }
if ($i -gt 0 -and ($i % 15) -eq 0) {
Say (" …等 OmniVoice 就绪 {0}s/{1}s(冷启动通常 1-3 分钟)" -f $i, $sec) DarkGray
}
Start-Sleep 1
}
return (Test-Port $h $p)
}
function Get-OmniBase {
# ⭐ 2026-10-10 实测: 本机 WSL2 已切镜像网络模式, 服务从 localhost 直达——
# LAN IP 随路由器换段(192.168.1.x→192.168.3.x)且对 LAN IP 的探测会被 Hyper-V
# 防火墙吞掉(连接挂死 150s) → 永远先试 localhost, 活着就用它。
# ⭐ 2026-10-14 加固: 候选地址**每个都先验证可达**(Test-Port 带超时), 不可达的一律不采用;
# 全部不通时**回落 localhost**(服务此刻可能还没起, 交给后面的 Wait-Port 等它),
# 绝不返回一个本机不可达的 LAN IP —— 那是"卡死在 [2/4]"的根因。
$cands = New-Object System.Collections.ArrayList
[void]$cands.Add('http://127.0.0.1:9877')
foreach ($f in @($BridgeCfg, $ViduCfg) | Where-Object { $_ }) {
try {
$j = Get-Content $f -Raw -Encoding UTF8 | ConvertFrom-Json
$b = if ($f -eq $BridgeCfg) { $j.omnivoice.base } else { $j.tts.omnivoice_base }
if ($b) { [void]$cands.Add([string]$b) }
} catch {}
}
foreach ($c in $cands) {
try { $u = [Uri]$c } catch { continue }
if ($u.Host -and $u.Port -gt 0 -and (Test-Port $u.Host $u.Port 2000)) { return $c }
}
return 'http://127.0.0.1:9877'
}
function Get-ViduBuild { # 源码里的版本号
try {
$m = Select-String -Path $EnginePy -Pattern 'BUILD\s*=\s*"([^"]+)"' | Select-Object -First 1
if ($m) { return $m.Matches[0].Groups[1].Value }
} catch {}
return ''
}
function Get-LiveBuild { # 正在跑的进程的版本号
try { return [string](Invoke-RestMethod "$ApiBase/api/health" -TimeoutSec 3).build } catch { return '' }
}
function Get-ReaderProcs {
# ⚠️ `return ,$list`: PS 5.1 的 return 会展开数组; 只剩一个对象时调用方 .Count = $null
# → `$null -gt 0` 为假 → 会误判"没在跑"并重复拉起一个 reader
$list = @(Get-CimInstance Win32_Process -Filter "Name like 'python%'" |
Where-Object { $_.CommandLine -and $_.CommandLine -match 'reader\.py' })
return ,$list
}
Say ''
Say ' ╔══════════════════════════════════════════════════════════╗' 'Magenta'
Say ' ║ 组件版数字人 + 弹幕朗读 一键全链路 ║' 'Magenta'
Say ' ╚══════════════════════════════════════════════════════════╝' 'Magenta'
Rule
# ── [1/4] 组件版数字人桥 8795(先起桥: 4 秒就绪且不依赖 OmniVoice, 防中途关窗全断) ──
$omniBase = Get-OmniBase
$omniUri = [Uri]$omniBase
Say ''; Say ' [1/4] 组件版数字人桥 :8795' Cyan
$needRestart = $false
if ($Restart -and (Test-Port '127.0.0.1' 8795)) {
Say ' -Restart:停掉旧桥…' Yellow
Get-NetTCPConnection -LocalPort 8795 -State Listen -ErrorAction SilentlyContinue | ForEach-Object {
$ownerId = $_.OwningProcess
$own = Get-CimInstance Win32_Process -Filter "ProcessId=$ownerId" -ErrorAction SilentlyContinue
$parId = if ($own) { $own.ParentProcessId } else { 0 }
$par = if ($parId) { Get-CimInstance Win32_Process -Filter "ProcessId=$parId" -ErrorAction SilentlyContinue } else { $null }
Stop-Process -Id $ownerId -Force -ErrorAction SilentlyContinue
# ⚠️⚠️ 关键安全判断(2026-10-14): 只有父进程是 python(venv 包装父进程) 时才一起杀。
# 桥改由计划任务托管后, 它的父进程是 **svchost.exe(任务计划服务)** ——
# 照原逻辑"连父进程一起杀"会把 svchost 干掉, 那是系统级事故。绝不可省这个判断。
if ($par -and $par.Name -like 'python*') { Stop-Process -Id $parId -Force -ErrorAction SilentlyContinue }
}
Start-Sleep 2
}
if (Test-Port '127.0.0.1' 8795) {
$live = Get-LiveBuild
$src = Get-ViduBuild
Say " ✅ 已在运行(build=$live)" Green
if ($src -and $live -and $src -ne $live) {
Say " ⚠ 进程跑的是旧代码:源码 build=$src ≠ 运行 build=$live" Red
Say ' 改完代码必须重启桥才生效 → 重跑本脚本并加 -Restart' Red
$needRestart = $true
}
} else {
Say ' ⏳ 未运行,启动中…(计划任务托管 · 无窗口)' Yellow
# ⭐ 2026-10-14: 服务改由 Windows 计划任务托管(父进程=svchost) —— 与窗口彻底解耦,
# 关窗口/关终端都不会带走服务; 任务不存在时(换机器/被清理)回退成隐藏窗口启动。
if (Get-ScheduledTask -TaskName 'DSH-vidu-bridge' -ErrorAction SilentlyContinue) {
Start-ScheduledTask -TaskName 'DSH-vidu-bridge'
} else {
Start-Process -FilePath $ViduPy -ArgumentList '-m', 'src.main', '--no-browser' `
-WorkingDirectory $ViduRoot -WindowStyle Hidden
}
$ok = $false
for ($i = 0; $i -lt 40; $i++) {
Start-Sleep 1
if (Test-Port '127.0.0.1' 8795) { $ok = $true; break }
}
if ($ok) { Say " ✅ 已就绪(build=$(Get-LiveBuild))" Green }
else { Say ' ❌ 启动失败:看 logs\bridge.log 末尾的报错' Red }
}
# ── [2/4] OmniVoice 引擎(数字人的嗓子,WSL2;冷启动 1-3 分钟,桥不等它也能用) ──
Say ''; Say " [2/4] OmniVoice 引擎 $omniBase" Cyan
if (Test-Port $omniUri.Host $omniUri.Port) {
Say ' ✅ 已在运行' Green
} else {
Say ' ⏳ 没在跑,正在拉起 WSL2 OmniVoice(首次约 1-3 分钟)…' Yellow
if (Test-Path $OmniCmd) {
# 隐藏窗口拉起(2026-10-14 用户要求"别弹这些窗口"): 进度看 WSL 里的 server.log
Start-Process -FilePath 'cmd.exe' -ArgumentList '/c', "`"$OmniCmd`"" -WindowStyle Hidden
if (Wait-Port $omniUri.Host $omniUri.Port 240) { Say ' ✅ 已就绪' Green }
else { Say ' ⚠ 超时未就绪——数字人可能没声音,先继续(桥/采集器已就绪)' Red }
} else {
Say ' ⏭ 未配置 OmniVoice 启动脚本(可用环境变量 DSH_OMNIVOICE_CMD 指定),跳过' DarkGray
}
}
# OmniVoice 地址若变了,同步给桥(桥的 tts.omnivoice_base)
if (Test-Port '127.0.0.1' 8795) {
try {
$cur = (Invoke-RestMethod "$ApiBase/api/settings" -TimeoutSec 5).tts.omnivoice_base
if ($cur -and $omniBase -and $cur -ne $omniBase) {
Invoke-RestMethod "$ApiBase/api/settings" -Method Post -ContentType 'application/json' `
-Body (@{ tts = @{ omnivoice_base = $omniBase } } | ConvertTo-Json -Compress) -TimeoutSec 5 | Out-Null
Say " ↻ OmniVoice 地址已更新为 $omniBase(WSL IP 变了)" Yellow
}
} catch {}
}
# ── [3/4] 弹幕采集转发(reader:抖音/B站/微信 → 数字人) ────────
Say ''; Say ' [3/4] 弹幕采集转发(reader.py + douyinLive + 微信)' Cyan
$rd = Get-ReaderProcs
if ($rd.Count -gt 0) {
Say " ✅ 已在运行($($rd.Count) 个进程)" Green
} else {
Say ' ⏳ 启动中…(计划任务托管 · 无窗口)' Yellow
if (Get-ScheduledTask -TaskName 'DSH-danmaku-reader' -ErrorAction SilentlyContinue) {
Start-ScheduledTask -TaskName 'DSH-danmaku-reader'
} else {
Start-Process -FilePath 'cmd.exe' -ArgumentList '/c', 'start.cmd' `
-WorkingDirectory $DanmakuRoot -WindowStyle Hidden
}
Start-Sleep 10
$rd = Get-ReaderProcs
if ($rd.Count -gt 0) { Say ' ✅ 已就绪(托盘图标在任务栏)' Green }
else { Say " ⚠ 没起来,手动跑 $DanmakuRoot\启动-弹幕朗读全链路.cmd 看看" Red }
}
# ── [4/4] 打开舞台页 ──────────────────────────────────────────
Say ''; Say ' [4/4] 舞台页' Cyan
if ($NoBrowser) { Say " (-NoBrowser)$StageUrl" DarkGray }
else { Start-Process $StageUrl; Say " ✅ 已打开 $StageUrl" Green }
# ── 汇总 ──────────────────────────────────────────────────────
Say ''; Rule
$h = $null
try { $h = Invoke-RestMethod "$ApiBase/api/health" -TimeoutSec 3 } catch {}
Say ' 【状态】' White
Say (" 桥 build = {0} phase = {1}" -f $(if ($h) { $h.build } else { '未响应' }), $(if ($h) { $h.phase } else { '-' })) Gray
Say (" OmniVoice {0} reader {1} 个进程" -f $omniBase, @(Get-ReaderProcs).Count) Gray
Say ''; Say ' 【弹幕朗读规则(当前生效)】' White
Say ' 优先级:歌曲 > 你的打字聊天 > 弹幕复读+回答' Gray
Say ' · 歌在播 → 弹幕/聊天回复全部排队(连 LLM 都不请求),等歌完或点「⏹ 停止演唱」' DarkGray
Say ' · 每 5 秒一拍:把这一拍里的多条弹幕一起喂 LLM(最多 14 条合并成一次回答)' DarkGray
Say ' · 只复读其中 1 条(优先“提问那条”);普通弹幕不念昵称,VIP 送礼观众照念' DarkGray
Say ' · 送礼观众(VIP) 优先取件,但守自己的 5 秒间隔' DarkGray
Say ' 弹幕模式:托盘「念弹幕内容」开关控制' Gray
Say ' · 开(默认) = 复读弹幕 + 回答;关 = 不复读,只让数字人回应(带观众名标签问 LLM)' DarkGray
Say ' 过滤:命中「触审词/黑名单」整条丢弃(空翻/倒立/劈叉/国旗/国徽/站起来等 23+43 词)' Gray
Say ' 昵称:中文前2字 / 数字前4位 / 英文首词 / 混排从首个汉字起取2个可读字符' Gray
Say ' · 不再加「水友」前缀(省朗读时间);纯符号 ID 走泛称「小可爱/宝子」' DarkGray
Say ' 礼物感谢:6 秒窗口合并成一句话(≥4 人或 ≥50 钻立刻发;每 25 秒最多一句、每分钟≤2 句)' Gray
Say ''; Say ' 【常用操作】' White
Say ' · 桥 / 采集器由 Windows 计划任务托管(无窗口),关窗口/关终端都不影响运行' Gray
Say ' · 重启桥:Start-ScheduledTask -TaskName DSH-vidu-bridge' DarkGray
Say ' · 重启采集器:Start-ScheduledTask -TaskName DSH-danmaku-reader' DarkGray
Say ' · 查状态:Get-ScheduledTask DSH-vidu-bridge,DSH-danmaku-reader | Get-ScheduledTaskInfo' DarkGray
Say ' · 改 config.json 里不热加载的项(门槛/模板/间隔)→ 托盘右键「重启朗读器」' Gray
Say ' · 改了桥的代码 → 重跑本脚本并加 -Restart(脚本会核对 build 版本)' Gray
Say ' · 歌词与画面字幕:放歌时画面字幕层自动让位,歌完自动贴回;位置/字号在设置抽屉调' Gray
Say ''; Say ' ⚠ 弹幕只在与数字人「连线」后转发:未连线时采集器只抓不转发' Yellow
Rule
# ── 收尾: 关掉本次运行日志(2026-10-17) ──
try { Write-Host ('[launcher] 完成 {0:HH:mm:ss}' -f (Get-Date)) -ForegroundColor DarkGray; Stop-Transcript | Out-Null } catch { }