Files
ZNJJ-api-server/docs/voice-websocket.md

377 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
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 即如此处理。Demo 的相机步骤通过发送 `input.text`(如 `【拍摄完成】`)完成,不上传图片帧。
## 服务端 → 客户端
### 用户语音转写
| 事件 | 说明 |
|------|------|
| `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`)可打断 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` 证书 |