feat: add Windows real RTC voice device simulator
This commit is contained in:
@@ -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 均不进入模拟器安装包,也不会通过渲染页面读取。
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user