418 lines
10 KiB
Markdown
418 lines
10 KiB
Markdown
# 实时会议 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` 直接打印到页面日志。
|