607 lines
24 KiB
Markdown
607 lines
24 KiB
Markdown
# sundynix-agentix · 语音交互设计文档
|
||
|
||
> 版本:2026-07-17
|
||
> 定位:让用户通过**语音**与 Agent 双向对话——语音命令让 Agent 干活(做任务/写报告/检索知识库),Agent 语音回答结果。
|
||
> 配套文档:`ARCHITECTURE_DESIGN.md`(架构总览)、`DEPTH_ROADMAP.md`(路线图)
|
||
|
||
---
|
||
|
||
## 1. 目标与范围
|
||
|
||
### 1.1 要做什么
|
||
|
||
用户对着麦克风说「帮我写一份关于 AI 医疗的报告」→ 系统实时识别语音 → 自动触发现有 Agent 编排 → Agent 边想边"说"结果给用户听。
|
||
|
||
**全链路**:
|
||
|
||
```
|
||
🎤 用户说话 → ASR(语音→文字) → 现有编排引擎(Eino) → Token 流 → TTS(文字→语音) → 🔊 用户听到
|
||
```
|
||
|
||
### 1.2 核心原则
|
||
|
||
1. **只加耳朵和嘴巴,不动大脑**——现有 Dispatcher/Eino/工具/RAG/报告 一行不改
|
||
2. **复用现有通信管道**——NATS token 流 + SSE 回流原样利用
|
||
3. **控制面统一管理**——语音配置(ASR/TTS)走现有的 admin 控制面 + NATS 热更新
|
||
4. **可降级**——语音服务不可用时平台文字功能不受影响
|
||
|
||
### 1.3 不做什么(本期)
|
||
|
||
- ❌ 语音唤醒 / 声纹识别
|
||
- ❌ 多人同时语音会议
|
||
- ❌ 视频通话
|
||
- ❌ 端到端语音大模型(绕开编排引擎的方案不考虑)
|
||
|
||
---
|
||
|
||
## 2. 火山引擎 API 选型
|
||
|
||
### 2.1 需要开通的服务
|
||
|
||
| 服务 | 用途 | API 协议 | 文档 |
|
||
|---|---|---|---|
|
||
| **流式语音识别** | 🎤 用户说话 → 文字 | WebSocket 双向流 | [流式语音识别 WebSocket](https://www.volcengine.com/docs/6561/1354869) |
|
||
| **双向流式语音合成 (V3)** | 🔊 文字 → Agent 说话 | WebSocket 双向流 | [双向流式 TTS WebSocket V3](https://www.volcengine.com/docs/6561/1329505) |
|
||
|
||
### 2.2 为什么选这两个
|
||
|
||
| 决策 | 原因 |
|
||
|---|---|
|
||
| **流式 ASR**(非一句话识别) | 用户说长句/多句时能实时出部分结果,体验像实时字幕 |
|
||
| **双向流式 TTS**(非 HTTP 非流式) | 可以边喂文字边拿音频,不用等全文生成完;与 token 流天然配合 |
|
||
| **不用端到端语音大模型 API** | 那个会绕开 Eino 编排引擎,图编排/工具调用/RAG/报告全废 |
|
||
|
||
### 2.3 火山引擎配置参数
|
||
|
||
```
|
||
# .env 新增(与现有 LLM 配置同级)
|
||
VOLC_ASR_APPID= # 火山引擎 appid
|
||
VOLC_ASR_TOKEN= # 火山引擎 access token
|
||
VOLC_ASR_CLUSTER= # ASR 集群(如 volcengine_streaming_common)
|
||
|
||
VOLC_TTS_APPID= # 可与 ASR 同一个 appid
|
||
VOLC_TTS_TOKEN= # 可与 ASR 同一个 token
|
||
VOLC_TTS_CLUSTER= # TTS 集群(如 volcano_tts)
|
||
VOLC_TTS_VOICE_TYPE= # 音色(如 BV700_streaming)
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 架构设计
|
||
|
||
### 3.1 整体架构(在现有 5 层上的增量)
|
||
|
||
```
|
||
┌── 客户端层 ─────────────────────────────────────────────────────────┐
|
||
│ 桌面端 Wails / 浏览器 │
|
||
│ │
|
||
│ ┌────────────┐ ┌──────────────┐ ┌─────────────────┐ │
|
||
│ │ 🎤 录音按钮 │ │ 📝 实时转写显示│ │ 🔊 音频播放队列 │ │
|
||
│ │ MediaRecorder│ │ (边说边显字) │ │ (边收边播放) │ │
|
||
│ └──────┬─────┘ └──────────────┘ └────────▲────────┘ │
|
||
│ │ 音频帧上行 音频帧下行 │ │
|
||
│ └──────────── WebSocket ────────────┘ │
|
||
└────────────────────────┬────────────────────────────────────────────┘
|
||
│
|
||
┌── Gateway(接入层)─────┴────────────────────────────────────────────┐
|
||
│ │
|
||
│ 新端点: GET /api/v1/voice/stream (WebSocket 升级) │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ VoiceSession │ │
|
||
│ │ │ │
|
||
│ │ 上行链路: │ │
|
||
│ │ 客户端音频帧 → volcASRClient(WS) → 转写文字 → 推回客户端显示 │ │
|
||
│ │ ↓ │ │
|
||
│ │ 用户说完(静音检测/手动结束) │ │
|
||
│ │ ↓ │ │
|
||
│ │ 自动组装 DSL → POST /tasks │ │
|
||
│ │ (复用现有任务提交,零改动) │ │
|
||
│ │ │ │
|
||
│ │ 下行链路: │ │
|
||
│ │ 订阅 sundynix.streams.<task_id>(现有 token 流) │ │
|
||
│ │ ↓ │ │
|
||
│ │ SentenceBuffer(攒到句号/逗号/问号/感叹号/换行) │ │
|
||
│ │ ↓ │ │
|
||
│ │ volcTTSClient(WS) → 音频帧 → 推回客户端播放 │ │
|
||
│ │ │ │
|
||
│ └─────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 现有路由/中间件/NATS 发布/SSE 回流 → 全部不动 │
|
||
└──────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
┌─────────────┴─────────────┐
|
||
▼ ▼
|
||
现有后端全套(不改) 火山引擎云端
|
||
NATS → Dispatcher ASR WebSocket 端点
|
||
Eino 编排 TTS WebSocket 端点
|
||
MCP 工具
|
||
RAG/报告/记忆
|
||
```
|
||
|
||
### 3.2 关键设计决策
|
||
|
||
| # | 决策 | 理由 |
|
||
|---|---|---|
|
||
| D1 | **语音逻辑全在 Gateway 内**,不加新微服务 | 音频只是 I/O 转码,不是业务逻辑;遵循 Monolith First |
|
||
| D2 | **Gateway ↔ 火山引擎直连 WebSocket** | 音频帧需毫秒级中继,过 NATS 多一跳反而增延迟 |
|
||
| D3 | **客户端 ↔ Gateway 用单条 WebSocket** | 同一连接承载上行音频 + 下行转写 + 下行 TTS 音频,用消息类型区分 |
|
||
| D4 | **转写完成后复用 POST /tasks 逻辑** | 语音只是输入方式替换,任务提交/编排/回流全走现有管道 |
|
||
| D5 | **TTS 攒句再合成** | 逐 token 喂 TTS 太碎(单字合成不自然);攒到标点再喂,自然度好 |
|
||
| D6 | **语音配置走 admin 控制面** | 与模型配置同管理,支持热更新(改音色/切 provider 不重启) |
|
||
|
||
---
|
||
|
||
## 4. 数据流详解
|
||
|
||
### 4.1 上行:用户说话 → 触发任务
|
||
|
||
```
|
||
时间线 →
|
||
用户: [====说话中====] [停顿/点击结束]
|
||
↓↓↓↓↓↓↓↓↓↓↓↓↓
|
||
客户端: 音频帧(PCM 16kHz/16bit) 每100ms一帧(3.2KB)
|
||
↓ WebSocket binary
|
||
Gateway: 转发 → 火山 ASR WebSocket
|
||
← 部分转写结果(partial) ← 火山 ASR
|
||
← 最终转写结果(final) ← 火山 ASR
|
||
↓
|
||
推回客户端显示(实时字幕)
|
||
↓
|
||
用户停止说话 → 取 final 结果
|
||
↓
|
||
组装简单 DSL:
|
||
{
|
||
"version": "1",
|
||
"nodes": [{"id":"input","kind":"input","config":{"text":"<转写文字>"}},
|
||
{"id":"agent","kind":"agent","config":{"autonomous":true}}],
|
||
"edges": [{"source":"input","target":"agent"}]
|
||
}
|
||
↓
|
||
调用现有 h.SubmitTask() 内部逻辑(经 NATS 发布任务)
|
||
↓
|
||
返回 task_id 给 VoiceSession(用于下行订阅)
|
||
```
|
||
|
||
### 4.2 下行:Agent 回答 → 用户听到
|
||
|
||
```
|
||
时间线 →
|
||
Dispatcher: token: "人" → "工" → "智" → "能" → "在" → "医" → "疗" → "领" → "域" → "," → ...
|
||
↓ sundynix.streams.<task_id>(现有,不改)
|
||
Gateway
|
||
VoiceSession: 订阅 token 流,逐 token 累积到 SentenceBuffer
|
||
↓
|
||
遇到断句符(,。!?\n)→ 截取一句完整文字
|
||
↓
|
||
喂给火山 TTS WebSocket → 收音频帧(PCM/opus)
|
||
↓
|
||
WebSocket binary 推给客户端
|
||
↓
|
||
客户端: AudioContext 播放队列,顺序播放每段音频
|
||
同时文字也在屏幕上显示(双通道:看+听)
|
||
```
|
||
|
||
### 4.3 WebSocket 消息协议
|
||
|
||
客户端 ↔ Gateway 的 WebSocket 用 JSON 控制帧 + Binary 音频帧:
|
||
|
||
```
|
||
// 客户端 → Gateway
|
||
|
||
// 1. 开始录音
|
||
{"type": "asr_start", "session_id": "xxx", "graph": {...}} // 可选携带编排图
|
||
|
||
// 2. 音频帧(binary,PCM 16kHz 16bit mono)
|
||
[binary data]
|
||
|
||
// 3. 停止录音
|
||
{"type": "asr_stop"}
|
||
|
||
// 4. 打断 TTS 播放(用户开始说下一句时)
|
||
{"type": "tts_interrupt"}
|
||
|
||
// ---
|
||
|
||
// Gateway → 客户端
|
||
|
||
// 1. ASR 部分结果(实时字幕)
|
||
{"type": "asr_partial", "text": "人工智能在医"}
|
||
|
||
// 2. ASR 最终结果
|
||
{"type": "asr_final", "text": "人工智能在医疗领域的应用"}
|
||
|
||
// 3. 任务已提交
|
||
{"type": "task_submitted", "task_id": "task_xxx"}
|
||
|
||
// 4. 文字流(同步显示)
|
||
{"type": "text_chunk", "text": "人工智能在医疗领域,"}
|
||
|
||
// 5. TTS 音频帧(binary,带前缀字节区分)
|
||
[0x01][binary audio data] // 0x01 前缀标识这是 TTS 音频
|
||
|
||
// 6. 回答完毕
|
||
{"type": "done"}
|
||
|
||
// 7. 错误
|
||
{"type": "error", "message": "ASR 连接失败"}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 模块设计
|
||
|
||
### 5.1 新增文件清单
|
||
|
||
```
|
||
sundynix-gateway/
|
||
internal/
|
||
voice/ ← 新增包
|
||
session.go ← VoiceSession:管理一次语音对话的生命周期
|
||
asr.go ← 火山 ASR WebSocket 客户端封装
|
||
tts.go ← 火山 TTS WebSocket 客户端封装
|
||
sentence_buffer.go ← Token 流攒句器
|
||
config.go ← 语音配置(appid/token/cluster/voice)
|
||
handler/
|
||
voice.go ← WebSocket 升级 + VoiceSession 入口(新增)
|
||
router/
|
||
router.go ← 加一条路由(改 1 行)
|
||
|
||
sundynix-desktop/frontend/
|
||
src/
|
||
components/
|
||
VoiceButton.tsx ← 🎤 按住说话按钮 + 录音逻辑(新增)
|
||
VoicePlayer.tsx ← 🔊 TTS 音频播放队列(新增)
|
||
lib/
|
||
voice.ts ← WebSocket 连接管理 + 音频采集/播放(新增)
|
||
|
||
sundynix-admin/
|
||
src/pages/
|
||
DatasourcesPage.tsx ← 加语音配置表单(改几行)
|
||
|
||
sundynix-shared/
|
||
contract/
|
||
voice.go ← 语音配置契约 VoiceConfig(新增)
|
||
```
|
||
|
||
### 5.2 Gateway `voice` 包设计
|
||
|
||
#### session.go — VoiceSession
|
||
|
||
```go
|
||
// VoiceSession 管理一次语音对话的完整生命周期:
|
||
// 1. 接收客户端音频帧 → 转发 ASR → 回传转写文字
|
||
// 2. 转写完成 → 组装 DSL → 调用现有任务提交
|
||
// 3. 订阅 token 流 → 攒句 → 喂 TTS → 回传音频帧
|
||
// 4. 支持打断:用户再次说话时中止当前 TTS
|
||
type VoiceSession struct {
|
||
ws *websocket.Conn // 客户端连接
|
||
asr *ASRClient // 火山 ASR
|
||
tts *TTSClient // 火山 TTS
|
||
buf *SentenceBuffer // 攒句器
|
||
bus *nats.Bus // 复用现有 NATS bus
|
||
submit func(text, graph) // 复用现有 SubmitTask 逻辑
|
||
taskID string // 当前任务 ID
|
||
mu sync.Mutex
|
||
}
|
||
|
||
func (s *VoiceSession) Run(ctx context.Context) // 主循环
|
||
func (s *VoiceSession) handleUpstream(ctx) // 上行:音频→ASR→转写
|
||
func (s *VoiceSession) handleDownstream(ctx) // 下行:token→攒句→TTS→音频
|
||
func (s *VoiceSession) interrupt() // 打断 TTS
|
||
```
|
||
|
||
#### asr.go — 火山 ASR 客户端
|
||
|
||
```go
|
||
// ASRClient 封装火山引擎流式语音识别 WebSocket 连接。
|
||
// 协议:wss://openspeech.bytedance.com/api/v3/sauc/bigmodel
|
||
// 上行:音频帧(PCM 16kHz 16bit)
|
||
// 下行:JSON(partial/final 转写结果)
|
||
type ASRClient struct {
|
||
conn *websocket.Conn
|
||
appid string
|
||
token string
|
||
cluster string
|
||
}
|
||
|
||
func NewASRClient(cfg VoiceConfig) (*ASRClient, error)
|
||
func (c *ASRClient) SendAudio(data []byte) error // 发音频帧
|
||
func (c *ASRClient) Recv() (text string, isFinal bool, err error) // 收转写
|
||
func (c *ASRClient) Close() error
|
||
```
|
||
|
||
#### tts.go — 火山 TTS 客户端
|
||
|
||
```go
|
||
// TTSClient 封装火山引擎双向流式语音合成 WebSocket 连接。
|
||
// 协议:wss://openspeech.bytedance.com/api/v3/tts/bidirection
|
||
// 上行:文字(可多次发送,流式喂入)
|
||
// 下行:音频帧(PCM/opus,流式返回)
|
||
type TTSClient struct {
|
||
conn *websocket.Conn
|
||
appid string
|
||
token string
|
||
cluster string
|
||
voiceType string
|
||
}
|
||
|
||
func NewTTSClient(cfg VoiceConfig) (*TTSClient, error)
|
||
func (c *TTSClient) SendText(text string) error // 喂一句文字
|
||
func (c *TTSClient) RecvAudio() (data []byte, done bool, err error) // 收音频
|
||
func (c *TTSClient) Close() error
|
||
```
|
||
|
||
#### sentence_buffer.go — 攒句器
|
||
|
||
```go
|
||
// SentenceBuffer 把逐 token 的文字流攒成完整句子。
|
||
// 遇到断句符(,。!?;\n)时输出一个句子段,喂给 TTS。
|
||
// 设超时兜底:超过 2s 没遇到断句符也强制输出(防长无标点段卡住)。
|
||
type SentenceBuffer struct {
|
||
buf strings.Builder
|
||
out chan string // 攒好的句子
|
||
timeout time.Duration // 无标点强制输出超时(默认 2s)
|
||
}
|
||
|
||
func NewSentenceBuffer() *SentenceBuffer
|
||
func (b *SentenceBuffer) Feed(token string) // 喂一个 token
|
||
func (b *SentenceBuffer) Flush() // 强制输出剩余
|
||
func (b *SentenceBuffer) Sentences() <-chan string // 读取攒好的句子
|
||
```
|
||
|
||
### 5.3 客户端设计
|
||
|
||
#### VoiceButton.tsx — 录音按钮
|
||
|
||
```
|
||
两种交互模式:
|
||
A. 按住说话(Push-to-Talk):按下录音、松开发送 — 适合短命令
|
||
B. 点击切换(Toggle):点一次开始录音、再点一次结束 — 适合长段落
|
||
|
||
录音参数:
|
||
- MediaRecorder / AudioWorklet 采集
|
||
- PCM 16kHz 16bit mono(火山 ASR 要求)
|
||
- 每 100ms 切一帧发送(~3.2KB/帧)
|
||
```
|
||
|
||
#### VoicePlayer.tsx — 音频播放
|
||
|
||
```
|
||
- Web Audio API (AudioContext) 播放队列
|
||
- 收到 TTS 音频帧 → 解码 → 入队 → 顺序播放
|
||
- 支持打断:用户再次说话时清空队列、发 tts_interrupt
|
||
- 播放状态指示:🔊 动画
|
||
```
|
||
|
||
#### voice.ts — WebSocket 管理
|
||
|
||
```
|
||
- 建立/维护到 gateway /api/v1/voice/stream 的 WebSocket
|
||
- 区分 JSON 控制帧和 binary 音频帧
|
||
- 自动重连 + 心跳保活
|
||
- 暴露 hooks:useVoice() → { startRecording, stopRecording, isListening, transcript, isPlaying }
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 与现有系统的集成点
|
||
|
||
### 6.1 改动清单(最小化)
|
||
|
||
| 文件 | 改动 | 行数 |
|
||
|---|---|---|
|
||
| `sundynix-gateway/internal/router/router.go` | 加一条 WebSocket 路由 | **+1 行** |
|
||
| `sundynix-gateway/internal/handler/voice.go` | 新增 handler(调 VoiceSession) | **新文件 ~80 行** |
|
||
| `sundynix-gateway/internal/voice/*.go` | 新增包(ASR/TTS/Session/Buffer/Config) | **新文件 ~500 行** |
|
||
| `sundynix-shared/contract/voice.go` | VoiceConfig 契约 | **新文件 ~30 行** |
|
||
| `sundynix-shared/bus/bus.go` | 加 ServeConfig/SubscribeConfig("voice",...) | **复用现有 config 模式,0 改动** |
|
||
| `sundynix-admin/.../DatasourcesPage.tsx` | 加语音配置表单 | **+~50 行** |
|
||
| `sundynix-desktop/frontend/...` | 新增 3 个文件 | **新文件 ~400 行** |
|
||
| **现有后端(Dispatcher/MCP/Eino/NATS)** | | **0 改动** |
|
||
|
||
### 6.2 路由变更
|
||
|
||
```go
|
||
// router.go 加一条:
|
||
api.GET("/voice/stream", h.VoiceStream) // WebSocket 升级,须在 Auth 后
|
||
```
|
||
|
||
### 6.3 配置管理(复用现有控制面)
|
||
|
||
语音配置与模型配置走**完全相同的管道**:
|
||
|
||
```
|
||
Admin 控制台 → POST /admin/voice → Gateway 写 DB → NATS 广播 "voice" 配置
|
||
↓
|
||
Gateway 自身热更新 VoiceConfig
|
||
(ASR/TTS 客户端用新配置重建)
|
||
```
|
||
|
||
契约:
|
||
```go
|
||
// contract/voice.go
|
||
type VoiceConfig struct {
|
||
ASRAppID string `json:"asr_appid"`
|
||
ASRToken string `json:"asr_token"` // 密文(AES-256-GCM,复用现有 secrets)
|
||
ASRCluster string `json:"asr_cluster"`
|
||
TTSAppID string `json:"tts_appid"`
|
||
TTSToken string `json:"tts_token"` // 密文
|
||
TTSCluster string `json:"tts_cluster"`
|
||
TTSVoiceType string `json:"tts_voice_type"`
|
||
TTSEncoding string `json:"tts_encoding"` // pcm / opus / mp3
|
||
TTSRate int `json:"tts_rate"` // 24000
|
||
Enabled bool `json:"enabled"`
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 实现步骤
|
||
|
||
### Phase 1:ASR(耳朵)— 3 天
|
||
|
||
```
|
||
目标:用户说话 → 屏幕上实时显示转写文字 → 手动提交为任务
|
||
```
|
||
|
||
| # | 任务 | 产出 |
|
||
|---|---|---|
|
||
| 1.1 | 火山引擎开通流式 ASR 服务,拿到 appid/token/cluster | 配置 |
|
||
| 1.2 | `voice/asr.go`:封装火山 ASR WebSocket 客户端 | 代码 |
|
||
| 1.3 | `voice/config.go`:读 env 配置 | 代码 |
|
||
| 1.4 | `voice/session.go`:VoiceSession 上行链路(音频→ASR→转写) | 代码 |
|
||
| 1.5 | `handler/voice.go` + `router.go`:WebSocket 端点 | 代码 |
|
||
| 1.6 | 客户端 `voice.ts` + `VoiceButton.tsx`:录音+WS+显示转写 | 代码 |
|
||
| 1.7 | 端到端验证:说话 → 实时转写 → 手动复制到输入框提交 | 验证 |
|
||
|
||
### Phase 2:语音直接触发任务 — 1 天
|
||
|
||
```
|
||
目标:说完自动提交任务,不用手动操作
|
||
```
|
||
|
||
| # | 任务 | 产出 |
|
||
|---|---|---|
|
||
| 2.1 | `session.go`:转写完成 → 组装默认 DSL → 调 SubmitTask | 代码 |
|
||
| 2.2 | 客户端:说完 → 自动提交 → 切到任务运行视图 | 代码 |
|
||
| 2.3 | 支持携带当前画布编排图(用户在 Studio 页说话时用画布图跑) | 代码 |
|
||
|
||
### Phase 3:TTS(嘴巴)— 3-4 天
|
||
|
||
```
|
||
目标:Agent 回答时语音朗读
|
||
```
|
||
|
||
| # | 任务 | 产出 |
|
||
|---|---|---|
|
||
| 3.1 | 火山引擎开通双向流式 TTS,选音色 | 配置 |
|
||
| 3.2 | `voice/tts.go`:封装火山 TTS WebSocket 客户端 | 代码 |
|
||
| 3.3 | `voice/sentence_buffer.go`:token 流攒句器 | 代码 + 单测 |
|
||
| 3.4 | `session.go`:下行链路(订阅 token 流→攒句→TTS→音频推送) | 代码 |
|
||
| 3.5 | 客户端 `VoicePlayer.tsx`:AudioContext 播放队列 | 代码 |
|
||
| 3.6 | 端到端验证:说话 → 任务执行 → Agent 边想边说 | 验证 |
|
||
|
||
### Phase 4:打断 + 连续对话 — 2 天
|
||
|
||
```
|
||
目标:自然对话体验
|
||
```
|
||
|
||
| # | 任务 | 产出 |
|
||
|---|---|---|
|
||
| 4.1 | 打断:用户再次说话时中止 TTS + 清空播放队列 | 代码 |
|
||
| 4.2 | 连续对话:上一轮完毕后自动重新激活麦克风 | 代码 |
|
||
| 4.3 | 会话历史串联:同一 session_id 下多轮对话共享上下文 | 代码 |
|
||
| 4.4 | 静音检测(VAD):客户端 3s 无声自动结束录音 | 代码 |
|
||
|
||
### Phase 5:控制面 + 上线 — 2 天
|
||
|
||
```
|
||
目标:运维可管理,生产可用
|
||
```
|
||
|
||
| # | 任务 | 产出 |
|
||
|---|---|---|
|
||
| 5.1 | Admin 控制台加语音配置页(ASR/TTS appid/token/音色选择) | 代码 |
|
||
| 5.2 | `contract/voice.go` + token 密文存储(复用 secrets) | 代码 |
|
||
| 5.3 | 语音配置热更新(NATS 广播,复用 config 模式) | 代码 |
|
||
| 5.4 | 健康检查:`/admin/status` 加 ASR/TTS 连通性探测 | 代码 |
|
||
| 5.5 | 语音用量计量(ASR 秒数 + TTS 字数)→ 现有计费管道 | 代码 |
|
||
| 5.6 | `.env.example` 加语音配置项说明 | 文档 |
|
||
|
||
---
|
||
|
||
## 8. 性能与延迟分析
|
||
|
||
### 8.1 端到端延迟拆解
|
||
|
||
```
|
||
用户说完最后一个字 → 听到 Agent 第一个字的时间:
|
||
|
||
ASR 尾部延迟 ~300ms (火山 ASR final 结果延迟)
|
||
+ 任务提交 → NATS ~10ms (现有管道)
|
||
+ Dispatcher 消费 ~5ms (现有管道)
|
||
+ LLM TTFT ~500ms (首 token 延迟,取决于模型)
|
||
+ 攒句(到第一个标点) ~200ms (模型每秒约 30-50 token,逗号很快出现)
|
||
+ TTS 首段合成 ~200ms (火山 TTS 流式首包延迟)
|
||
─────────────────────────────────
|
||
总计 ~1.2s ← 可接受(人类对话轮转间隔约 0.5-2s)
|
||
```
|
||
|
||
### 8.2 后续句子的延迟
|
||
|
||
首句之后,**TTS 与 LLM 推理流水线重叠**——LLM 在生成下一句时,上一句的 TTS 还在播放。用户感知是连续朗读,无等待。
|
||
|
||
### 8.3 音频带宽
|
||
|
||
```
|
||
上行(ASR): PCM 16kHz 16bit mono = 32KB/s ≈ 256kbps → 完全可接受
|
||
下行(TTS):
|
||
- PCM 24kHz 16bit: 48KB/s
|
||
- opus 编码后: ~6-12KB/s ≈ 48-96kbps → 推荐用 opus 省带宽
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 安全考量
|
||
|
||
| 维度 | 措施 |
|
||
|---|---|
|
||
| **ASR/TTS Token** | 复用现有 `secrets.Encrypt/Decrypt` (AES-256-GCM),DB 存密文,NATS 过密文 |
|
||
| **WebSocket 鉴权** | 升级前经过 `middleware.Auth()`,未登录不可连 |
|
||
| **音频不落盘** | 语音帧只在内存中转,不存储不持久化 |
|
||
| **TTS 内容脱敏** | 复用现有 `harness/output.go` 的流式脱敏——脱敏后的文字再喂 TTS |
|
||
| **限流** | 语音连接数按用户限制(默认同时 1 个语音会话) |
|
||
|
||
---
|
||
|
||
## 10. 可观测性
|
||
|
||
| 指标 | 来源 |
|
||
|---|---|
|
||
| ASR 识别延迟 | Gateway OTel span `voice.asr` |
|
||
| TTS 合成延迟 | Gateway OTel span `voice.tts` |
|
||
| 语音会话数 | Prometheus gauge `sundynix_voice_sessions_active` |
|
||
| ASR 用量(秒) | 计入现有用量管道 |
|
||
| TTS 用量(字) | 计入现有用量管道 |
|
||
| 错误率 | span error + Prometheus counter |
|
||
|
||
---
|
||
|
||
## 11. 未来扩展(本期不做)
|
||
|
||
| 方向 | 说明 |
|
||
|---|---|
|
||
| **声纹识别** | 用声纹替代/辅助登录鉴权 |
|
||
| **多语种实时翻译** | ASR → 翻译 → TTS,三段流水线 |
|
||
| **自定义唤醒词** | "Hey Sundynix" 免点击启动 |
|
||
| **音色克隆** | 用户上传自己的声音,Agent 用用户喜欢的声音回答 |
|
||
| **Provider failover** | ASR/TTS 也做主备链(火山→讯飞),复用 LLM failover 模式 |
|
||
|
||
---
|
||
|
||
## 12. 总工期与资源
|
||
|
||
| 阶段 | 内容 | 工期 |
|
||
|---|---|---|
|
||
| Phase 1 | ASR(耳朵) | 3 天 |
|
||
| Phase 2 | 语音触发任务 | 1 天 |
|
||
| Phase 3 | TTS(嘴巴) | 3-4 天 |
|
||
| Phase 4 | 打断 + 连续对话 | 2 天 |
|
||
| Phase 5 | 控制面 + 上线 | 2 天 |
|
||
| **总计** | | **~2 周** |
|
||
|
||
**改动影响**:
|
||
|
||
```diff
|
||
+ 新增文件: ~10 个(Go 5 个 + TS 3 个 + 契约 1 个 + handler 1 个)
|
||
+ 新增代码: ~1,500 行(Go ~600 + TS ~400 + 测试 ~300 + 配置/文档 ~200)
|
||
~ 修改文件: 3 个(router.go +1行, DatasourcesPage.tsx +50行, .env.example +8行)
|
||
不动文件: Dispatcher / MCP-Go / MCP-Py / Eino / NATS bus / shared 核心 = 0 改动
|
||
```
|
||
|
||
---
|
||
|
||
*本文档描述语音交互功能的设计方案;实现时以本文档为准,如有重大变更需更新本文档。*
|