未成功录音
This commit is contained in:
parent
4d3ae549e1
commit
cbe0ebe1c5
250
xuniYou/NO_VALID_AUDIO_ERROR问题修复.md
Normal file
250
xuniYou/NO_VALID_AUDIO_ERROR问题修复.md
Normal file
|
|
@ -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
|
||||||
|
**状态**: ✅ 已修复,待测试
|
||||||
|
|
@ -431,10 +431,30 @@
|
||||||
// 监听录音停止 - 作为备用方案
|
// 监听录音停止 - 作为备用方案
|
||||||
recorderManager.onStop((res) => {
|
recorderManager.onStop((res) => {
|
||||||
console.log('⏹️ 录音已停止')
|
console.log('⏹️ 录音已停止')
|
||||||
|
console.log('📋 完整的 res 对象:', JSON.stringify(res))
|
||||||
console.log('📁 文件路径:', res.tempFilePath)
|
console.log('📁 文件路径:', res.tempFilePath)
|
||||||
console.log('⏱️ 录音时长:', res.duration, 'ms')
|
console.log('⏱️ 录音时长:', res.duration, 'ms')
|
||||||
console.log('📦 文件大小:', res.fileSize, 'bytes')
|
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 状态
|
// 检查 WebSocket 状态
|
||||||
if (!this.socketTask) {
|
if (!this.socketTask) {
|
||||||
console.error('❌ socketTask 不存在')
|
console.error('❌ socketTask 不存在')
|
||||||
|
|
@ -461,10 +481,11 @@
|
||||||
const fs = uni.getFileSystemManager()
|
const fs = uni.getFileSystemManager()
|
||||||
fs.readFile({
|
fs.readFile({
|
||||||
filePath: res.tempFilePath,
|
filePath: res.tempFilePath,
|
||||||
encoding: 'binary', // 明确指定二进制编码
|
// ⚠️ 不指定 encoding,让它返回 ArrayBuffer
|
||||||
success: (fileRes) => {
|
success: (fileRes) => {
|
||||||
console.log('✅ 文件读取成功')
|
console.log('✅ 文件读取成功')
|
||||||
console.log('📊 数据类型:', typeof fileRes.data)
|
console.log('📊 数据类型:', typeof fileRes.data)
|
||||||
|
console.log('📊 是否为 ArrayBuffer:', fileRes.data instanceof ArrayBuffer)
|
||||||
console.log('📊 数据大小:', fileRes.data.byteLength || fileRes.data.length, 'bytes')
|
console.log('📊 数据大小:', fileRes.data.byteLength || fileRes.data.length, 'bytes')
|
||||||
|
|
||||||
// 再次检查 WebSocket 状态
|
// 再次检查 WebSocket 状态
|
||||||
|
|
@ -473,8 +494,19 @@
|
||||||
return
|
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) => {
|
fail: (err) => {
|
||||||
console.error('❌ 文件读取失败:', err)
|
console.error('❌ 文件读取失败:', err)
|
||||||
|
|
@ -557,7 +589,18 @@
|
||||||
// PCM 16kHz 单声道:16000 * 2 * 0.1 = 3200 bytes/100ms
|
// PCM 16kHz 单声道:16000 * 2 * 0.1 = 3200 bytes/100ms
|
||||||
const chunkSize = 3200 // 3.2KB per chunk(官方推荐)
|
const chunkSize = 3200 // 3.2KB per chunk(官方推荐)
|
||||||
const chunkDelay = 100 // 100ms(官方推荐)
|
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 offset = 0
|
||||||
let chunkCount = 0
|
let chunkCount = 0
|
||||||
|
|
||||||
|
|
@ -566,6 +609,7 @@
|
||||||
console.log('📊 每片大小:', chunkSize, 'bytes')
|
console.log('📊 每片大小:', chunkSize, 'bytes')
|
||||||
console.log('📊 发送间隔:', chunkDelay, 'ms')
|
console.log('📊 发送间隔:', chunkDelay, 'ms')
|
||||||
console.log('📊 预计发送时间:', Math.ceil(totalSize / chunkSize) * chunkDelay, 'ms')
|
console.log('📊 预计发送时间:', Math.ceil(totalSize / chunkSize) * chunkDelay, 'ms')
|
||||||
|
console.log('📊 预计录音时长:', (totalSize / 32000).toFixed(2), '秒')
|
||||||
|
|
||||||
// 显示加载提示
|
// 显示加载提示
|
||||||
uni.showLoading({
|
uni.showLoading({
|
||||||
|
|
|
||||||
248
xuniYou/录音失败问题诊断.md
Normal file
248
xuniYou/录音失败问题诊断.md
Normal file
|
|
@ -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
|
||||||
|
<view class="opt_name">
|
||||||
|
{{ isTalking ? '松开结束(至少3秒)' : '按住说话' }}
|
||||||
|
</view>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案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 格式
|
||||||
194
xuniYou/立即测试指南.md
Normal file
194
xuniYou/立即测试指南.md
Normal file
|
|
@ -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%(如果按照指南操作)
|
||||||
Loading…
Reference in New Issue
Block a user