772 lines
19 KiB
Markdown
772 lines
19 KiB
Markdown
# 实时语音对话 WebSocket API 文档
|
||
|
||
> Version: 2.0.0 (Flux)
|
||
> Last Updated: 2026-01-21
|
||
> Author: Backend Team
|
||
|
||
---
|
||
|
||
## 概述
|
||
|
||
本文档描述实时语音对话 WebSocket API,用于 iOS 客户端与后端进行实时语音交互。
|
||
|
||
**v2.0 更新**: 升级为 Deepgram Flux 模型,支持智能轮次检测和 EagerEndOfTurn 提前响应。
|
||
|
||
### 核心特性
|
||
- **智能轮次检测**: Flux 模型语义理解,自动判断用户说完(非简单静默检测)
|
||
- **EagerEndOfTurn**: 提前启动 LLM 响应,进一步降低延迟
|
||
- **实时语音识别**: 边说边识别,实时显示转写文本
|
||
- **流式响应**: AI 响应边生成边返回,无需等待完整响应
|
||
- **流式音频**: TTS 音频边合成边播放,极低延迟
|
||
- **Barge-in 支持**: 用户可以打断 AI 说话
|
||
|
||
### 性能指标
|
||
| 指标 | 目标值 | 说明 |
|
||
|------|--------|------|
|
||
| 端点检测延迟 | ~260ms | Flux 智能检测 |
|
||
| TTFA (首音频延迟) | < 300ms | EagerEndOfTurn 优化 |
|
||
| 端到端延迟 | < 1.5秒 | 完整对话周期 |
|
||
| 实时转写延迟 | < 100ms | 中间结果 |
|
||
|
||
---
|
||
|
||
## 连接信息
|
||
|
||
### WebSocket 端点
|
||
|
||
```
|
||
生产环境: wss://api.yourdomain.com/api/ws/chat?token={sa_token}
|
||
开发环境: ws://localhost:7529/api/ws/chat?token={sa_token}
|
||
```
|
||
|
||
### 认证方式
|
||
|
||
通过 URL Query 参数传递 Sa-Token:
|
||
|
||
```
|
||
ws://host:port/api/ws/chat?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
|
||
```
|
||
|
||
| 参数 | 类型 | 必填 | 描述 |
|
||
|------|------|------|------|
|
||
| token | String | ✅ | Sa-Token 登录令牌,通过 Apple Sign-In 获取 |
|
||
|
||
### 认证失败
|
||
|
||
如果 token 无效或过期,WebSocket 连接将被拒绝(HTTP 403)。
|
||
|
||
---
|
||
|
||
## 消息格式
|
||
|
||
### 通用规则
|
||
|
||
1. **文本消息**: JSON 格式,用于控制指令和状态通知
|
||
2. **二进制消息**: 原始字节,用于音频数据传输
|
||
3. **编码**: UTF-8
|
||
|
||
---
|
||
|
||
## 客户端 → 服务端消息
|
||
|
||
### 1. 开始会话 (session_start)
|
||
|
||
**发送时机**: 建立 WebSocket 连接后,准备开始录音前
|
||
|
||
```json
|
||
{
|
||
"type": "session_start",
|
||
"config": {
|
||
"language": "en",
|
||
"voice_id": "a5zfmqTslZJBP0jutmVY"
|
||
}
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 描述 |
|
||
|------|------|------|------|
|
||
| type | String | ✅ | 固定值 `session_start` |
|
||
| config | Object | ❌ | 会话配置(可选) |
|
||
| config.language | String | ❌ | 语音识别语言,默认 `en` |
|
||
| config.voice_id | String | ❌ | TTS 声音 ID,默认使用服务端配置 |
|
||
|
||
**响应**: 服务端返回 `session_started` 消息
|
||
|
||
---
|
||
|
||
### 2. 音频数据 (Binary)
|
||
|
||
**发送时机**: 用户正在录音时,持续发送音频数据
|
||
|
||
**格式**: Binary WebSocket Frame,直接发送原始音频字节
|
||
|
||
**音频规格要求**:
|
||
|
||
| 参数 | 值 | 说明 |
|
||
|------|------|------|
|
||
| 编码格式 | PCM (Linear16) | 未压缩的脉冲编码调制 |
|
||
| 采样率 | 16000 Hz | 16kHz |
|
||
| 位深度 | 16-bit | 有符号整数 |
|
||
| 声道数 | 1 (Mono) | 单声道 |
|
||
| 字节序 | Little-Endian | 小端序 |
|
||
|
||
**iOS 代码示例**:
|
||
|
||
```swift
|
||
// AVAudioEngine 配置
|
||
let format = AVAudioFormat(
|
||
commonFormat: .pcmFormatInt16,
|
||
sampleRate: 16000,
|
||
channels: 1,
|
||
interleaved: true
|
||
)!
|
||
|
||
// 发送音频数据
|
||
audioEngine.inputNode.installTap(
|
||
onBus: 0,
|
||
bufferSize: 1024,
|
||
format: format
|
||
) { buffer, time in
|
||
let audioData = buffer.int16ChannelData![0]
|
||
let byteCount = Int(buffer.frameLength) * 2 // 16-bit = 2 bytes
|
||
let data = Data(bytes: audioData, count: byteCount)
|
||
|
||
webSocket.write(data: data)
|
||
}
|
||
```
|
||
|
||
**发送频率**: 建议每 20-100ms 发送一次,每次 320-1600 字节
|
||
|
||
---
|
||
|
||
### 3. 结束录音 (audio_end)
|
||
|
||
**发送时机**: 用户停止录音(松开录音按钮)
|
||
|
||
```json
|
||
{
|
||
"type": "audio_end"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 描述 |
|
||
|------|------|------|------|
|
||
| type | String | ✅ | 固定值 `audio_end` |
|
||
|
||
**说明**: 发送此消息后,服务端将完成语音识别并开始生成 AI 响应
|
||
|
||
---
|
||
|
||
### 4. 取消会话 (cancel)
|
||
|
||
**发送时机**: 用户主动取消对话(如点击取消按钮)
|
||
|
||
```json
|
||
{
|
||
"type": "cancel"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 描述 |
|
||
|------|------|------|------|
|
||
| type | String | ✅ | 固定值 `cancel` |
|
||
|
||
**说明**: 服务端将停止所有处理,不再返回任何消息
|
||
|
||
---
|
||
|
||
## 服务端 → 客户端消息
|
||
|
||
### 1. 会话已启动 (session_started)
|
||
|
||
**接收时机**: 发送 `session_start` 后
|
||
|
||
```json
|
||
{
|
||
"type": "session_started",
|
||
"session_id": "abc123-def456-ghi789"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 描述 |
|
||
|------|------|------|
|
||
| type | String | 固定值 `session_started` |
|
||
| session_id | String | 服务端分配的会话 ID |
|
||
|
||
**客户端处理**: 收到此消息后,可以开始发送音频数据
|
||
|
||
---
|
||
|
||
### 2. 轮次开始 (turn_start) 🆕
|
||
|
||
**接收时机**: 用户开始说话时(Flux 检测到语音活动)
|
||
|
||
```json
|
||
{
|
||
"type": "turn_start",
|
||
"turn_index": 0
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 描述 |
|
||
|------|------|------|
|
||
| type | String | 固定值 `turn_start` |
|
||
| turn_index | Integer | 当前轮次索引(从 0 开始) |
|
||
|
||
**客户端处理**:
|
||
- 可显示"正在听..."状态
|
||
- 准备接收转写结果
|
||
|
||
---
|
||
|
||
### 3. 中间转写结果 (transcript_interim)
|
||
|
||
**接收时机**: 用户说话过程中,实时返回
|
||
|
||
```json
|
||
{
|
||
"type": "transcript_interim",
|
||
"text": "Hello how are",
|
||
"is_final": false
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 描述 |
|
||
|------|------|------|
|
||
| type | String | 固定值 `transcript_interim` |
|
||
| text | String | 当前识别到的文本(可能会变化) |
|
||
| is_final | Boolean | 固定为 `false` |
|
||
|
||
**客户端处理**:
|
||
- 实时更新 UI 显示转写文本
|
||
- 此文本可能会被后续消息覆盖
|
||
- 可用于显示"正在识别..."效果
|
||
|
||
---
|
||
|
||
### 3. 最终转写结果 (transcript_final)
|
||
|
||
**接收时机**: 一句话识别完成时
|
||
|
||
```json
|
||
{
|
||
"type": "transcript_final",
|
||
"text": "Hello, how are you?"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 描述 |
|
||
|------|------|------|
|
||
| type | String | 固定值 `transcript_final` |
|
||
| text | String | 最终确定的转写文本 |
|
||
|
||
**客户端处理**:
|
||
- 用此文本替换之前的中间结果
|
||
- 此文本不会再变化
|
||
|
||
---
|
||
|
||
### 6. 提前端点检测 (eager_eot) 🆕
|
||
|
||
**接收时机**: Flux 检测到用户可能说完时(置信度达到阈值)
|
||
|
||
```json
|
||
{
|
||
"type": "eager_eot",
|
||
"transcript": "Hello, how are you",
|
||
"confidence": 0.65
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 描述 |
|
||
|------|------|------|
|
||
| type | String | 固定值 `eager_eot` |
|
||
| transcript | String | 当前转写文本 |
|
||
| confidence | Double | 端点置信度 (0.0-1.0) |
|
||
|
||
**客户端处理**:
|
||
- 这是一个**预测性事件**,表示用户可能说完了
|
||
- 服务端已开始提前准备 LLM 响应
|
||
- 可显示"准备响应..."状态
|
||
- **注意**: 用户可能继续说话,此时会收到 `turn_resumed`
|
||
|
||
---
|
||
|
||
### 7. 轮次恢复 (turn_resumed) 🆕
|
||
|
||
**接收时机**: 收到 `eager_eot` 后,用户继续说话
|
||
|
||
```json
|
||
{
|
||
"type": "turn_resumed"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 描述 |
|
||
|------|------|------|
|
||
| type | String | 固定值 `turn_resumed` |
|
||
|
||
**客户端处理**:
|
||
- 用户继续说话,之前的 `eager_eot` 是误判
|
||
- 服务端已取消正在准备的草稿响应
|
||
- 恢复"正在听..."状态
|
||
- 继续接收 `transcript_interim` 更新
|
||
|
||
---
|
||
|
||
### 8. LLM 开始生成 (llm_start)
|
||
|
||
**接收时机**: 语音识别完成,AI 开始生成响应
|
||
|
||
```json
|
||
{
|
||
"type": "llm_start"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 描述 |
|
||
|------|------|------|
|
||
| type | String | 固定值 `llm_start` |
|
||
|
||
**客户端处理**:
|
||
- 可显示"AI 正在思考..."状态
|
||
- 准备接收 AI 响应文本和音频
|
||
|
||
---
|
||
|
||
### 5. LLM Token (llm_token)
|
||
|
||
**接收时机**: AI 生成过程中,逐 token 返回
|
||
|
||
```json
|
||
{
|
||
"type": "llm_token",
|
||
"token": "Hi"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 描述 |
|
||
|------|------|------|
|
||
| type | String | 固定值 `llm_token` |
|
||
| token | String | AI 输出的单个 token(词或字符片段) |
|
||
|
||
**客户端处理**:
|
||
- 可选择实现打字机效果
|
||
- 逐个 token 追加显示 AI 响应文本
|
||
- 如不需要打字效果,可忽略此消息
|
||
|
||
---
|
||
|
||
### 6. 音频数据 (Binary)
|
||
|
||
**接收时机**: TTS 合成过程中,流式返回音频
|
||
|
||
**格式**: Binary WebSocket Frame,MP3 音频块
|
||
|
||
**音频规格**:
|
||
|
||
| 参数 | 值 |
|
||
|------|------|
|
||
| 格式 | MP3 |
|
||
| 采样率 | 44100 Hz |
|
||
| 比特率 | 64 kbps |
|
||
| 声道 | 单声道 |
|
||
|
||
**客户端处理**:
|
||
|
||
```swift
|
||
// 使用 AVAudioEngine 或 AudioQueue 播放流式音频
|
||
webSocket.onEvent = { event in
|
||
switch event {
|
||
case .binary(let data):
|
||
// 方案1: 追加到缓冲区,使用 AVAudioPlayerNode
|
||
audioBuffer.append(data)
|
||
playBufferedAudio()
|
||
|
||
// 方案2: 使用 AVAudioEngine + AVAudioCompressedBuffer
|
||
// 方案3: 累积后使用 AVAudioPlayer
|
||
}
|
||
}
|
||
```
|
||
|
||
**重要提示**:
|
||
- 音频是分块返回的,需要正确拼接或流式播放
|
||
- 每个二进制消息是 MP3 数据的一部分
|
||
- 收到 `complete` 消息后,音频传输完成
|
||
|
||
---
|
||
|
||
### 7. 处理完成 (complete)
|
||
|
||
**接收时机**: AI 响应生成完成,所有音频已发送
|
||
|
||
```json
|
||
{
|
||
"type": "complete",
|
||
"transcript": "Hello, how are you?",
|
||
"ai_response": "Hi! I'm doing great, thanks for asking!"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 描述 |
|
||
|------|------|------|
|
||
| type | String | 固定值 `complete` |
|
||
| transcript | String | 完整的用户语音转写文本 |
|
||
| ai_response | String | 完整的 AI 响应文本 |
|
||
|
||
**客户端处理**:
|
||
- 更新 UI 显示完整对话
|
||
- 可开始下一轮对话
|
||
- 建议保存对话历史
|
||
|
||
---
|
||
|
||
### 8. 错误 (error)
|
||
|
||
**接收时机**: 处理过程中发生错误
|
||
|
||
```json
|
||
{
|
||
"type": "error",
|
||
"code": "DEEPGRAM_ERROR",
|
||
"message": "Speech recognition failed"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 描述 |
|
||
|------|------|------|
|
||
| type | String | 固定值 `error` |
|
||
| code | String | 错误代码 |
|
||
| message | String | 错误描述 |
|
||
|
||
**错误代码列表**:
|
||
|
||
| 错误代码 | 描述 | 建议处理 |
|
||
|----------|------|----------|
|
||
| PARSE_ERROR | 消息解析失败 | 检查消息格式 |
|
||
| DEEPGRAM_ERROR | 语音识别服务错误 | 重试或提示用户 |
|
||
| DEEPGRAM_INIT_ERROR | 语音识别初始化失败 | 重新开始会话 |
|
||
| LLM_ERROR | AI 生成错误 | 重试或提示用户 |
|
||
| PIPELINE_ERROR | 处理流程错误 | 重新开始会话 |
|
||
| EMPTY_TRANSCRIPT | 未检测到语音 | 提示用户重新说话 |
|
||
|
||
**客户端处理**:
|
||
- 显示友好的错误提示
|
||
- 根据错误类型决定是否重试
|
||
|
||
---
|
||
|
||
## 完整交互流程
|
||
|
||
### 时序图
|
||
|
||
```
|
||
iOS Client Server
|
||
| |
|
||
|------ WebSocket Connect --------->|
|
||
| ?token=xxx |
|
||
| |
|
||
|<-------- Connected ---------------|
|
||
| |
|
||
|------ session_start ------------->|
|
||
| |
|
||
|<----- session_started ------------|
|
||
| {session_id: "abc"} |
|
||
| |
|
||
|======= 用户开始说话 ===============|
|
||
| |
|
||
|------ Binary (audio) ------------>|
|
||
|------ Binary (audio) ------------>|
|
||
|<----- transcript_interim ---------|
|
||
| {text: "Hello"} |
|
||
|------ Binary (audio) ------------>|
|
||
|<----- transcript_interim ---------|
|
||
| {text: "Hello how"} |
|
||
|------ Binary (audio) ------------>|
|
||
|<----- transcript_final -----------|
|
||
| {text: "Hello, how are you?"}|
|
||
| |
|
||
|======= 用户停止说话 ===============|
|
||
| |
|
||
|------ audio_end ----------------->|
|
||
| |
|
||
|<----- llm_start ------------------|
|
||
| |
|
||
|<----- llm_token ------------------|
|
||
| {token: "Hi"} |
|
||
|<----- llm_token ------------------|
|
||
| {token: "!"} |
|
||
|<----- Binary (mp3) ---------------|
|
||
|<----- Binary (mp3) ---------------|
|
||
|<----- llm_token ------------------|
|
||
| {token: " I'm"} |
|
||
|<----- Binary (mp3) ---------------|
|
||
| ... |
|
||
|<----- complete -------------------|
|
||
| {transcript, ai_response} |
|
||
| |
|
||
|======= 可以开始下一轮 =============|
|
||
| |
|
||
```
|
||
|
||
---
|
||
|
||
## iOS 代码示例
|
||
|
||
### 完整 Swift 实现
|
||
|
||
```swift
|
||
import Foundation
|
||
import Starscream // WebSocket 库
|
||
|
||
class VoiceChatManager: WebSocketDelegate {
|
||
|
||
private var socket: WebSocket?
|
||
private var audioBuffer = Data()
|
||
|
||
// MARK: - 回调
|
||
var onSessionStarted: ((String) -> Void)?
|
||
var onTranscriptInterim: ((String) -> Void)?
|
||
var onTranscriptFinal: ((String) -> Void)?
|
||
var onLLMStart: (() -> Void)?
|
||
var onLLMToken: ((String) -> Void)?
|
||
var onAudioChunk: ((Data) -> Void)?
|
||
var onComplete: ((String, String) -> Void)?
|
||
var onError: ((String, String) -> Void)?
|
||
|
||
// MARK: - 连接
|
||
func connect(token: String) {
|
||
let urlString = "wss://api.yourdomain.com/api/ws/chat?token=\(token)"
|
||
guard let url = URL(string: urlString) else { return }
|
||
|
||
var request = URLRequest(url: url)
|
||
request.timeoutInterval = 30
|
||
|
||
socket = WebSocket(request: request)
|
||
socket?.delegate = self
|
||
socket?.connect()
|
||
}
|
||
|
||
func disconnect() {
|
||
socket?.disconnect()
|
||
socket = nil
|
||
}
|
||
|
||
// MARK: - 发送消息
|
||
func startSession(language: String = "en", voiceId: String? = nil) {
|
||
var config: [String: Any] = ["language": language]
|
||
if let voiceId = voiceId {
|
||
config["voice_id"] = voiceId
|
||
}
|
||
|
||
let message: [String: Any] = [
|
||
"type": "session_start",
|
||
"config": config
|
||
]
|
||
|
||
sendJSON(message)
|
||
}
|
||
|
||
func sendAudio(_ data: Data) {
|
||
socket?.write(data: data)
|
||
}
|
||
|
||
func endAudio() {
|
||
sendJSON(["type": "audio_end"])
|
||
}
|
||
|
||
func cancel() {
|
||
sendJSON(["type": "cancel"])
|
||
}
|
||
|
||
private func sendJSON(_ dict: [String: Any]) {
|
||
guard let data = try? JSONSerialization.data(withJSONObject: dict),
|
||
let string = String(data: data, encoding: .utf8) else { return }
|
||
socket?.write(string: string)
|
||
}
|
||
|
||
// MARK: - WebSocketDelegate
|
||
func didReceive(event: WebSocketEvent, client: WebSocketClient) {
|
||
switch event {
|
||
case .connected(_):
|
||
print("WebSocket connected")
|
||
|
||
case .disconnected(let reason, let code):
|
||
print("WebSocket disconnected: \(reason) (\(code))")
|
||
|
||
case .text(let text):
|
||
handleTextMessage(text)
|
||
|
||
case .binary(let data):
|
||
// 收到 MP3 音频数据
|
||
onAudioChunk?(data)
|
||
|
||
case .error(let error):
|
||
print("WebSocket error: \(error?.localizedDescription ?? "unknown")")
|
||
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
|
||
private func handleTextMessage(_ text: String) {
|
||
guard let data = text.data(using: .utf8),
|
||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||
let type = json["type"] as? String else { return }
|
||
|
||
switch type {
|
||
case "session_started":
|
||
if let sessionId = json["session_id"] as? String {
|
||
onSessionStarted?(sessionId)
|
||
}
|
||
|
||
case "transcript_interim":
|
||
if let text = json["text"] as? String {
|
||
onTranscriptInterim?(text)
|
||
}
|
||
|
||
case "transcript_final":
|
||
if let text = json["text"] as? String {
|
||
onTranscriptFinal?(text)
|
||
}
|
||
|
||
case "llm_start":
|
||
onLLMStart?()
|
||
|
||
case "llm_token":
|
||
if let token = json["token"] as? String {
|
||
onLLMToken?(token)
|
||
}
|
||
|
||
case "complete":
|
||
if let transcript = json["transcript"] as? String,
|
||
let aiResponse = json["ai_response"] as? String {
|
||
onComplete?(transcript, aiResponse)
|
||
}
|
||
|
||
case "error":
|
||
if let code = json["code"] as? String,
|
||
let message = json["message"] as? String {
|
||
onError?(code, message)
|
||
}
|
||
|
||
default:
|
||
print("Unknown message type: \(type)")
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 使用示例
|
||
|
||
```swift
|
||
class VoiceChatViewController: UIViewController {
|
||
|
||
let chatManager = VoiceChatManager()
|
||
let audioRecorder = AudioRecorder() // 自定义录音类
|
||
let audioPlayer = StreamingAudioPlayer() // 自定义流式播放类
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
setupCallbacks()
|
||
}
|
||
|
||
func setupCallbacks() {
|
||
chatManager.onSessionStarted = { [weak self] sessionId in
|
||
print("Session started: \(sessionId)")
|
||
// 开始录音
|
||
self?.audioRecorder.start { audioData in
|
||
self?.chatManager.sendAudio(audioData)
|
||
}
|
||
}
|
||
|
||
chatManager.onTranscriptInterim = { [weak self] text in
|
||
self?.transcriptLabel.text = text + "..."
|
||
}
|
||
|
||
chatManager.onTranscriptFinal = { [weak self] text in
|
||
self?.transcriptLabel.text = text
|
||
}
|
||
|
||
chatManager.onLLMStart = { [weak self] in
|
||
self?.statusLabel.text = "AI is thinking..."
|
||
}
|
||
|
||
chatManager.onLLMToken = { [weak self] token in
|
||
self?.aiResponseLabel.text = (self?.aiResponseLabel.text ?? "") + token
|
||
}
|
||
|
||
chatManager.onAudioChunk = { [weak self] data in
|
||
self?.audioPlayer.appendData(data)
|
||
}
|
||
|
||
chatManager.onComplete = { [weak self] transcript, aiResponse in
|
||
self?.statusLabel.text = "Complete"
|
||
self?.addToHistory(user: transcript, ai: aiResponse)
|
||
}
|
||
|
||
chatManager.onError = { [weak self] code, message in
|
||
self?.showError(message)
|
||
}
|
||
}
|
||
|
||
@IBAction func startTapped(_ sender: UIButton) {
|
||
// 连接并开始会话
|
||
chatManager.connect(token: AuthManager.shared.saToken)
|
||
chatManager.onSessionStarted = { [weak self] _ in
|
||
self?.chatManager.startSession()
|
||
}
|
||
}
|
||
|
||
@IBAction func stopTapped(_ sender: UIButton) {
|
||
audioRecorder.stop()
|
||
chatManager.endAudio()
|
||
}
|
||
|
||
@IBAction func cancelTapped(_ sender: UIButton) {
|
||
audioRecorder.stop()
|
||
audioPlayer.stop()
|
||
chatManager.cancel()
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 注意事项
|
||
|
||
### 1. 音频录制
|
||
- 必须使用 PCM 16-bit, 16kHz, Mono 格式
|
||
- 建议每 20-100ms 发送一次音频数据
|
||
- 录音权限需要在 Info.plist 中声明
|
||
|
||
### 2. 音频播放
|
||
- 返回的是 MP3 格式音频块
|
||
- 需要实现流式播放或缓冲播放
|
||
- 建议使用 AVAudioEngine 实现低延迟播放
|
||
|
||
### 3. 网络处理
|
||
- 实现自动重连机制
|
||
- 处理网络切换场景
|
||
- 设置合理的超时时间
|
||
|
||
### 4. 用户体验
|
||
- 显示实时转写文本
|
||
- 显示 AI 响应状态
|
||
- 提供取消按钮
|
||
- 处理录音权限被拒绝的情况
|
||
|
||
### 5. 调试建议
|
||
- 使用 `wss://` 确保生产环境安全
|
||
- 本地开发可使用 `ws://`
|
||
- 检查 Sa-Token 是否过期
|
||
|
||
---
|
||
|
||
## 版本历史
|
||
|
||
| 版本 | 日期 | 变更 |
|
||
|------|------|------|
|
||
| 1.0.0 | 2026-01-21 | 初始版本 |
|
||
|