guoyu/fronted_uniapp/utils/speech-recorder.js

263 lines
7.7 KiB
JavaScript
Raw Normal View History

2025-12-07 00:11:06 +08:00
/**
* 语音录音工具类
* 使用 uni-app 原生录音 API
* 支持内网环境录音后上传到服务器进行识别
*/
2025-12-07 08:40:26 +08:00
import config from './config.js'
2025-12-07 00:11:06 +08:00
class SpeechRecorder {
constructor() {
this.recorderManager = null
this.isRecording = false
this.tempFilePath = ''
}
/**
* 初始化录音管理器
*/
init() {
this.recorderManager = uni.getRecorderManager()
// 录音开始监听
this.recorderManager.onStart(() => {
console.log('[录音] 开始录音')
this.isRecording = true
})
// 录音结束监听
this.recorderManager.onStop((res) => {
2025-12-07 08:40:26 +08:00
console.log('[录音] 录音结束,详细信息:', {
tempFilePath: res.tempFilePath,
duration: res.duration || '未知',
fileSize: res.fileSize || '未知'
})
2025-12-07 00:11:06 +08:00
this.isRecording = false
this.tempFilePath = res.tempFilePath
2025-12-07 08:40:26 +08:00
// 验证文件是否存在(异步,不阻塞)
if (res.tempFilePath) {
uni.getFileInfo({
filePath: res.tempFilePath,
success: (fileInfo) => {
console.log('[录音] 文件验证成功,大小:', fileInfo.size, 'bytes')
},
fail: (err) => {
console.error('[录音] 文件验证失败:', err)
}
})
}
2025-12-07 00:11:06 +08:00
})
// 录音错误监听
this.recorderManager.onError((err) => {
console.error('[录音] 录音错误', err)
this.isRecording = false
uni.showToast({
title: '录音失败:' + err.errMsg,
icon: 'none'
})
})
}
/**
* 开始录音
* @param {Object} options 录音配置
*/
start(options = {}) {
if (!this.recorderManager) {
this.init()
}
const defaultOptions = {
duration: 60000, // 最长录音时间(毫秒)
2025-12-07 08:40:26 +08:00
sampleRate: 16000, // 采样率百度API推荐
numberOfChannels: 1, // 声道数(单声道)
encodeBitRate: 48000, // 编码码率
format: 'wav', // 音频格式WAV格式百度API完美支持
frameSize: 50 // 指定帧大小,增加缓冲(避免数据丢失)
2025-12-07 00:11:06 +08:00
}
const config = { ...defaultOptions, ...options }
this.recorderManager.start(config)
}
/**
* 停止录音
* @returns {Promise<string>} 录音文件临时路径
*/
stop() {
return new Promise((resolve, reject) => {
if (!this.isRecording) {
2025-12-07 01:19:40 +08:00
// 如果已经停止了,但有临时文件,返回该文件
if (this.tempFilePath) {
2025-12-07 08:40:26 +08:00
console.log('[录音] 使用已有的录音文件:', this.tempFilePath)
2025-12-07 01:19:40 +08:00
resolve(this.tempFilePath)
} else {
reject(new Error('未在录音中'))
}
2025-12-07 00:11:06 +08:00
return
}
2025-12-07 08:40:26 +08:00
console.log('[录音] 准备停止录音...')
console.log('[录音] 录音状态:', this.isRecording)
2025-12-07 00:11:06 +08:00
2025-12-07 08:40:26 +08:00
// 先停止录音
2025-12-07 00:11:06 +08:00
this.recorderManager.stop()
2025-12-07 08:40:26 +08:00
// 等待800ms让音频缓冲完全写入增加延迟解决数据丢失
console.log('[录音] 等待音频缓冲写入...')
setTimeout(() => {
// 等待录音停止完成
const timeout = setTimeout(() => {
console.error('[录音] 停止录音超时!')
reject(new Error('停止录音超时'))
}, 8000)
// 等待onStop事件已在init中注册
const checkInterval = setInterval(() => {
if (!this.isRecording && this.tempFilePath) {
clearTimeout(timeout)
clearInterval(checkInterval)
console.log('[录音] 停止成功,文件路径:', this.tempFilePath)
// 再等待500ms确保文件完全写入磁盘增加延迟
console.log('[录音] 等待文件完全保存...')
setTimeout(() => {
console.log('[录音] 文件已完全保存,准备返回')
resolve(this.tempFilePath)
}, 500)
}
}, 100)
}, 800)
2025-12-07 00:11:06 +08:00
})
}
/**
* 上传录音到服务器进行识别
* @param {string} filePath 录音文件路径
* @param {Object} params 附加参数如题目文本等
* @returns {Promise<Object>} 识别结果
*/
uploadAndRecognize(filePath, params = {}) {
return new Promise((resolve, reject) => {
2025-12-07 08:40:26 +08:00
// 使用导入的服务器配置
2025-12-07 01:19:40 +08:00
// 开发环境可以用 localhost:5000 测试
// const serverUrl = 'http://localhost:5000' // Windows本地测试
2025-12-07 00:11:06 +08:00
const serverUrl = config.API_BASE_URL
2025-12-07 08:40:26 +08:00
console.log('[上传] 开始上传录音文件')
console.log('[上传] 服务器地址:', serverUrl)
console.log('[上传] 文件路径:', filePath)
console.log('[上传] 参数:', params)
2025-12-07 00:11:06 +08:00
uni.uploadFile({
url: `${serverUrl}/api/speech/recognize`,
filePath: filePath,
name: 'audio',
formData: {
...params,
2025-12-07 08:40:26 +08:00
format: 'wav', // 匹配录音格式
2025-12-07 00:11:06 +08:00
sampleRate: 16000
},
success: (uploadRes) => {
2025-12-07 08:40:26 +08:00
console.log('[上传] 上传成功,状态码:', uploadRes.statusCode)
console.log('[上传] 响应数据:', uploadRes.data)
2025-12-07 00:11:06 +08:00
if (uploadRes.statusCode === 200) {
try {
const result = JSON.parse(uploadRes.data)
if (result.code === 200) {
resolve(result.data)
} else {
reject(new Error(result.msg || '识别失败'))
}
} catch (e) {
2025-12-07 08:40:26 +08:00
console.error('[上传] 解析响应失败:', e)
2025-12-07 00:11:06 +08:00
reject(new Error('解析结果失败'))
}
} else {
2025-12-07 08:40:26 +08:00
console.error('[上传] HTTP状态码错误:', uploadRes.statusCode)
2025-12-07 00:11:06 +08:00
reject(new Error('上传失败'))
}
},
fail: (err) => {
2025-12-07 08:40:26 +08:00
console.error('[上传] 上传请求失败:', err)
2025-12-07 00:11:06 +08:00
reject(err)
}
})
})
}
/**
* 完整的语音评测流程
* @param {string} referenceText 参考文本标准答案
* @param {number} duration 录音时长毫秒
* @returns {Promise<Object>} 评测结果
*/
async evaluate(referenceText, duration = 10000) {
try {
// 1. 开始录音
this.start({ duration })
// 2. 等待录音完成(用户手动停止或超时)
const filePath = await this.stop()
// 3. 上传并识别
const result = await this.uploadAndRecognize(filePath, {
referenceText: referenceText
})
return {
success: true,
...result
}
} catch (error) {
console.error('[语音评测] 失败', error)
return {
success: false,
error: error.message
}
}
}
2025-12-07 01:19:40 +08:00
/**
* 异步评测方法用于已停止录音的场景
* @param {string} referenceText 参考文本
* @param {number} questionId 题目ID可选
* @returns {Promise<Object>} 评测结果
*/
async evaluateAsync(referenceText, questionId = null) {
try {
console.log('[语音评测] 开始异步评测', { referenceText, questionId, tempFilePath: this.tempFilePath })
// 检查是否有录音文件
if (!this.tempFilePath) {
throw new Error('没有可用的录音文件')
}
// 上传并识别
const result = await this.uploadAndRecognize(this.tempFilePath, {
referenceText: referenceText,
questionId: questionId
})
console.log('[语音评测] 评测成功', result)
return {
success: true,
...result
}
} catch (error) {
console.error('[语音评测] 评测失败', error)
return {
success: false,
error: error.message || '评测失败'
}
}
}
2025-12-07 00:11:06 +08:00
}
// 导出单例
export default new SpeechRecorder()