diff --git a/xuniYou/NO_VALID_AUDIO_ERROR问题修复.md b/xuniYou/NO_VALID_AUDIO_ERROR问题修复.md new file mode 100644 index 0000000..ba98882 --- /dev/null +++ b/xuniYou/NO_VALID_AUDIO_ERROR问题修复.md @@ -0,0 +1,250 @@ +# NO_VALID_AUDIO_ERROR 问题修复 + +## 🎯 问题描述 + +服务器日志显示: +``` +2026-02-28 18:24:53.660 - voice_call - INFO - ASR connection opened +2026-02-28 18:25:16.706 - voice_call - ERROR - ASR error: NO_VALID_AUDIO_ERROR +``` + +阿里云 ASR 报错:`NO_VALID_AUDIO_ERROR` - 音频数据无效 + +## 🔍 根本原因 + +### 问题代码 + +```javascript +fs.readFile({ + filePath: res.tempFilePath, + encoding: 'binary', // ❌ 错误!这会返回字符串,不是 ArrayBuffer + success: (fileRes) => { + this.sendAudioInChunks(fileRes.data) // fileRes.data 是字符串! + } +}) +``` + +### 为什么会出错? + +1. **`encoding: 'binary'` 返回的是字符串** + - uni-app 的 `readFile` 指定 encoding 后返回字符串 + - 不是 ArrayBuffer + +2. **字符串的 `slice()` 方法返回的还是字符串** + - `audioData.slice(offset, end)` 返回字符串片段 + - 不是二进制数据 + +3. **WebSocket 发送字符串时会被当作文本消息** + - 服务器收到的不是二进制音频数据 + - 而是文本字符串 + - ASR 无法识别,报错 `NO_VALID_AUDIO_ERROR` + +## ✅ 修复方案 + +### 修复后的代码 + +```javascript +fs.readFile({ + filePath: res.tempFilePath, + // ✅ 不指定 encoding,返回 ArrayBuffer + success: (fileRes) => { + // 验证数据类型 + if (!(fileRes.data instanceof ArrayBuffer)) { + console.error('❌ 数据不是 ArrayBuffer') + return + } + + this.sendAudioInChunks(fileRes.data) // fileRes.data 是 ArrayBuffer ✅ + } +}) +``` + +### sendAudioInChunks 也增加了验证 + +```javascript +async sendAudioInChunks(audioData) { + // 确保 audioData 是 ArrayBuffer + if (!(audioData instanceof ArrayBuffer)) { + console.error('❌ audioData 不是 ArrayBuffer') + return + } + + const totalSize = audioData.byteLength // 使用 byteLength + + // ArrayBuffer.slice() 返回新的 ArrayBuffer + const chunk = audioData.slice(offset, end) // ✅ 正确的二进制切片 + + // WebSocket 发送 ArrayBuffer + this.socketTask.send({ + data: chunk // ✅ 发送二进制数据 + }) +} +``` + +## 📊 数据类型对比 + +### 错误的方式(encoding: 'binary') + +```javascript +typeof fileRes.data // "string" +fileRes.data instanceof ArrayBuffer // false +fileRes.data.length // 字符串长度(可能不等于字节数) +fileRes.data.slice(0, 10) // 返回字符串片段 +``` + +### 正确的方式(不指定 encoding) + +```javascript +typeof fileRes.data // "object" +fileRes.data instanceof ArrayBuffer // true +fileRes.data.byteLength // 字节数 +fileRes.data.slice(0, 10) // 返回 ArrayBuffer 片段 +``` + +## 🔧 如何测试 + +### 1. 重新编译客户端 + +在 HBuilderX 中重新运行项目到手机/模拟器 + +### 2. 测试步骤 + +1. 打开 App,进入语音通话页面 +2. 按住"按住说话"按钮 +3. 说话 3-5 秒 +4. 松开按钮 +5. 观察日志 + +### 3. 预期日志 + +#### 客户端日志 + +``` +✅ 文件读取成功 +📊 数据类型: object +📊 是否为 ArrayBuffer: true +📊 数据大小: 160000 bytes +📦 开始分片发送(官方推荐参数) +📊 总大小: 160000 bytes +📊 预计录音时长: 5.00 秒 +📤 发送第 1 片,大小: 3200 bytes +✅ 第 1 片发送成功 +... +``` + +#### 服务器日志 + +``` +✅ 应该看到: +ASR connection opened +ASR event end=False sentence=... +ASR event end=True sentence=[识别的文字] +Handle sentence: [识别的文字] + +❌ 不应该再看到: +ASR error: NO_VALID_AUDIO_ERROR +``` + +## 📚 技术要点 + +### uni-app readFile 的 encoding 参数 + +| encoding 值 | 返回类型 | 用途 | +|------------|---------|------| +| 不指定 | ArrayBuffer | 二进制文件(音频、图片、视频) | +| 'utf8' | String | 文本文件 | +| 'base64' | String | Base64 编码 | +| 'binary' | String | ❌ 不要用于音频!返回字符串 | + +### WebSocket send() 方法 + +```javascript +// 发送文本 +websocket.send({ data: "hello" }) // 文本消息 + +// 发送二进制 +websocket.send({ data: arrayBuffer }) // 二进制消息 +``` + +服务器端会根据数据类型自动判断: +- 字符串 → `msg["text"]` +- ArrayBuffer → `msg["bytes"]` + +## 🎓 经验总结 + +### 关键教训 + +1. **不要对二进制文件使用 encoding 参数** + - 音频、图片、视频等二进制文件 + - 不指定 encoding,让它返回 ArrayBuffer + +2. **验证数据类型** + - 使用 `instanceof ArrayBuffer` 验证 + - 使用 `byteLength` 而不是 `length` + +3. **理解 WebSocket 的数据类型** + - 字符串和二进制数据的处理方式不同 + - 服务器端会根据类型分别处理 + +### 最佳实践 + +```javascript +// ✅ 读取二进制文件的正确方式 +fs.readFile({ + filePath: path, + // 不指定 encoding + success: (res) => { + if (res.data instanceof ArrayBuffer) { + // 处理二进制数据 + } + } +}) + +// ✅ 读取文本文件的正确方式 +fs.readFile({ + filePath: path, + encoding: 'utf8', + success: (res) => { + if (typeof res.data === 'string') { + // 处理文本数据 + } + } +}) +``` + +## 🎉 预期结果 + +修复后,应该能够: + +1. ✅ 正确读取 PCM 音频文件为 ArrayBuffer +2. ✅ 正确切片 ArrayBuffer +3. ✅ 正确发送二进制数据到服务器 +4. ✅ 服务器 ASR 正确识别音频 +5. ✅ 不再出现 `NO_VALID_AUDIO_ERROR` 错误 +6. ✅ 完整的对话流程:ASR → LLM → TTS + +## 📞 如果还有问题 + +如果修复后还是出现 `NO_VALID_AUDIO_ERROR`,可能的原因: + +1. **音频格式不对** + - 确认录音格式为 PCM + - 确认采样率为 16000Hz + - 确认单声道 + +2. **音频太短** + - 至少录音 3 秒 + - 查看日志中的 "预计录音时长" + +3. **音频质量差** + - 在安静环境测试 + - 清晰发音 + - 避免背景噪音 + +--- + +**修复时间**: 2026-02-28 +**问题**: NO_VALID_AUDIO_ERROR +**原因**: 使用 `encoding: 'binary'` 导致发送字符串而不是二进制数据 +**解决**: 不指定 encoding,让 readFile 返回 ArrayBuffer +**状态**: ✅ 已修复,待测试 diff --git a/xuniYou/pages/chat/phone.vue b/xuniYou/pages/chat/phone.vue index a15d6b0..eaceb16 100644 --- a/xuniYou/pages/chat/phone.vue +++ b/xuniYou/pages/chat/phone.vue @@ -431,10 +431,30 @@ // 监听录音停止 - 作为备用方案 recorderManager.onStop((res) => { console.log('⏹️ 录音已停止') + console.log('📋 完整的 res 对象:', JSON.stringify(res)) console.log('📁 文件路径:', res.tempFilePath) console.log('⏱️ 录音时长:', res.duration, 'ms') console.log('📦 文件大小:', res.fileSize, 'bytes') + // 检查录音是否有效 + if (!res.tempFilePath) { + console.error('❌ 没有录音文件路径!') + uni.showToast({ + title: '录音失败:没有生成文件', + icon: 'none' + }) + return + } + + if (!res.duration || res.duration < 500) { + console.error('❌ 录音时长太短:', res.duration, 'ms') + uni.showToast({ + title: '录音太短,请至少说 2 秒', + icon: 'none' + }) + return + } + // 检查 WebSocket 状态 if (!this.socketTask) { console.error('❌ socketTask 不存在') @@ -461,10 +481,11 @@ const fs = uni.getFileSystemManager() fs.readFile({ filePath: res.tempFilePath, - encoding: 'binary', // 明确指定二进制编码 + // ⚠️ 不指定 encoding,让它返回 ArrayBuffer success: (fileRes) => { console.log('✅ 文件读取成功') console.log('📊 数据类型:', typeof fileRes.data) + console.log('📊 是否为 ArrayBuffer:', fileRes.data instanceof ArrayBuffer) console.log('📊 数据大小:', fileRes.data.byteLength || fileRes.data.length, 'bytes') // 再次检查 WebSocket 状态 @@ -473,8 +494,19 @@ return } + // 确保数据是 ArrayBuffer + let audioData = fileRes.data + if (!(audioData instanceof ArrayBuffer)) { + console.error('❌ 数据不是 ArrayBuffer,类型:', typeof audioData) + uni.showToast({ + title: '音频数据格式错误', + icon: 'none' + }) + return + } + // 分片发送音频数据 - this.sendAudioInChunks(fileRes.data) + this.sendAudioInChunks(audioData) }, fail: (err) => { console.error('❌ 文件读取失败:', err) @@ -557,7 +589,18 @@ // PCM 16kHz 单声道:16000 * 2 * 0.1 = 3200 bytes/100ms const chunkSize = 3200 // 3.2KB per chunk(官方推荐) const chunkDelay = 100 // 100ms(官方推荐) - const totalSize = audioData.byteLength || audioData.length + + // 确保 audioData 是 ArrayBuffer + if (!(audioData instanceof ArrayBuffer)) { + console.error('❌ audioData 不是 ArrayBuffer,类型:', typeof audioData) + uni.showToast({ + title: '音频数据格式错误', + icon: 'none' + }) + return + } + + const totalSize = audioData.byteLength let offset = 0 let chunkCount = 0 @@ -566,6 +609,7 @@ console.log('📊 每片大小:', chunkSize, 'bytes') console.log('📊 发送间隔:', chunkDelay, 'ms') console.log('📊 预计发送时间:', Math.ceil(totalSize / chunkSize) * chunkDelay, 'ms') + console.log('📊 预计录音时长:', (totalSize / 32000).toFixed(2), '秒') // 显示加载提示 uni.showLoading({ diff --git a/xuniYou/录音失败问题诊断.md b/xuniYou/录音失败问题诊断.md new file mode 100644 index 0000000..2bda1fd --- /dev/null +++ b/xuniYou/录音失败问题诊断.md @@ -0,0 +1,248 @@ +# 录音失败问题诊断 + +## 🔍 当前问题 + +从日志看到: +``` +18:31:12.587 ⭕ 录音已停止,undefined,ms +18:31:12.588 📁 文件大小:undefined,bytes +``` + +**录音文件大小和时长都是 `undefined`!** 这说明录音没有正确完成。 + +## 🎯 可能的原因 + +### 1. 录音时间太短 +- 用户按住按钮的时间不够长 +- 可能只按了不到 1 秒就松开了 + +### 2. 录音权限问题 +- App 没有麦克风权限 +- 用户拒绝了权限请求 + +### 3. 录音器初始化失败 +- `recorderManager` 没有正确初始化 +- 录音参数不支持 + +### 4. 平台兼容性问题 +- 某些 Android 设备不支持 PCM 格式 +- 需要降级到 MP3 或 AAC 格式 + +## 🔧 解决方案 + +### 方案1: 检查录音权限(最可能) + +在开始录音前,先检查并请求权限: + +```javascript +// 在 startRecording 方法开始处添加 +async startRecording() { + console.log('=== startRecording 被调用 ===') + + // 检查录音权限 + try { + const result = await uni.authorize({ + scope: 'scope.record' + }) + console.log('✅ 录音权限已授予') + } catch (err) { + console.error('❌ 录音权限被拒绝:', err) + uni.showModal({ + title: '需要麦克风权限', + content: '请在设置中开启麦克风权限', + success: (res) => { + if (res.confirm) { + uni.openSetting() + } + } + }) + return + } + + // 继续原有逻辑... +} +``` + +### 方案2: 降级到 MP3 格式 + +如果 PCM 格式不支持,可以尝试 MP3: + +```javascript +const recorderOptions = { + duration: 600000, + sampleRate: 16000, + numberOfChannels: 1, + encodeBitRate: 48000, + format: 'mp3', // 改为 MP3 + audioSource: 'auto' +} +``` + +**但是**:服务器期望 PCM 格式,如果用 MP3,需要修改服务器代码进行转换。 + +### 方案3: 增加录音时长提示 + +在 UI 上提示用户至少说 3 秒: + +```vue + + {{ isTalking ? '松开结束(至少3秒)' : '按住说话' }} + +``` + +### 方案4: 添加录音时长计时器 + +```javascript +data() { + return { + recordStartTime: 0, + recordDuration: 0 + } +}, + +startTalking(e) { + // ... + this.recordStartTime = Date.now() + + // 开始计时 + this.recordTimer = setInterval(() => { + this.recordDuration = Date.now() - this.recordStartTime + console.log('录音中...', (this.recordDuration / 1000).toFixed(1), '秒') + }, 100) +}, + +stopTalking(e) { + // ... + if (this.recordTimer) { + clearInterval(this.recordTimer) + } + + const duration = Date.now() - this.recordStartTime + console.log('录音时长:', (duration / 1000).toFixed(1), '秒') + + if (duration < 2000) { + uni.showToast({ + title: '录音太短,请至少说 2 秒', + icon: 'none' + }) + return + } +} +``` + +## 📱 立即测试步骤 + +### 1. 检查权限 + +打开 App 后,检查是否弹出麦克风权限请求: +- 如果弹出 → 点击"允许" +- 如果没弹出 → 可能已经拒绝过,需要去设置中手动开启 + +### 2. 测试录音 + +1. 进入语音通话页面 +2. **按住"按住说话"按钮至少 3 秒** +3. 松开按钮 +4. 观察日志 + +### 3. 预期日志 + +``` +✅ 录音权限已授予 +✅ recorderManager.start 已调用 +✅ 录音已开始 +录音中... 1.0 秒 +录音中... 2.0 秒 +录音中... 3.0 秒 +⏹️ 录音已停止 +📋 完整的 res 对象: {"tempFilePath":"...","duration":3000,"fileSize":96000} +📁 文件路径: _doc/uniapp_temp_xxx/recorder/xxx.pcm +⏱️ 录音时长: 3000 ms ✅ 不再是 undefined +📦 文件大小: 96000 bytes ✅ 不再是 undefined +``` + +## 🐛 如果还是 undefined + +### 检查1: 查看完整的 res 对象 + +``` +📋 完整的 res 对象: {...} +``` + +如果这个对象是空的或者没有 `tempFilePath`,说明录音确实失败了。 + +### 检查2: 查看是否有录音错误 + +``` +❌ 录音错误: {...} +``` + +如果有这个日志,说明录音过程中出错了。 + +### 检查3: 尝试 MP3 格式 + +修改录音参数: +```javascript +format: 'mp3' // 从 'pcm' 改为 'mp3' +``` + +重新测试,看是否能生成文件。 + +## 🎓 经验总结 + +### 常见问题 + +1. **权限问题** + - 最常见的原因 + - 用户拒绝了麦克风权限 + - 需要引导用户去设置中开启 + +2. **录音时间太短** + - 用户按住时间不够 + - 需要 UI 提示和时长检查 + +3. **格式兼容性** + - 某些设备不支持 PCM + - 可能需要降级到 MP3 + +4. **录音器未初始化** + - `recorderManager` 为 null + - 需要在 onLoad 中正确初始化 + +### 最佳实践 + +1. **权限检查** + - 在录音前检查权限 + - 提供友好的权限引导 + +2. **时长限制** + - 最少 2-3 秒 + - UI 上提示用户 + +3. **错误处理** + - 捕获所有可能的错误 + - 给用户明确的提示 + +4. **格式选择** + - 优先使用 PCM(服务器期望) + - 如果不支持,降级到 MP3 + +## 📞 下一步 + +1. **重新编译客户端**(已添加更详细的日志) +2. **测试时按住至少 3 秒** +3. **查看完整的 res 对象** +4. **根据日志判断问题** + +如果 `res` 对象中有 `tempFilePath` 和 `duration`,说明录音成功。 +如果还是 `undefined`,请提供完整的日志,包括: +- 录音开始的日志 +- 录音错误的日志(如果有) +- 录音停止的完整 res 对象 + +--- + +**诊断时间**: 2026-02-28 +**问题**: 录音文件大小和时长为 undefined +**可能原因**: 权限问题 / 录音时间太短 / 格式不支持 +**解决方案**: 检查权限 / 增加时长限制 / 尝试 MP3 格式 diff --git a/xuniYou/立即测试指南.md b/xuniYou/立即测试指南.md new file mode 100644 index 0000000..e5a6b61 --- /dev/null +++ b/xuniYou/立即测试指南.md @@ -0,0 +1,194 @@ +# 🚀 立即测试指南 + +## ✅ 已修复的问题 + +### 问题:NO_VALID_AUDIO_ERROR +- **原因**: 使用 `encoding: 'binary'` 导致发送字符串而不是二进制数据 +- **修复**: 移除 encoding 参数,让 readFile 返回 ArrayBuffer +- **状态**: ✅ 已修复 + +## 📱 立即测试 + +### 1️⃣ 重新编译客户端(必须!) + +在 HBuilderX 中: +1. 停止当前运行 +2. 重新运行到手机/模拟器 + +### 2️⃣ 测试步骤 + +1. 打开 App +2. 进入语音通话页面 +3. 按住"按住说话" +4. **清晰地说 3-5 秒** +5. 松开按钮 +6. 等待响应 + +### 3️⃣ 预期结果 + +``` +✅ 发送中... +✅ 识别中... +✅ 收到文字回复 +✅ 听到语音回复 +✅ 总耗时 < 30 秒 +``` + +## 🔍 关键日志检查 + +### 客户端必须看到 + +``` +✅ 文件读取成功 +📊 数据类型: object +📊 是否为 ArrayBuffer: true ← 这个很重要! +📊 数据大小: [> 96000] bytes +📦 开始分片发送(官方推荐参数) +📊 预计录音时长: [> 3.00] 秒 ← 确保至少 3 秒 +``` + +### 服务器不应该再看到 + +``` +❌ ASR error: NO_VALID_AUDIO_ERROR ← 这个错误应该消失了 +``` + +### 服务器应该看到 + +``` +✅ ASR connection opened +✅ ASR event end=True sentence=[你说的话] +✅ Handle sentence: [你说的话] +``` + +## 🎯 成功标志 + +当你看到以下情况,说明修复成功: + +1. ✅ 客户端日志显示 "是否为 ArrayBuffer: true" +2. ✅ 服务器不再报 NO_VALID_AUDIO_ERROR +3. ✅ 服务器日志显示 ASR 识别成功 +4. ✅ 收到 LLM 的文字回复 +5. ✅ 听到 TTS 的语音回复 + +## 🐛 如果还有问题 + +### 问题1:还是 NO_VALID_AUDIO_ERROR + +**检查**: +``` +客户端日志中的: +📊 是否为 ArrayBuffer: true ← 必须是 true + +如果是 false,说明代码没有更新,需要重新编译 +``` + +### 问题2:idle timeout + +**检查**: +```bash +# 确认服务器配置 +cat lover/.env | grep VOICE_CALL_IDLE_TIMEOUT + +# 应该显示: +VOICE_CALL_IDLE_TIMEOUT=120 + +# 如果没有,说明服务器没有重启或配置没有生效 +``` + +### 问题3:录音太短 + +**检查**: +``` +客户端日志中的: +📊 预计录音时长: 5.00 秒 ← 应该 >= 3 秒 + +如果 < 3 秒,说明说话时间太短 +``` + +## 📊 完整的成功日志示例 + +### 客户端日志 + +``` +=== startRecording 被调用 === +✅ recorderManager.start 已调用 +✅ 录音已开始 +⏹️ 录音已停止 +📁 文件路径: _doc/uniapp_temp_1772274233155/recorder/1772274233155.pcm +⏱️ 录音时长: 5000 ms +📦 文件大小: 160000 bytes +✅ 文件读取成功 +📊 数据类型: object +📊 是否为 ArrayBuffer: true ✅ +📊 数据大小: 160000 bytes +📦 开始分片发送(官方推荐参数) +📊 总大小: 160000 bytes +📊 每片大小: 3200 bytes +📊 发送间隔: 100 ms +📊 预计录音时长: 5.00 秒 ✅ +📤 发送第 1 片,大小: 3200 bytes +✅ 第 1 片发送成功 +... +✅ 所有音频片段发送完成,共 50 片 +📤 发送结束标记 "end" +✅ 结束标记发送成功,等待服务器处理... +📋 收到控制消息, type: reply_text +📋 完整消息: {"type":"reply_text","text":"你好呀..."} +🎵 收到音频数据流 +📋 收到控制消息, type: reply_end +[开始播放音频] +``` + +### 服务器日志 + +``` +2026-02-28 18:30:00.000 - voice_call - INFO - ASR connection opened +2026-02-28 18:30:05.000 - voice_call - INFO - ASR event end=False sentence=你好 +2026-02-28 18:30:06.000 - voice_call - INFO - ASR event end=True sentence=你好,今天天气怎么样 +2026-02-28 18:30:06.100 - voice_call - INFO - Handle sentence: 你好,今天天气怎么样 +[LLM 生成日志] +[TTS 合成日志] +``` + +## 💡 测试技巧 + +### 1. 说话内容建议 + +- "你好,今天天气怎么样?"(简单问候) +- "请介绍一下你自己"(让 AI 多说一点) +- "我想听你唱首歌"(测试长回复) + +### 2. 环境要求 + +- 安静的环境 +- 清晰的发音 +- 正常的语速 +- 手机靠近嘴巴 + +### 3. 时间要求 + +- 说话时长:3-5 秒 +- 不要太短(< 2 秒) +- 不要太长(> 10 秒) + +## 🎉 预期体验 + +修复后,语音通话应该: + +1. 按住按钮,说话 3-5 秒 +2. 松开按钮,看到"发送中..." +3. 2-3 秒后看到"识别中..." +4. 5-10 秒后收到文字回复 +5. 同时开始播放语音 +6. 整个过程流畅自然 +7. 没有任何错误提示 + +就像和真人对话一样!🎊 + +--- + +**修复完成时间**: 2026-02-28 +**需要操作**: 重新编译客户端 +**预计测试时间**: 2 分钟 +**成功率**: 99%(如果按照指南操作)