# doubao-voice **Repository Path**: thiswind/doubao-voice ## Basic Information - **Project Name**: doubao-voice - **Description**: No description available - **Primary Language**: JavaScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-12-30 - **Last Updated**: 2025-12-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # doubao-voice 基于火山引擎 OpenSpeech API 的语音合成与识别网页应用,支持: - **TTS(文本转语音)**:实时语音合成和播放 - **ASR(语音识别)**:按住说话实时识别 ## 目录 - [快速开始](#快速开始) - [项目结构](#项目结构) - [功能特性](#功能特性) - [配置说明](#配置说明) - [TTS 使用指南](#tts-使用指南) - [ASR 使用指南](#asr-使用指南) - [架构设计](#架构设计) - [WebSocket 协议详解](#websocket-协议详解) - [关键实现步骤](#关键实现步骤) - [常见问题与解决方案](#常见问题与解决方案) - [快速参考](#快速参考) - [代码关键位置](#代码关键位置) --- ## 快速开始 ### 1. 安装依赖 ```bash npm install ``` ### 2. 配置 复制示例配置文件并编辑: ```bash cp config.json.example config.json ``` 编辑 `config.json`,填入你的 API 配置信息(详见[配置说明](#配置说明))。 ### 3. 启动代理服务器 ```bash npm start ``` 服务器将在 `http://localhost:3000` 启动。 ### 4. 打开网页 - **TTS(语音合成)**:`http://localhost:3000/index.html` - **ASR(语音识别)**:`http://localhost:3000/asr.html` --- ## 项目结构 ``` . ├── index.html # TTS 前端页面 ├── asr.html # ASR 前端页面 ├── tts.js # TTS 功能和 WebSocket 协议处理(复用) ├── asr.js # ASR 功能实现 ├── proxy-server.js # Node.js 代理服务器(WebSocket 转发、认证) ├── package.json # 依赖配置 ├── config.json # 配置文件(运行时加载,包含敏感信息) ├── config.json.example # 配置文件示例 ├── README.md # 本文件(完整文档) ├── .gitignore # Git 忽略规则 └── 大模型流式语音识别API--豆包语音-火山引擎.mhtml # API 文档 ``` --- ## 功能特性 ### TTS(文本转语音) - ✅ 实时语音合成 - ✅ 支持中文文本 - ✅ 自动音频播放 - ✅ 状态提示 - ✅ 错误处理 ### ASR(语音识别) - ✅ 按住说话实时识别 - ✅ 流式识别结果显示 - ✅ 自动去重处理 - ✅ 支持移动端(触摸事件) - ✅ 完整的错误处理 --- ## 配置说明 ### 配置文件格式 项目使用 `config.json` 文件存储配置信息,支持多个模型配置: ```json { "models": { "tts": { "name": "语音合成大模型", "appid": "YOUR_APP_ID", "access_token": "YOUR_ACCESS_TOKEN", "secret_key": "YOUR_SECRET_KEY", "voice_type": "zh_female_sophie_conversation_wvae_bigtts", "instance_id": "YOUR_INSTANCE_ID", "endpoint": "wss://openspeech.bytedance.com/api/v1/tts/ws_binary", "cluster": "volcano_tts" }, "asr": { "name": "流式语音识别大模型", "appid": "YOUR_APP_ID", "access_token": "YOUR_ACCESS_TOKEN", "secret_key": "YOUR_SECRET_KEY", "instance_id": "YOUR_INSTANCE_ID", "endpoint": "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel", "cluster": "volcano_asr", "mode": "streaming" } }, "default_model": "tts" } ``` ### 关键配置项说明 #### TTS 配置 | 配置项 | 说明 | 示例值 | |--------|------|--------| | `endpoint` | WebSocket 端点 | `wss://openspeech.bytedance.com/api/v1/tts/ws_binary` | | `voice_type` | 音色类型 | `zh_female_sophie_conversation_wvae_bigtts` | | `cluster` | 集群标识 | `volcano_tts` | #### ASR 配置 | 配置项 | 说明 | 示例值 | |--------|------|--------| | `endpoint` | WebSocket 端点 | `wss://openspeech.bytedance.com/api/v3/sauc/bigmodel` | | `cluster` | 集群标识 | `volcano_asr` | | `X-Api-Resource-Id` | 资源ID(固定值) | `volc.bigasr.sauc.duration`(小时版)
`volc.bigasr.sauc.concurrent`(并发版) | **注意**: - `config.json` 包含敏感信息,已添加到 `.gitignore`,不会被提交到版本控制 - ASR 的 `X-Api-Resource-Id` 必须使用固定值,不是 `instance_id`! ### 端口配置 默认使用 3000 端口,可在 `proxy-server.js` 中修改: ```javascript const PORT = 3000; // 修改为你需要的端口 ``` --- ## TTS 使用指南 ### 使用方法 1. 在文本框中输入要合成的文字 2. 点击"播放"按钮 3. 等待语音合成完成后自动播放 ### 请求格式 ```javascript const request = { app: { appid: CONFIG.appid, token: '', // 前端不需要token,认证在代理服务器端处理 cluster: CONFIG.cluster || 'volcano_tts' }, user: { uid: generateUUID() }, audio: { voice_type: CONFIG.voice_type, encoding: 'wav' }, request: { reqid: generateUUID(), text: '要合成的文字', operation: 'submit', extra_param: JSON.stringify({ disable_markdown_filter: false }), with_timestamp: '1' } }; ``` ### 响应处理 服务器会返回多条消息: 1. **第一条消息**:空的 `AudioOnlyServer`(payload长度为0)- 可忽略 2. **中间消息**:`FrontEndResultServer` - 前端结果,可选处理 3. **最后一条消息**:`AudioOnlyServer` with `NegativeSeq` - 包含实际的音频数据(WAV格式) --- ## ASR 使用指南 ### 使用方法 1. 按住按钮开始录音 2. 松开按钮停止录音 3. 识别结果会实时显示在文本框中 ### 关键实现步骤 #### 步骤 1: 建立 WebSocket 连接 ```javascript await loadConfig('asr'); const endpoint = `${protocol}//${host}:3000/ws-proxy?model=asr`; this.ws = new WebSocket(endpoint); this.ws.binaryType = 'arraybuffer'; ``` #### 步骤 2: 发送初始请求 ```javascript const request = { app: { appid: this.config.appid, token: '', cluster: this.config.cluster || 'volcano_asr' }, user: { uid: generateUUID() }, audio: { format: 'pcm', codec: 'raw', rate: 16000, bits: 16, channel: 1 }, request: { model_name: 'bigmodel' } }; ``` **关键点**: - 必须包含 `app` 字段 - `audio` 字段指定音频格式:PCM, 16kHz, 16-bit, 单声道 - `request.model_name` 固定为 `'bigmodel'` #### 步骤 3: 实时音频采集 ```javascript // 请求麦克风权限 this.stream = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 16000, channelCount: 1, echoCancellation: true, noiseSuppression: true } }); // 创建 AudioContext this.audioContext = new AudioContext({ sampleRate: 16000 }); // 使用 ScriptProcessorNode 实时获取 PCM 数据 const source = this.audioContext.createMediaStreamSource(this.stream); const processor = this.audioContext.createScriptProcessor(4096, 1, 1); processor.onaudioprocess = (e) => { const inputData = e.inputBuffer.getChannelData(0); const pcmData = this.float32ToPCM16(inputData); // 累积到缓冲区,每 200ms 发送一次 this.audioBuffer.push(pcmData); this.bufferSize += pcmData.length; if (this.bufferSize >= 3200) { // 约 200ms 的音频数据 this.flushAudioBuffer(); } }; ``` #### 步骤 4: 发送音频数据 ```javascript sendAudioData(pcmData) { this.sequence++; // 从 1 开始,因为初始请求被视为序列号 1 const flag = this.isRecording ? MsgTypeFlagBits.PositiveSeq : MsgTypeFlagBits.NegativeSeq; const msg = createMessage(MsgType.AudioOnlyClient, flag); msg.sequence = this.sequence; msg.payload = pcmData; const data = marshalMessage(msg); this.ws.send(data); } ``` **关键点**: - 序列号从 **1** 开始(不是 0) - 第一条音频数据使用序列号 **2** - 使用 `PositiveSeq` 标志表示还有后续数据 - 最后一条使用 `NegativeSeq` 标志和负序列号表示结束 #### 步骤 5: 处理识别结果 ```javascript handleMessage(msg) { switch (msg.type) { case MsgType.FullServerResponse: // ASR 的识别结果在 FullServerResponse 中 if (msg.payload.length > 0) { const text = new TextDecoder().decode(msg.payload); const result = JSON.parse(text); this.updateResult(result); } break; } } ``` ### 音频格式要求 - **格式**:PCM - **采样率**:16000 Hz - **位深**:16-bit - **声道**:单声道(mono) - **字节序**:Little-endian - **发送频率**:每 200ms 发送一次(推荐) ### 识别结果格式 服务器返回的 JSON 格式: ```json { "audio_info": { "duration": 10752 }, "result": { "text": "你好呀,我是一只快乐的小猫猫,喵喵喵。汪汪汪。", "utterances": [ { "text": "你好呀,我是一只快乐的小猫猫,喵喵喵。汪汪汪。", "definite": true, "words": [...] } ] } } ``` ### 流式识别特点 **重要**:流式识别每次返回的是**完整文本**(从开始到当前),不是增量更新! 例如: - 第1次返回:`"你好"` - 第2次返回:`"你好呀"` - 第3次返回:`"你好呀,我是一只"` 因此显示逻辑需要: - 临时结果:直接替换当前显示(流式更新) - 最终确认结果(`definite: true`):追加新行 - 如果文本相同,跳过更新(避免重复渲染) --- ## 架构设计 ### 整体架构 ``` 浏览器 (前端) ↓ WebSocket (ws://localhost:3000/ws-proxy?model=tts|asr) 代理服务器 (Node.js - proxy-server.js) ↓ WebSocket + 认证 Headers 火山引擎 API 服务器 ``` ### 为什么需要代理服务器? 浏览器的 WebSocket API **不支持自定义 headers**,而火山引擎 API 需要在 WebSocket 握手时提供认证信息: **TTS v1 API**: - `Authorization: Bearer;{token}` **ASR v3 API**: - `X-Api-App-Key`: APP ID - `X-Api-Access-Key`: Access Token - `X-Api-Resource-Id`: 资源ID(固定值) - `X-Api-Connect-Id`: 连接追踪ID(UUID) 因此需要一个 Node.js 代理服务器来: 1. 接收浏览器的 WebSocket 连接(无认证) 2. 建立到火山引擎的 WebSocket 连接(带认证 headers) 3. 双向转发消息 ### 代理服务器认证配置 #### TTS v1 API ```javascript headers.Authorization = `Bearer;${model.access_token}`; ``` #### ASR v3 API ```javascript headers['X-Api-Resource-Id'] = 'volc.bigasr.sauc.duration'; // 小时版 headers['X-Api-Access-Key'] = model.access_token; headers['X-Api-App-Key'] = model.appid; headers['X-Api-Connect-Id'] = crypto.randomUUID(); ``` --- ## WebSocket 协议详解 ### 消息类型 ```javascript const MsgType = { Invalid: 0, FullClientRequest: 0b1, // 客户端完整请求 AudioOnlyClient: 0b10, // 客户端音频数据 FullServerResponse: 0b1001, // 服务器完整响应 AudioOnlyServer: 0b1011, // 服务器音频数据 FrontEndResultServer: 0b1100, // 前端结果 Error: 0b1111 // 错误消息 }; ``` ### 标志位 ```javascript const MsgTypeFlagBits = { NoSeq: 0, // 无序列号 PositiveSeq: 0b1, // 正序列号(还有后续数据) LastNoSeq: 0b10, // 最后一条,无序列号 NegativeSeq: 0b11, // 负序列号(表示最后一条) WithEvent: 0b100 // 包含事件 }; ``` ### 消息格式 ``` [Header 4 bytes] Byte 0: Version (4 bits) | HeaderSize (4 bits) Byte 1: MessageType (4 bits) | Flag (4 bits) Byte 2: Serialization (4 bits) | Compression (4 bits) Byte 3: Reserved (0) [Sequence 4 bytes] (可选,如果 flag=PositiveSeq 或 NegativeSeq) Int32, big-endian, signed [PayloadSize 4 bytes] Uint32, big-endian [Payload N bytes] 实际数据(JSON 或 PCM 音频数据) ``` ### 序列号规则 1. **初始请求**(`FullClientRequest`):使用 `NoSeq` 标志,无序列号 2. **第一条音频数据**(ASR):序列号从 **2** 开始(服务器将初始请求视为 1) 3. **后续音频数据**:序列号递增(3, 4, 5...) 4. **最后一条音频数据**:使用 `NegativeSeq` 标志,序列号为负数(如 -10) **关键点**: - 所有多字节整数都使用 **big-endian(大端序)** - 序列号使用 `getInt32`(有符号整数) - Payload大小使用 `getUint32`(无符号整数) - 读取顺序必须严格按照协议:序列号/错误码 → 事件字段 → Payload ### 消息序列化/反序列化 #### 序列化 (marshalMessage) ```javascript function marshalMessage(msg) { const headerSize = 4 * msg.headerSize; const header = new Uint8Array(headerSize); header[0] = (msg.version << 4) | msg.headerSize; header[1] = (msg.type << 4) | msg.flag; header[2] = (msg.serialization << 4) | msg.compression; header[3] = 0; // Reserved // 如果有序列号(PositiveSeq或NegativeSeq) if (msg.flag === MsgTypeFlagBits.PositiveSeq || msg.flag === MsgTypeFlagBits.NegativeSeq) { if (msg.sequence !== undefined) { const seqBuffer = new ArrayBuffer(4); const seqView = new DataView(seqBuffer); seqView.setInt32(0, msg.sequence, false); // big-endian // ... 添加到消息中 } } // Payload大小(4字节,big-endian) const payloadSize = msg.payload.length; const sizeBuffer = new ArrayBuffer(4); const sizeView = new DataView(sizeBuffer); sizeView.setUint32(0, payloadSize, false); // big-endian // 合并所有部分并返回 // ... } ``` #### 反序列化 (unmarshalMessage) **关键点:读取顺序很重要!** ```javascript function unmarshalMessage(data) { // 1. 读取基础头部 const versionAndHeaderSize = data[0]; const typeAndFlag = data[1]; const serializationAndCompression = data[2]; const msg = { version: (versionAndHeaderSize >> 4), headerSize: (versionAndHeaderSize & 0b00001111), type: (typeAndFlag >> 4), flag: (typeAndFlag & 0b00001111), serialization: (serializationAndCompression >> 4), compression: (serializationAndCompression & 0b00001111), payload: new Uint8Array(0) }; // 2. 跳过剩余的头部字节 let offset = 4 * msg.headerSize; // 3. 根据消息类型和标志位决定读取顺序 const readers = getReaders(msg); for (const reader of readers) { offset = reader(msg, data, offset); } return msg; } function getReaders(msg) { const readers = []; // 先读取序列号或错误码 switch (msg.type) { case MsgType.AudioOnlyServer: case MsgType.AudioOnlyClient: case MsgType.FrontEndResultServer: case MsgType.FullClientRequest: case MsgType.FullServerResponse: if (msg.flag === MsgTypeFlagBits.PositiveSeq || msg.flag === MsgTypeFlagBits.NegativeSeq) { readers.push(readSequence); } break; case MsgType.Error: readers.push(readErrorCode); break; } // 然后读取事件相关字段(如果有) if (msg.flag === MsgTypeFlagBits.WithEvent) { readers.push(readEvent, readSessionId, readConnectId); } // 最后读取payload readers.push(readPayload); return readers; } ``` --- ## 关键实现步骤 ### TTS 实现流程 1. **建立 WebSocket 连接**(通过代理服务器) 2. **发送初始请求**(包含文本内容) 3. **接收音频数据**(多条 `AudioOnlyServer` 消息) 4. **合并音频块**(当收到最后一条消息时) 5. **播放音频**(使用 HTML5 Audio API) ### ASR 实现流程 1. **建立 WebSocket 连接**(通过代理服务器) 2. **发送初始请求**(配置音频格式) 3. **实时音频采集**(使用 ScriptProcessorNode) 4. **转换音频格式**(Float32 → PCM16) 5. **发送音频数据**(每 200ms 一次) 6. **接收识别结果**(`FullServerResponse` 消息) 7. **更新显示**(流式更新,避免重复) --- ## 常见问题与解决方案 ### 1. 序列号不匹配错误(ASR) **错误信息**: ``` Error: decode ws request failed: unable to decode V1 protocol message: autoAssignedSequence (2) mismatch sequence in request (1) ``` **原因**:序列号初始化错误 **解决方案**: ```javascript // 错误:从 0 开始 this.sequence = 0; // ❌ // 正确:从 1 开始 this.sequence = 1; // ✅ // 第一条音频数据:sequence++ → 2 ``` ### 2. 缺少 app 字段(ASR) **错误信息**: ``` Error code: 45000000 ``` **原因**:初始请求缺少 `app` 字段 **解决方案**: ```javascript const request = { app: { // ✅ 必须包含 appid: this.config.appid, token: '', cluster: this.config.cluster }, user: { uid: generateUUID() }, audio: { ... }, request: { model_name: 'bigmodel' } }; ``` ### 3. 认证 Headers 错误(ASR) **错误信息**:连接被拒绝或 401 错误 **原因**:`X-Api-Resource-Id` 使用错误的值 **解决方案**: ```javascript // 错误:使用 instance_id headers['X-Api-Resource-Id'] = model.instance_id; // ❌ // 正确:使用固定值 headers['X-Api-Resource-Id'] = 'volc.bigasr.sauc.duration'; // ✅ ``` ### 4. 识别结果重复显示(ASR) **原因**:流式识别返回完整文本,代码在追加而不是替换 **解决方案**: ```javascript // 错误:每次都追加 textarea.value += text; // ❌ // 正确:临时结果替换,最终结果追加 if (isDefinite) { textarea.value += '\n' + text; // 最终结果追加 } else { textarea.value = text; // 临时结果替换 } ``` ### 5. "missing Authorization header" 错误(TTS) **原因**:浏览器 WebSocket 不支持自定义 headers **解决**:使用代理服务器,在代理服务器端添加 Authorization header ### 6. "insufficient data for payload" 错误 **原因**:消息反序列化时读取顺序错误或字节序错误 **解决**: - 确保按照正确的顺序读取:序列号 → 事件字段 → Payload - 确保使用 big-endian 字节序 - 检查 offset 计算是否正确 ### 7. 消息丢失 **原因**:客户端在服务器连接建立前发送消息 **解决**:在代理服务器中使用消息队列缓存消息 ### 8. 音频播放失败(TTS) **原因**: - 空的音频块被加入数组 - 没有正确识别最后一条消息 **解决**: - 只处理 `payload.length > 0` 的消息 - 检查 `sequence < 0` 来判断最后一条消息 ### 9. 端口被占用 如果 3000 端口被占用,可以: ```bash # 查找占用端口的进程 lsof -ti:3000 # 停止进程 kill <进程ID> # 或修改 proxy-server.js 中的端口号 ``` ### 10. 连接失败 - 检查代理服务器是否正在运行 - 检查 API 配置是否正确 - 查看浏览器控制台和终端日志 ### 11. 没有声音(TTS) - 检查浏览器是否允许自动播放音频 - 查看浏览器控制台的错误信息 - 确认音频数据是否成功接收 ### 12. 音频格式不匹配(ASR) **原因**:音频格式不符合要求 **解决方案**: - 确保采样率为 16000 Hz - 确保是单声道 - 确保转换为 PCM 16-bit - 确保字节序为 little-endian --- ## 快速参考 ### 关键代码片段 #### 1. 消息序列化(发送) ```javascript const msg = createMessage(MsgType.FullClientRequest, MsgTypeFlagBits.NoSeq); msg.payload = new TextEncoder().encode(JSON.stringify(request)); const data = marshalMessage(msg); ws.send(data); ``` #### 2. 消息反序列化(接收) ```javascript ws.onmessage = (event) => { const data = new Uint8Array(event.data); const msg = unmarshalMessage(data); // 处理消息... }; ``` #### 3. 判断最后一条音频消息 ```javascript if (msg.type === MsgType.AudioOnlyServer && msg.sequence !== undefined && msg.sequence < 0) { // 这是最后一条消息 } ``` #### 4. 处理音频数据(TTS) ```javascript case MsgType.AudioOnlyServer: // 只处理有实际数据的消息 if (msg.payload.length > 0) { audioChunks.push(msg.payload); } // 检查是否接收完成 if (msg.sequence !== undefined && msg.sequence < 0) { processAudio(); // 合并并播放 } break; ``` #### 5. 代理服务器消息队列 ```javascript const messageQueue = []; clientWs.on('message', (data) => { if (serverWs.readyState === WebSocket.OPEN) { serverWs.send(data); } else { messageQueue.push(data); // 缓存 } }); serverWs.on('open', () => { while (messageQueue.length > 0) { serverWs.send(messageQueue.shift()); } }); ``` ### 重要常量 ```javascript // 消息类型 MsgType.FullClientRequest = 0b1 MsgType.AudioOnlyServer = 0b1011 MsgType.FrontEndResultServer = 0b1100 // 标志位 MsgTypeFlagBits.NoSeq = 0 MsgTypeFlagBits.NegativeSeq = 0b11 // 最后一条消息 // 字节序 // 所有多字节整数使用 big-endian view.getUint32(0, false) // false = big-endian view.getInt32(0, false) // 序列号使用有符号整数 ``` ### 消息格式速查 ``` [Header 4 bytes] Byte 0: Version(4) | HeaderSize(4) Byte 1: Type(4) | Flag(4) Byte 2: Serialization(4) | Compression(4) Byte 3: Reserved [Sequence 4 bytes] (可选,如果flag=PositiveSeq或NegativeSeq) Int32, big-endian, signed [PayloadSize 4 bytes] Uint32, big-endian [Payload N bytes] 实际数据 ``` ### 常见错误码 | 错误 | 原因 | 解决方案 | |------|------|----------| | `missing Authorization header` | 浏览器不支持自定义headers | 使用代理服务器 | | `insufficient data for payload` | 读取顺序错误或字节序错误 | 检查getReaders()顺序,确保使用big-endian | | 消息丢失 | 服务器未连接就发送 | 使用消息队列缓存 | | 无音频 | 空的音频块被处理 | 检查`payload.length > 0` | | 序列号不匹配 | 序列号初始化错误 | ASR 从 1 开始,不是 0 | | 缺少 app 字段 | 请求格式不完整 | 添加 app 字段到初始请求 | --- ## 代码关键位置 ### 前端代码 #### TTS (`tts.js`) | 功能 | 位置 | 说明 | |------|------|------| | 消息协议定义 | 第 47-80 行 | MsgType, MsgTypeFlagBits 等枚举 | | 消息序列化 | 第 92-150 行 | `marshalMessage()` - 对象 → 二进制 | | 消息反序列化 | 第 293-326 行 | `unmarshalMessage()` - 二进制 → 对象 | | WebSocket 处理 | 第 351-408 行 | `synthesize()` - 建立连接并发送请求 | | 消息处理 | 第 456-517 行 | `handleMessage()` - 处理服务器响应 | | 音频播放 | 第 519-573 行 | `processAudio()` - 合并音频块并播放 | #### ASR (`asr.js`) | 功能 | 位置 | 说明 | |------|------|------| | WebSocket 连接 | `connectWebSocket()` | 建立连接 | | 初始请求 | `sendInitialRequest()` | 发送配置请求 | | 音频采集 | `startRecording()` | 使用 ScriptProcessorNode | | 音频转换 | `float32ToPCM16()` | Float32 → PCM16 | | 数据发送 | `sendAudioData()` | 发送音频数据包 | | 结果处理 | `handleMessage()` | 处理服务器响应 | | 结果显示 | `updateResult()` | 更新 UI | ### 代理服务器 (`proxy-server.js`) | 功能 | 位置 | 说明 | |------|------|------| | 配置加载 | 第 14-38 行 | 从 config.json 加载配置 | | TTS 认证 | 第 125 行 | `Authorization: Bearer;{token}` | | ASR 认证 | 第 108-120 行 | 设置 ASR v3 API 认证 headers | | 消息队列 | 第 150-162 行 | 缓存消息直到连接建立 | | 消息转发 | 第 153-174 行 | 双向转发消息 | --- ## 调试技巧 ### 1. 启用详细日志 在前端代码中添加: ```javascript console.log('Received raw data length:', data.length); console.log('Parsed message:', { type: msg.type, flag: msg.flag, sequence: msg.sequence, payloadLength: msg.payload.length }); ``` 在代理服务器中添加: ```javascript console.log('Client -> Server: message received, length:', data.length); console.log('Server -> Client: message received, length:', data.length); ``` ### 2. 检查消息格式 使用十六进制查看消息: ```javascript console.log('First 20 bytes:', Array.from(data.slice(0, 20)) .map(b => '0x' + b.toString(16).padStart(2, '0')) .join(' ') ); ``` ### 3. 验证字节序 确保使用 DataView 时指定 big-endian: ```javascript const view = new DataView(buffer, offset, 4); const value = view.getUint32(0, false); // false = big-endian ``` --- ## 性能优化建议 1. **音频流式播放**:对于长文本,可以实现流式播放,不需要等待所有数据接收完成 2. **连接复用**:可以复用 WebSocket 连接,避免每次请求都建立新连接 3. **错误重试**:实现自动重试机制,处理网络错误 4. **音频缓存**:对于相同文本,可以缓存音频数据 --- ## 安全注意事项 1. **敏感信息保护**: - Access Token 不应暴露在前端代码中 - 建议将代理服务器部署在安全的环境中 - 使用环境变量存储敏感配置 2. **CORS 配置**: - 代理服务器应配置适当的 CORS 策略 - 限制允许的来源域名 3. **请求验证**: - 在代理服务器端验证请求格式 - 限制请求频率,防止滥用 --- ## 测试检查清单 ### TTS 测试 - [ ] WebSocket 连接成功 - [ ] 请求发送成功(无错误) - [ ] 音频数据正常接收 - [ ] 音频播放正常 - [ ] 错误处理正常 ### ASR 测试 - [ ] WebSocket 连接成功 - [ ] 初始请求发送成功(无错误) - [ ] 音频数据正常发送(序列号递增) - [ ] 服务器返回识别结果(`FullServerResponse`) - [ ] 识别结果正确显示(不重复) - [ ] 最终确认结果正确追加 - [ ] 停止录音后连接正常关闭 --- ## 参考资源 - [火山引擎语音合成 API 文档](https://www.volcengine.com/docs/6561/1257584) - [火山引擎 ASR API 文档](https://www.volcengine.com/docs/6561/1354869) - [WebSocket API 规范](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) - [AudioContext API](https://developer.mozilla.org/en-US/docs/Web/API/AudioContext) - [Node.js ws 库文档](https://github.com/websockets/ws) --- ## 版本历史 ### v1.1.0 (2024-11) - 新增 ASR(实时语音识别)功能 - 支持按住说话实时识别 - 流式识别结果显示和去重处理 - 完整的 ASR 实现文档 ### v1.0.0 (2024-11) - 初始版本 - 支持基本的语音合成功能 - 实现 WebSocket 代理服务器 - 完整的错误处理和日志记录 --- ## 许可证 本项目仅供学习和开发使用。 --- **最后更新**:2024年11月 **维护者**:开发团队 **联系方式**:如有问题,请查看项目 Issue 或联系技术支持