Add voice ws docs
This commit is contained in:
409
docs/voice-websocket.md
Normal file
409
docs/voice-websocket.md
Normal 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` |
|
||||
| 声道 | 1(mono) | 配置项 `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` 证书 |
|
||||
Reference in New Issue
Block a user