test/docs/realtime_meeting_websocket.md

418 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 实时会议 WebSocket 调用说明
本文档说明当前项目实时会议识别接口的调用方式、入参与返回格式。
## 1. 接口地址
- 推荐地址:`/ws/v1/asr`
- 显式地址:`/ws/v1/asr/qwen`
说明:
- `/ws/v1/asr/funasr` 已废弃,不再使用。
- 当前返回结构按腾讯实时识别风格组织,核心消息类型为:`voice_id`、`start`、`sentences`、`end`、`error`。
## 2. 交互流程
客户端调用顺序:
1. 建立 WebSocket 连接
2. 发送 `start` 消息
3. 持续发送音频二进制数据
4. 接收服务端返回的 `voice_id`、`start`、`sentences`
5. 发送 `stop`
6. 接收最终 `end`
## 3. start 消息
客户端先发送文本消息:
```json
{
"type": "start",
"payload": {
"format": "pcm",
"sample_rate": 16000,
"session_id": "meeting-abc-123",
"language": null,
"context": "",
"enable_inverse_text_normalization": true,
"silence_duration_ms": 800,
"min_partial_sec": 0.3,
"pre_roll_ms": 240,
"max_sentence_count": 8,
"partial_holdback_chars": 2,
"unfixed_token_num": 3,
"enable_native_partial_stream": true,
"enable_speaker": true,
"match_speaker_registry": true,
"speaker_threshold": 0.6,
"enable_realtime_vad_split": true,
"enable_realtime_longform": false,
"force_stable_segment_sec": 6,
"force_stable_min_chars": 24,
"max_segment_sec": 12
}
}
```
## 4. start 参数说明
### 必填/常用参数
| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `format` | string | `pcm` | 当前建议传 `pcm` |
| `sample_rate` | number | `16000` | 采样率 |
| `session_id` | string | 自动生成/可不传 | 会话标识;断线重连时传同一个值可在保活期内恢复同一会议会话 |
| `language` | string/null | `null` | 识别语言,留空表示自动判断 |
| `context` | string | `""` | 上下文提示,如会议主题、术语 |
| `enable_inverse_text_normalization` | boolean | `true` | 是否启用数字归一化 |
### partial / 切段相关
| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `silence_duration_ms` | number | `800` | 静音多久触发收段 |
| `min_partial_sec` | number | `0.9`(后端默认) | 最短 partial 窗口 |
| `pre_roll_ms` | number | `240` | 句首预读毫秒数 |
| `max_sentence_count` | number | `8` | 一段内最多允许的句子数 |
| `partial_holdback_chars` | number | 后端默认 | partial 尾部保留字数,减少尾字抖动 |
| `unfixed_token_num` | number | `5`(后端默认) | native partial 模式下回滚 token 数 |
| `enable_native_partial_stream` | boolean | `false` | 是否启用底层原生流式 partial |
| `enable_realtime_vad_split` | boolean | `false` | 是否启用实时 VAD 拆段 |
| `enable_realtime_longform` | boolean | `false` | 是否启用长段重转写 |
| `force_stable_segment_sec` | number | 后端默认 | 超时后软提交的时间阈值 |
| `force_stable_min_chars` | number | 后端默认 | 软提交最少字数 |
| `max_segment_sec` | number | 后端默认 | 单段最长秒数 |
### speaker 相关
| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `enable_speaker` | boolean | `true` | 是否开启说话人分离 |
| `match_speaker_registry` | boolean | `false` | 是否匹配已注册声纹库 |
| `speaker_threshold` | number/null | `null` | 本次声纹匹配阈值 |
说明:
- `enable_speaker=true` 时,服务端会先返回文本,再异步补充 speaker 归属。
- `match_speaker_registry=true` 时,若匹配到声纹库,会返回实名 speaker 信息。
## 5. 音频数据发送
`start` 成功后,客户端持续发送音频二进制帧。
当前推荐:
- 单声道 PCM
- 16k 采样率
- 小块持续发送
示例:
```js
ws.send(pcmChunkArrayBuffer);
```
## 6. stop 消息
识别结束后发送:
```json
{
"type": "stop"
}
```
## 7. 服务端返回消息
## 7.1 `voice_id`
连接启动后首先返回:
```json
{
"type": "voice_id",
"voice_id": "41ff1926",
"session_id": "meeting-abc-123"
}
```
## 7.2 `start`
服务端确认开始识别:
```json
{
"type": "start",
"session_id": "meeting-abc-123"
}
```
说明:
- 若客户端在 `start.payload.session_id` 中传入固定值,服务端会优先复用该值。
- 若客户端未传 `session_id`,服务端仍按当前连接生成 `voice_id`,并同步作为 `session_id` 返回。
- 在 `REALTIME_SESSION_RESUME_TTL_SEC` 保活时间内,客户端断线后使用相同 `session_id` 再次发送 `start`,服务端会恢复原会话上下文。
## 7.3 `sentences`
识别过程中会持续返回 `sentences`。
重要说明:
- `sentence_type=0` / `slice_type=1` 表示实时 partial,仅用于实时展示。
- `sentence_type=1` / `slice_type=2` 表示该段最终定稿,建议作为最终业务入库依据。
- 若开启 `enable_native_partial_stream=true`,partial 刷新会更快,但中间文本可能更活。
- 若希望少存数据,建议不要存 partial,只对 `sentence_type=1` 做落库或 upsert。
### partial 返回
```json
{
"type": "sentences",
"code": 0,
"voice_id": "41ff1926",
"final": 0,
"result": {
"slice_type": 1,
"index": 3,
"voice_text_str": "今天这个会议主要讨论预算。"
},
"sentences": [
{
"sentence_id": 3,
"sentence_type": 0,
"speaker_id": -1,
"start_time": 12000,
"end_time": 15600,
"sentence": "今天这个会议主要讨论预算。"
}
]
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `slice_type=1` | partial |
| `sentence_type=0` | partial 句子 |
| `speaker_id=-1` | 当前还未确认 speaker |
### 定稿返回
```json
{
"type": "sentences",
"code": 0,
"voice_id": "41ff1926",
"final": 0,
"result": {
"slice_type": 2,
"index": 3,
"voice_text_str": "今天这个会议主要讨论预算。"
},
"sentences": [
{
"sentence_id": 3,
"sentence_type": 1,
"speaker_id": -1,
"start_time": 12000,
"end_time": 15880,
"sentence": "今天这个会议主要讨论预算。",
"speaker_name": "",
"user_id": null
}
]
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `slice_type=2` | 该段文本已定稿 |
| `sentence_type=1` | final 句子 |
| `speaker_name` | 项目扩展字段 |
| `user_id` | 项目扩展字段 |
### speaker 回写返回
如果 speaker 后续识别完成,服务端会再次返回相同 `sentence_id` 的 `sentences` 消息,只更新 speaker 归属:
```json
{
"type": "sentences",
"code": 0,
"voice_id": "41ff1926",
"final": 0,
"result": {
"slice_type": 2,
"index": 3,
"voice_text_str": "今天这个会议主要讨论预算。"
},
"sentences": [
{
"sentence_id": 3,
"sentence_type": 1,
"speaker_id": 19,
"start_time": 12000,
"end_time": 15880,
"sentence": "今天这个会议主要讨论预算。",
"speaker_name": "Alan Paine",
"user_id": "3"
}
]
}
```
说明:
- speaker 是异步补归属,不保证和文本首个 final 同时返回。
- 前端应使用 `sentence_id` 做幂等更新,而不是简单追加。
## 7.3.1 partial 与 final 的使用建议
推荐使用方式:
- 前端实时显示:使用 `sentence_type=0`
- 最终文本入库:使用 `sentence_type=1`
- 说话人归属更新:继续按相同 `sentence_id` 更新已有 final 记录
推荐入库字段:
- `sentence_id`
- `start_time`
- `end_time`
- `sentence`
- `speaker_id`
- `speaker_name`
- `user_id`
推荐入库策略:
1. 收到 `sentences` 后,仅处理 `sentence_type=1`
2. 使用 `sentence_id` 作为同一连接内的幂等更新键
3. 若后续相同 `sentence_id` 再次返回,通常表示 speaker 归属补写,直接更新原记录
4. 收到 `end` 后,可将本次会话的 final 结果视为最终完成结果
## 7.4 `end`
客户端发送 `stop` 后,服务端返回最终结束消息:
```json
{
"type": "end",
"code": 0,
"message": "",
"voice_id": "41ff1926",
"final": 1,
"result": {
"slice_type": 2,
"index": 3,
"voice_text_str": "完整识别文本"
},
"sentences": [
{
"sentence_id": 0,
"sentence_type": 1,
"speaker_id": 19,
"start_time": 0,
"end_time": 3200,
"sentence": "第一句",
"speaker_name": "Alan Paine",
"user_id": "3"
}
]
}
```
## 7.5 `error`
异常时返回:
```json
{
"type": "error",
"code": -1,
"message": "错误信息",
"voice_id": "41ff1926"
}
```
## 8. 腾讯风格字段兼容说明
当前实时会议返回格式按腾讯实时 speaker demo 风格组织,核心字段保持一致:
- `type`
- `code`
- `voice_id`
- `final`
- `result.slice_type`
- `result.index`
- `result.voice_text_str`
- `sentences[].sentence_id`
- `sentences[].sentence_type`
- `sentences[].speaker_id`
- `sentences[].start_time`
- `sentences[].end_time`
- `sentences[].sentence`
在此基础上,项目额外补充:
- `sentences[].speaker_name`
- `sentences[].user_id`
## 9. JavaScript 调用示例
```js
const ws = new WebSocket("ws://127.0.0.1:8000/ws/v1/asr");
ws.onopen = () => {
ws.send(JSON.stringify({
type: "start",
payload: {
format: "pcm",
sample_rate: 16000,
language: null,
context: "预算评审会",
enable_inverse_text_normalization: true,
silence_duration_ms: 800,
min_partial_sec: 0.3,
pre_roll_ms: 240,
max_sentence_count: 8,
partial_holdback_chars: 2,
unfixed_token_num: 3,
enable_native_partial_stream: true,
enable_speaker: true,
match_speaker_registry: true,
speaker_threshold: 0.6,
enable_realtime_vad_split: true,
enable_realtime_longform: false,
force_stable_segment_sec: 6,
force_stable_min_chars: 24,
max_segment_sec: 12
}
}));
};
ws.onmessage = (event) => {
const payload = JSON.parse(event.data);
console.log("ws message:", payload);
};
function sendPcmChunk(arrayBuffer) {
ws.send(arrayBuffer);
}
function stopRecognition() {
ws.send(JSON.stringify({ type: "stop" }));
}
```
## 10. 对接注意事项
- 前端要按 `sentence_id` 更新句子,不要把 speaker 回写当成新句子追加。
- `speaker_id=-1` 代表 speaker 暂未确认,不代表识别失败。
- 默认启用 `enable_native_partial_stream`;partial 刷新更快,但中间文本可能会修订。
- 最终展示建议以 `sentence_type=1` 的句子为准。
- 若需要排查前端是否真的传了某个参数,建议把 `start payload` 直接打印到页面日志。