305 lines
9.0 KiB
Markdown
305 lines
9.0 KiB
Markdown
# 云端无人机语音服务 - 接口规范 (v1.0)
|
||
|
||
本文档定义了香橙派客户端与云端语音服务之间的完整交互协议。
|
||
|
||
---
|
||
|
||
## 1. 连接信息
|
||
|
||
| 项目 | 值 |
|
||
|------|-----|
|
||
| **协议** | WebSocket (ws://) |
|
||
| **地址** | `ws://<服务器IP>:8765/v1/voice/session` |
|
||
| **鉴权** | Bearer Token |
|
||
|
||
### 1.1 仅 TTS(不走 LLM)
|
||
|
||
在**已建立**的 WebSocket 会话上发送 **`tts.synthesize`**(见 §3.3),下行仍为 `tts_audio_chunk`(text 元数据 + binary PCM)与 **`turn.complete`**;与 `turn.text` **互斥排队**,共用同一连接与播放器路径,无需额外 HTTP。
|
||
|
||
### 1.2 机端麦克风 + 云端 Fun-ASR(`pcm_asr_uplink`)
|
||
|
||
`session.start` 中设置 **`transport_profile`: `pcm_asr_uplink`**,使用 **`turn.audio.start` / `turn.audio.chunk` / `turn.audio.end`** 上行 **16 kHz mono pcm_s16le**(chunk 内 **Base64** 裸 PCM)。服务端用**与 Qwen 相同的百炼 API Key** 调用阿里云 **Fun-ASR 实时识别**;识别完成后下行与 `text_uplink` 相同(并可选 **`asr.partial`**)。**完整字段与时序见** [`CLOUD_VOICE_PROTOCOL_pcm_asr_uplink_v1.md`](./CLOUD_VOICE_PROTOCOL_pcm_asr_uplink_v1.md)。
|
||
|
||
**机端「未唤醒不上云 / 节省 Fun-ASR 按量」**:见 [`CLOUD_VOICE_CLIENT_WAKE_GATE_v1.md`](./CLOUD_VOICE_CLIENT_WAKE_GATE_v1.md)。
|
||
|
||
**小爱类多轮会话(问候、滴声、断句提示、5s 超时、飞控/闲聊分支,服务端+机端分工)**:见 [`CLOUD_VOICE_ASSISTANT_SESSION_v1.md`](./CLOUD_VOICE_ASSISTANT_SESSION_v1.md)。
|
||
|
||
---
|
||
|
||
## 2. 完整通信时序
|
||
|
||
```
|
||
客户端 (香橙派) 云端服务端
|
||
| |
|
||
|-------- 1. session.start ------------------->|
|
||
| |
|
||
|<------- 2. session.ready --------------------|
|
||
| |
|
||
|-------- 3. turn.text (STT 结果) ------------>|
|
||
| |
|
||
|<------- 4. dialog_result (闲聊/飞控JSON) ----|
|
||
|<------- 5. tts_audio_chunk (text 帧) --------|
|
||
|<------- 6. tts_audio_chunk (binary 音频帧) --|
|
||
|<------- 7. ... (多个音频块) ... --------------|
|
||
|<------- 8. turn.complete -------------------|
|
||
| |
|
||
|-------- 9. session.end --------------------->|
|
||
```
|
||
|
||
**仅 TTS(无对话)**:在 `session.ready` 之后可发送 `tts.synthesize`(每条自带 `turn_id`);服务端仅下行 `tts_audio_chunk*` → `turn.complete`(**无** `dialog_result` / `llm.text_delta`)。
|
||
|
||
---
|
||
|
||
## 3. 消息格式详情
|
||
|
||
### 3.1 建立会话 (session.start)
|
||
|
||
**客户端 → 服务端**
|
||
|
||
```json
|
||
{
|
||
"type": "session.start",
|
||
"proto_version": "1.0",
|
||
"transport_profile": "text_uplink",
|
||
"session_id": "唯一会话ID-UUID",
|
||
"auth_token": "与服务端 .env 中的 BEARER_TOKEN 一致",
|
||
"client": {
|
||
"device_id": "设备唯一标识符",
|
||
"locale": "zh-CN",
|
||
"capabilities": {
|
||
"playback_sample_rate_hz": 24000,
|
||
"prefer_tts_codec": "pcm_s16le"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.2 服务端就绪 (session.ready)
|
||
|
||
**服务端 → 客户端**
|
||
|
||
```json
|
||
{
|
||
"type": "session.ready",
|
||
"proto_version": "1.0",
|
||
"transport_profile": "text_uplink",
|
||
"session_id": "唯一会话ID-UUID",
|
||
"server_caps": {
|
||
"accepts_audio_uplink": false,
|
||
"llm": true,
|
||
"tts_codecs": ["pcm_s16le"],
|
||
"llm_context_turns": 4
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.3 仅 TTS 上行 (tts.synthesize)
|
||
|
||
在 `session.ready` 之后,不经过 LLM:由服务端直接合成 `text` 并推送 **`tts_audio_chunk*`**,最后 **`turn.complete`**(`metrics.llm_ms` 为 `0`)。**不**下发 `dialog_result`、`llm.text_delta`;**不**写入对话历史。与 **`turn.text` pipeline 互斥**(同会话上一条处理完再发下一条)。
|
||
|
||
**客户端 → 服务端**
|
||
|
||
```json
|
||
{
|
||
"type": "tts.synthesize",
|
||
"proto_version": "1.0",
|
||
"transport_profile": "text_uplink",
|
||
"turn_id": "唯一轮次ID-UUID(与 tts_audio_chunk.turn_id 一致)",
|
||
"text": "要播报的中文"
|
||
}
|
||
```
|
||
|
||
(`text` 长度受服务端 `TTS_MAX_CHARS` 限制,超出截断。)
|
||
|
||
---
|
||
|
||
### 3.4 发送文本 (turn.text)
|
||
|
||
**客户端 → 服务端**
|
||
|
||
```json
|
||
{
|
||
"type": "turn.text",
|
||
"proto_version": "1.0",
|
||
"transport_profile": "text_uplink",
|
||
"turn_id": "唯一轮次ID-UUID",
|
||
"text": "本地 STT 识别的完整中文文本",
|
||
"is_final": true,
|
||
"source": "device_stt"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.5 识别结果 (dialog_result)
|
||
|
||
**服务端 → 客户端**
|
||
|
||
客户端必须根据 `routing` 字段判断处理逻辑。
|
||
|
||
#### 场景 A:闲聊 (chitchat)
|
||
|
||
```json
|
||
{
|
||
"type": "dialog_result",
|
||
"proto_version": "1.0",
|
||
"transport_profile": "text_uplink",
|
||
"turn_id": "唯一轮次ID-UUID",
|
||
"user_input": {
|
||
"text": "用户原始文本",
|
||
"language": "zh",
|
||
"is_final": true,
|
||
"source": "device_stt"
|
||
},
|
||
"routing": "chitchat",
|
||
"flight_intent": null,
|
||
"chat_reply": "你好!我无法获取实时天气信息,建议查看手机天气App。",
|
||
"tts_hint": {
|
||
"speak_summary_or_reply": true,
|
||
"voice_id": "default"
|
||
}
|
||
}
|
||
```
|
||
|
||
**客户端动作:**
|
||
1. 使用 `chat_reply` 字段的内容进行 TTS 播放。
|
||
2. **必须**继续接收后续的 `tts_audio_chunk` 和 `turn.complete`。
|
||
|
||
---
|
||
|
||
#### 场景 B:飞控指令 (flight_intent) ✨ 重点
|
||
|
||
```json
|
||
{
|
||
"type": "dialog_result",
|
||
"proto_version": "1.0",
|
||
"transport_profile": "text_uplink",
|
||
"turn_id": "唯一轮次ID-UUID",
|
||
"user_input": {
|
||
"text": "起飞然后在前方十米悬停",
|
||
"language": "zh",
|
||
"is_final": true,
|
||
"source": "device_stt"
|
||
},
|
||
"routing": "flight_intent",
|
||
"flight_intent": {
|
||
"is_flight_intent": true,
|
||
"version": 1,
|
||
"actions": [
|
||
{"type": "takeoff", "args": {}},
|
||
{"type": "goto", "args": {"frame": "local_ned", "x": 10, "y": 0, "z": null}},
|
||
{"type": "hover", "args": {}}
|
||
],
|
||
"summary": "起飞后前往机头方向约十米并保持高度"
|
||
},
|
||
"chat_reply": null,
|
||
"tts_hint": {
|
||
"speak_summary_or_reply": true,
|
||
"voice_id": "default"
|
||
}
|
||
}
|
||
```
|
||
|
||
> 飞控载荷的严格字段表与 `wait` / `takeoff.relative_altitude_m` / 可选 `trace_id` 等,见 `docs/FLIGHT_INTENT_SCHEMA_v1.md`。
|
||
|
||
**🔊 TTS 播报约定(重要):**
|
||
* **当 `routing` 为 `flight_intent` 时,云端 TTS 合成并下发的语音固定为:**
|
||
> **"识别到飞控指令,正在下发指令"**
|
||
* 客户端收到 `flight_intent` 后,应开始解析 `actions` 数组并准备下发飞控,同时播放该提示音。
|
||
|
||
**客户端动作:**
|
||
1. **解析 `flight_intent.actions`** 数组,按顺序执行飞控逻辑。
|
||
2. **必须**继续接收后续的 `tts_audio_chunk`(听到提示音)和 `turn.complete`,**绝对不能提前断开或退出循环**。
|
||
|
||
---
|
||
|
||
### 3.6 TTS 音频流 (tts_audio_chunk)
|
||
|
||
**协议规则:**
|
||
音频流由成对的消息组成:
|
||
1. **Text 帧 (JSON)**:描述接下来音频块的元数据。
|
||
2. **Binary 帧 (Raw Bytes)**:真正的 PCM 音频数据。
|
||
|
||
**Text 帧 (JSON) 格式:**
|
||
|
||
```json
|
||
{
|
||
"type": "tts_audio_chunk",
|
||
"proto_version": "1.0",
|
||
"transport_profile": "text_uplink",
|
||
"turn_id": "唯一轮次ID-UUID",
|
||
"seq": 0,
|
||
"codec": "pcm_s16le",
|
||
"sample_rate_hz": 24000,
|
||
"is_final": false
|
||
}
|
||
```
|
||
|
||
**Binary 帧 (音频数据):**
|
||
* **格式**:PCM S16LE (16-bit, Little-Endian)
|
||
* **采样率**:24000 Hz
|
||
* **声道**:Mono (单声道)
|
||
|
||
**⚠️ 客户端接收关键逻辑(必须遵守):**
|
||
```python
|
||
msg = await ws.recv()
|
||
|
||
# ✅ 必须判断类型!
|
||
if isinstance(msg, bytes):
|
||
# 这是二进制音频数据,追加到缓冲区
|
||
audio_buffer.append(msg)
|
||
# ❌ 绝对不能对 msg 执行 json.loads(msg)!
|
||
else:
|
||
# 这是 JSON 元数据
|
||
data = json.loads(msg)
|
||
...
|
||
```
|
||
|
||
---
|
||
|
||
### 3.7 轮次完成 (turn.complete)
|
||
|
||
**服务端 → 客户端**
|
||
|
||
收到此消息表示当前轮次(LLM + TTS)**完全结束**。
|
||
|
||
```json
|
||
{
|
||
"type": "turn.complete",
|
||
"proto_version": "1.0",
|
||
"transport_profile": "text_uplink",
|
||
"turn_id": "唯一轮次ID-UUID",
|
||
"metrics": {
|
||
"llm_ms": 1400,
|
||
"tts_first_byte_ms": 3100
|
||
}
|
||
}
|
||
```
|
||
|
||
**客户端动作:**
|
||
1. 收到此消息后,拼接 `audio_buffer` 中的二进制块。
|
||
2. 将完整的 PCM 数据送入声卡播放。
|
||
3. 准备下一轮对话。
|
||
|
||
---
|
||
|
||
## 4. 客户端检查清单 (Checklist)
|
||
|
||
客户端团队在联调前,请确保以下逻辑已正确实现:
|
||
|
||
- [ ] **区分帧类型**:接收循环中是否有 `isinstance(msg, bytes)` 判断?
|
||
- [ ] **禁止提前退出**:无论 `routing` 是闲聊还是飞控,**必须**收到 `turn.complete` 才能结束当前 `run_turn`。
|
||
- [ ] **JSON 解析安全**:确保**只对 Text 帧**调用 `json.loads()`,Binary 帧直接 `append`。
|
||
- [ ] **音频播放**:收到的 PCM 数据是 `int16` 格式,播放前需转为 `float32` (除以 32768.0)。
|
||
- [ ] **飞控提示音**:收到 `routing=flight_intent` 时,期望听到 TTS 语音:"识别到飞控指令,正在下发指令"。
|
||
|
||
---
|
||
|
||
**版本**: v1.0
|
||
**更新日期**: 2024-04-07
|
||
**协议标识**: `text_uplink`
|