Add voice ws docs

This commit is contained in:
Xin Wang
2026-06-01 11:18:41 +08:00
parent 00c1bbdc6b
commit 0ef5de399a

409
docs/voice-websocket.md Normal file
View File

@@ -0,0 +1,409 @@
# Voice WebSocket 使用说明
基于 `src/voice` 产品语音管线与 `static/voice-demo` 浏览器示例整理。
## 概览
| 项目 | 说明 |
|------|------|
| WebSocket 路径 | `/ws-product` |
| 协议标识 | `va.ws.v1`JSON + base64音频上行也支持二进制 PCM |
| 默认音频 | PCM16 小端(`pcm_s16le`、16 kHz、单声道 |
| 会话 ID | 连接 URL 查询参数 `chatId``chat_id`;未传时服务端自动生成 |
| 健康检查 | `GET /voice/health` |
| 浏览器 Demo | 默认挂载于 `/voice-demo`(由 voice 配置 `server.serve_webpage` 控制) |
完整 URL 示例:
```
ws://127.0.0.1:8000/ws-product?chatId=voice_abc123
wss://your-host/ws-product?chatId=voice_abc123
```
## 连接流程
```mermaid
sequenceDiagram
participant Client
participant Server
Client->>Server: WebSocket connect (?chatId=...)
Server-->>Client: 101 Switching Protocols
Client->>Server: session.start (JSON)
Note over Client,Server: 可选:固定开场白 / FastGPT opener / LLM 生成问候
loop 会话中
Client->>Server: input.audio (binary 或 JSON)
Client->>Server: input.text / input.image / response.cancel
Server-->>Client: input.transcript.* / response.text.* / response.audio.*
Server-->>Client: response.state若启用状态标签
end
Client->>Server: session.stop
Server-->>Client: WebSocket close
```
推荐顺序(与 `voice-demo/app.js` 一致):
1. 建立 WebSocket 连接(建议 `binaryType = "arraybuffer"`)。
2. 连接成功后立即发送 `session.start`
3. 开始推送麦克风音频(二进制帧或 `input.audio` JSON
4. 处理服务端 JSON 事件文本、转写、TTS 音频等)。
5. 断开前发送 `session.stop`,再关闭连接。
## 消息信封
除二进制音频外,所有消息均为 UTF-8 JSON 对象。服务端下发事件统一包含:
| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | string | 事件类型 |
| `protocol` | string | 固定为 `va.ws.v1` |
| `seq` | number | 单调递增序号(仅服务端事件) |
## 客户端 → 服务端
### `session.start`
开始会话,必须在发送音频或文本输入之前调用。
```json
{
"type": "session.start",
"protocol": "va.ws.v1",
"chatId": "voice_abc123",
"audio": {
"encoding": "pcm_s16le",
"sample_rate": 16000,
"channels": 1
}
}
```
`chatId` 也可写作 `chat_id`。若省略,服务端使用 URL 查询参数或自动生成 ID。
### `session.stop`
正常结束会话。
```json
{
"type": "session.stop",
"reason": "client_disconnect"
}
```
### `input.audio`JSON 形式)
```json
{
"type": "input.audio",
"audio": "<base64 PCM16>",
"sample_rate": 16000,
"channels": 1
}
```
`audio` 字段也可命名为 `data``sample_rate` / `channels` 可省略,默认与服务端配置一致。
### 二进制音频(推荐)
直接发送 **原始 PCM16 小端** 字节流,无需 JSON 包装。`voice-demo` 通过 AudioWorklet 每 20 ms 发送一帧16 kHz 单声道下约 640 字节/帧)。
服务端同时接受 JSON 与二进制两种上行格式。
### `input.text`
发送文本回合;默认会打断当前 bot 回复(`interrupt: true`)。
```json
{
"type": "input.text",
"text": "你好,我想报案",
"interrupt": true
}
```
注意:文本输入**不会**以 `input.transcript.final` 回显,客户端需自行在 UI 中展示用户消息Demo 即如此处理)。
### `input.image`
上传相机/图片并附带可选说明文本,供多模态 LLM 使用。
```json
{
"type": "input.image",
"image": "<base64>",
"mime_type": "image/jpeg",
"width": 1920,
"height": 1080,
"text": "Answer using this camera image.",
"append_to_context": true
}
```
约束:
- 支持 `image/jpeg``image/png``image/webp`
- 单张最大 8 MB
- `width``height` 必须为正整数
- 支持 `data:image/jpeg;base64,...` 格式(会自动剥离前缀)
### `response.cancel`
取消当前 bot 回复(等价于打断 TTS / LLM 流)。
```json
{
"type": "response.cancel"
}
```
## 服务端 → 客户端
### 用户语音转写
| 事件 | 说明 |
|------|------|
| `input.transcript.interim` | ASR 中间结果(流式识别过程中) |
| `input.transcript.final` | 用户一句话结束后的最终转写 |
```json
{
"type": "input.transcript.final",
"protocol": "va.ws.v1",
"seq": 12,
"text": "发生了交通事故",
"user_id": "product-user",
"timestamp": "2026-06-01T10:00:00.000Z"
}
```
### 助手文本流
文本通常**早于**对应 TTS 音频到达,便于客户端先渲染字幕。
| 事件 | 说明 |
|------|------|
| `response.text.started` | 新一轮助手回复开始 |
| `response.text.delta` | 流式文本片段 |
| `response.text.final` | 本轮文本结束;`interrupted: true` 表示被打断 |
```json
{
"type": "response.text.delta",
"protocol": "va.ws.v1",
"seq": 20,
"text": "您好,"
}
```
```json
{
"type": "response.text.final",
"protocol": "va.ws.v1",
"seq": 45,
"text": "您好,请问发生了什么情况?",
"interrupted": false
}
```
### 助手语音TTS
| 事件 | 说明 |
|------|------|
| `response.audio.started` | Bot 开始说话 |
| `response.audio.delta` | PCM16 音频块base64 |
| `response.audio.stopped` | Bot 说完 |
```json
{
"type": "response.audio.delta",
"protocol": "va.ws.v1",
"seq": 30,
"audio": "<base64 PCM16>",
"bytes": 640,
"sample_rate": 16000,
"channels": 1
}
```
客户端应将各 `delta` 块按序解码并无缝拼接播放Demo 使用 Web Audio `AudioContext` 调度)。
### 助手状态(可选)
当 voice 配置启用 `agent.response_state`LLM 输出开头的 `<state>...</state>` 标签会被剥离,并单独下发:
```json
{
"type": "response.state",
"protocol": "va.ws.v1",
"seq": 18,
"state": "2000"
}
```
Demo 根据状态码展示拍照引导(如 `2000``2015` 等车险场景状态)。
## 音频参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| 采样率 | 16000 Hz | 配置项 `audio.sample_rate_hz` |
| 声道 | 1mono | 配置项 `audio.channels` |
| 帧长 | 20 ms | 配置项 `audio.frame_ms`;每帧 640 字节 |
| 编码 | PCM signed 16-bit LE | 小端有符号 16 位整数 |
## 会话与打断行为
- **chatId**:同一 ID 用于 LLM如 FastGPT多轮上下文连接时可写在 URL 或 `session.start` 中。
- **语音回合**VAD + 静音超时判定用户说完;说完后触发 STT 最终转写与 LLM。
- **打断**:用户说话、`input.text``interrupt: true`)或 `response.cancel` 可打断 bot被打断的助手文本在 `response.text.final` 中带 `interrupted: true`
- **空闲超时**:长时间无活动会断开(`session.inactivity_timeout_sec`,默认 60 秒);可配置空闲提示语。
- **开场白**:由 `agent.greeting_mode` 控制(`fixed` / `fastgpt_opener` / `generated` 等)。
## 浏览器 Demo 参考实现
Demo 位于 `static/voice-demo/`,无构建步骤,核心文件:
| 文件 | 职责 |
|------|------|
| `app.js` | WebSocket 连接、事件处理、聊天 UI、TTS 播放 |
| `pcm-recorder.worklet.js` | 麦克风采集、重采样至 16 kHz、20 ms 二进制帧 |
| `index.html` / `styles.css` | 页面与样式 |
### 启动 Demo
1. 启动 API 服务并加载 voice 配置(环境变量 `VOICE_CONFIG` 指向 JSON默认 `config/voice.json`)。
2. 浏览器打开 `http://127.0.0.1:8000/voice-demo/`(挂载路径见配置 `server.webpage_mount`)。
3. 点击 **Connect****Enable mic** 开始对话。
### Demo 关键实现要点
**连接与握手**
```javascript
const ws = new WebSocket("ws://127.0.0.1:8000/ws-product?chatId=voice_xxx");
ws.binaryType = "arraybuffer";
ws.onopen = () => {
ws.send(JSON.stringify({
type: "session.start",
protocol: "va.ws.v1",
chatId: "voice_xxx",
audio: { encoding: "pcm_s16le", sample_rate: 16000, channels: 1 },
}));
};
```
**发送麦克风(二进制,与 Demo 一致)**
```javascript
// AudioWorklet 每 20ms postMessage { type: "frame", buffer: ArrayBuffer }
recorderNode.port.onmessage = (event) => {
if (event.data?.type === "frame") {
ws.send(event.data.buffer);
}
};
```
**播放 TTS**
```javascript
function decodeBase64ToInt16(b64) {
const binary = atob(b64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return new Int16Array(bytes.buffer);
}
// 收到 response.audio.delta 后,将 Int16 转为 Float32 并调度到 AudioContext
```
**发送文本并打断**
```javascript
ws.send(JSON.stringify({
type: "input.text",
text: "【拍摄完成】",
interrupt: true,
}));
// 客户端应停止本地 TTS 播放队列;服务端会发 response.text.final(interrupted=true)
```
### 跨域静态页
若 Demo 托管在其他端口,需在 voice 配置中设置 `server.cors_origins`,并将 WebSocket URL 指向 API 主机。
> 浏览器 `getUserMedia` 需要安全上下文:`https://` 或 `http://localhost` 可用;其他 HTTP 源需改用 HTTPS + `wss://`。
## 最小客户端示例(伪代码)
```javascript
const ws = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/ws-product?chatId=voice_demo_1`);
ws.binaryType = "arraybuffer";
ws.onopen = () => {
ws.send(JSON.stringify({
type: "session.start",
protocol: "va.ws.v1",
audio: { encoding: "pcm_s16le", sample_rate: 16000, channels: 1 },
}));
};
ws.onmessage = (event) => {
if (typeof event.data !== "string") return;
const msg = JSON.parse(event.data);
switch (msg.type) {
case "input.transcript.final":
console.log("User:", msg.text);
break;
case "response.text.delta":
process.stdout?.write?.(msg.text); // 流式打印助手文本
break;
case "response.audio.delta":
playPcm16(decodeBase64(msg.audio));
break;
case "response.state":
console.log("State:", msg.state);
break;
}
};
function disconnect() {
ws.send(JSON.stringify({ type: "session.stop", reason: "done" }));
ws.close(1000, "done");
}
```
## 健康检查响应示例
```bash
curl http://127.0.0.1:8000/voice/health
```
```json
{
"status": "healthy",
"config": "/path/to/config/voice.json",
"protocols": {
"/ws-product": "va.ws.v1.json_base64"
},
"features": {
"product_text_input": true,
"product_text_interrupt": true
},
"demo": "/voice-demo",
"llm_provider": "fastgpt",
"stt_provider": "xfyun",
"tts_provider": "xfyun"
}
```
## 常见问题
| 现象 | 可能原因 |
|------|----------|
| 连接后立即断开 | 未发送 `session.start`;或超过 inactivity 超时 |
| 无 bot 语音 | 未处理 `response.audio.delta`AudioContext 未在用户手势后 resume |
| 回声/啸叫 | 建议使用耳机Demo 已开启浏览器 AEC但扬声器外放仍可能串音 |
| 文本发送无用户气泡 | 设计如此,需客户端本地展示 `input.text` 内容 |
| 跨域 WebSocket 失败 | 检查 `cors_origins``wss` 证书 |