feat: add Windows real RTC voice device simulator

This commit is contained in:
Codex Local
2026-08-27 11:02:32 +08:00
parent 3b827431c8
commit 5ff1961d79
18 changed files with 6036 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
# Windows 本地真实 RTC 联调
该覆盖层只用于本机联调,不访问阿里云服务器。它把 API 的 RTC Token 签发器和 Worker 的语音供应商切换到火山引擎,同时保留数据库、Redis、MinIO、Keycloak和告警模拟器在本地 Docker 中运行。
## 1. 生成本地凭据
在项目根目录运行:
```powershell
.\deploy\local-rtc\configure.ps1
```
脚本会以隐藏输入方式依次读取:
- RTC AppKey;
- 火山引擎 AccessKey ID / Secret AccessKey;
- 豆包端到端语音 APP ID / Access Token;
- 豆包音色 ID。
密钥文件只生成到`deploy/local-rtc/runtime`,该目录不会提交到 Git。当前首轮联调使用豆包 S2S-O 纯端到端模式(`OutputMode=0`),平台本地路由仍沿用`s2s_hybrid`模板槽位;这是为了只依赖豆包端到端语音凭据,先验证真实双向音频。后续接入火山方舟 Endpoint 后再切换真正的混合编排模式。
## 2. 启动本地后端
```powershell
.\deploy\local-rtc\start.ps1
```
首次构建需要下载 Docker 基础镜像。脚本会等待`http://localhost:58080/health/ready`可用。
## 3. 启动模拟器
双击`device-simulator-windows/release/voice-device-simulator-0.1.0-windows-x64.exe`,然后:
1. 保持默认本地平台地址、设备 ID 和本地测试设备密钥;
2. 点击“检测平台和音频设备”;
3. 选择麦克风和扬声器;
4. 点击“开始语音聊天”;
5. 看到“豆包语音任务已激活”后直接说话;
6. 点击“结束通话”验证`StopVoiceChat`和RTC离房。
RTC AppKey、火山 AK/SK 和豆包 Access Token 均不进入模拟器安装包,也不会通过渲染页面读取。
+24
View File
@@ -0,0 +1,24 @@
services:
api:
environment:
RTC_APP_ID: ${RTC_APP_ID}
RTC_TOKEN_ISSUER: volcengine
RTC_APP_KEY: ""
RTC_APP_KEY_FILE: /run/secrets/local-rtc/rtc-app-key
volumes:
- ./deploy/local-rtc/runtime/rtc-app-key:/run/secrets/local-rtc/rtc-app-key:ro
worker:
environment:
VOICE_PROVIDER: volcengine
VOLC_ACCESS_KEY_ID: ""
VOLC_ACCESS_KEY_ID_FILE: /run/secrets/local-rtc/volc-access-key-id
VOLC_SECRET_ACCESS_KEY: ""
VOLC_SECRET_ACCESS_KEY_FILE: /run/secrets/local-rtc/volc-secret-access-key
VOLC_RTC_REGION: cn-north-1
VOLC_RTC_API_VERSION: 2024-12-01
VOLC_VOICE_CHAT_TEMPLATE_FILE: /run/secrets/local-rtc/voice-chat-template.json
volumes:
- ./deploy/local-rtc/runtime/volc-access-key-id:/run/secrets/local-rtc/volc-access-key-id:ro
- ./deploy/local-rtc/runtime/volc-secret-access-key:/run/secrets/local-rtc/volc-secret-access-key:ro
- ./deploy/local-rtc/runtime/voice-chat-template.json:/run/secrets/local-rtc/voice-chat-template.json:ro
+118
View File
@@ -0,0 +1,118 @@
[CmdletBinding()]
param(
[string]$RtcAppId = "6a8f92ca43726c017d34cce1",
[switch]$Force
)
$ErrorActionPreference = "Stop"
$scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path
$runtimeDirectory = Join-Path $scriptDirectory "runtime"
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
function Read-RequiredText {
param([string]$Prompt, [string]$Default = "")
$suffix = if ($Default) { " [$Default]" } else { "" }
$value = Read-Host "$Prompt$suffix"
if ([string]::IsNullOrWhiteSpace($value)) { $value = $Default }
if ([string]::IsNullOrWhiteSpace($value)) { throw "$Prompt 不能为空" }
return $value.Trim()
}
function Read-RequiredSecret {
param([string]$Prompt, [int]$MinimumLength = 1)
$secureValue = Read-Host $Prompt -AsSecureString
$pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureValue)
try {
$value = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer)
}
finally {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer)
}
if ($value.Length -lt $MinimumLength) { throw "$Prompt 长度不足" }
return $value
}
function Write-SecretFile {
param([string]$Name, [string]$Value)
$target = Join-Path $runtimeDirectory $Name
[IO.File]::WriteAllText($target, $Value, $utf8NoBom)
}
if ($RtcAppId.Length -ne 24) {
throw "RTC AppId 必须正好为 24 个字符"
}
if ((Test-Path -LiteralPath $runtimeDirectory) -and -not $Force) {
throw "本地凭据目录已经存在。确认需要重新配置后使用 -Force。"
}
New-Item -ItemType Directory -Path $runtimeDirectory -Force | Out-Null
Write-Host "`n配置只保存在本机 deploy/local-rtc/runtime,已被 Git 忽略。" -ForegroundColor Cyan
Write-Host "输入密钥时屏幕不会显示字符。`n" -ForegroundColor DarkGray
$rtcAppKey = Read-RequiredSecret "RTC AppKey" 16
$volcAccessKeyId = Read-RequiredText "火山引擎 AccessKey ID"
$volcSecretAccessKey = Read-RequiredSecret "火山引擎 Secret AccessKey"
$doubaoAppId = Read-RequiredText "豆包端到端语音 APP ID"
$doubaoAccessToken = Read-RequiredSecret "豆包端到端语音 Access Token"
$speaker = Read-RequiredText "豆包音色 ID" "zh_female_vv_jupiter_bigtts"
$template = [ordered]@{
schema_version = "voice-chat-template-bundle-v1"
templates = [ordered]@{
s2s_hybrid = [ordered]@{
AgentConfig = [ordered]@{
TargetUserId = @()
UserId = ""
WelcomeMessage = "您好,我是豆包语音助手。今天想聊些什么?"
EnableConversationStateCallback = $false
}
Config = [ordered]@{
InterruptMode = 0
S2SConfig = [ordered]@{
Provider = "volcano"
OutputMode = 0
ProviderParams = [ordered]@{
app = [ordered]@{
appid = $doubaoAppId
token = $doubaoAccessToken
}
dialog = [ordered]@{
extra = [ordered]@{
model = "O"
strict_audit = $true
audit_response = "抱歉,这个话题我暂时无法回答。我们换个轻松的话题吧。"
}
}
asr = [ordered]@{
extra = [ordered]@{
end_smooth_window_ms = 1200
}
}
tts = [ordered]@{
speaker = $speaker
}
}
}
}
}
}
}
Write-SecretFile "rtc-app-key" $rtcAppKey
Write-SecretFile "volc-access-key-id" $volcAccessKeyId
Write-SecretFile "volc-secret-access-key" $volcSecretAccessKey
[IO.File]::WriteAllText(
(Join-Path $runtimeDirectory "voice-chat-template.json"),
($template | ConvertTo-Json -Depth 20),
$utf8NoBom
)
[IO.File]::WriteAllText(
(Join-Path $runtimeDirectory "local-rtc.env"),
"RTC_APP_ID=$RtcAppId`n",
$utf8NoBom
)
Write-Host "`n本地真实 RTC 配置已生成。" -ForegroundColor Green
Write-Host "下一步运行: .\deploy\local-rtc\start.ps1" -ForegroundColor Green
+57
View File
@@ -0,0 +1,57 @@
[CmdletBinding()]
param([switch]$NoBuild)
$ErrorActionPreference = "Stop"
$scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path
$projectRoot = Resolve-Path (Join-Path $scriptDirectory "..\..")
$runtimeDirectory = Join-Path $scriptDirectory "runtime"
$environmentFile = Join-Path $runtimeDirectory "local-rtc.env"
$requiredFiles = @(
"rtc-app-key",
"volc-access-key-id",
"volc-secret-access-key",
"voice-chat-template.json",
"local-rtc.env"
)
foreach ($name in $requiredFiles) {
if (-not (Test-Path -LiteralPath (Join-Path $runtimeDirectory $name) -PathType Leaf)) {
throw "缺少 $name,请先运行 .\deploy\local-rtc\configure.ps1"
}
}
Push-Location $projectRoot
try {
$arguments = @(
"compose",
"-f", "compose.yaml",
"-f", "deploy/local-rtc/compose.override.yaml",
"--env-file", $environmentFile,
"up", "-d"
)
if (-not $NoBuild) { $arguments += "--build" }
$arguments += @(
"postgres", "redis", "minio", "keycloak", "migrate", "seed",
"callbacks", "mock-alert-receiver", "mock-provider", "api", "worker"
)
& docker @arguments
if ($LASTEXITCODE -ne 0) { throw "Docker Compose 启动失败" }
Write-Host "`n等待本地 API 就绪..." -ForegroundColor Cyan
$deadline = [DateTime]::UtcNow.AddMinutes(3)
do {
Start-Sleep -Seconds 2
try {
$health = Invoke-RestMethod -Uri "http://localhost:58080/health/ready" -TimeoutSec 3
if ($health) { break }
}
catch { }
} while ([DateTime]::UtcNow -lt $deadline)
if (-not $health) { throw "本地 API 未在三分钟内就绪,请检查 docker compose logs api worker" }
Write-Host "本地真实 RTC 后端已就绪:http://localhost:58080" -ForegroundColor Green
Write-Host "现在可以启动 Windows 语音终端模拟器。" -ForegroundColor Green
}
finally {
Pop-Location
}