/** * 语音录音工具类 * 使用 uni-app 原生录音 API * 支持内网环境,录音后上传到服务器进行识别 */ import config from './config.js' 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) => { console.log('[录音] 录音结束,详细信息:', { tempFilePath: res.tempFilePath, duration: res.duration || '未知', fileSize: res.fileSize || '未知' }) this.isRecording = false this.tempFilePath = res.tempFilePath // 验证文件是否存在(异步,不阻塞) if (res.tempFilePath) { uni.getFileInfo({ filePath: res.tempFilePath, success: (fileInfo) => { console.log('[录音] 文件验证成功,大小:', fileInfo.size, 'bytes') }, fail: (err) => { console.error('[录音] 文件验证失败:', err) } }) } }) // 录音错误监听 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, // 最长录音时间(毫秒) sampleRate: 16000, // 采样率(百度API推荐) numberOfChannels: 1, // 声道数(单声道) encodeBitRate: 48000, // 编码码率 format: 'wav', // 音频格式(WAV格式,百度API完美支持) frameSize: 50 // 指定帧大小,增加缓冲(避免数据丢失) } const config = { ...defaultOptions, ...options } this.recorderManager.start(config) } /** * 停止录音 * @returns {Promise} 录音文件临时路径 */ stop() { return new Promise((resolve, reject) => { if (!this.isRecording) { // 如果已经停止了,但有临时文件,返回该文件 if (this.tempFilePath) { console.log('[录音] 使用已有的录音文件:', this.tempFilePath) resolve(this.tempFilePath) } else { reject(new Error('未在录音中')) } return } console.log('[录音] 准备停止录音...') console.log('[录音] 录音状态:', this.isRecording) // 先停止录音 this.recorderManager.stop() // 等待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) }) } /** * 上传录音到服务器进行识别 * @param {string} filePath 录音文件路径 * @param {Object} params 附加参数(如题目文本等) * @returns {Promise} 识别结果 */ uploadAndRecognize(filePath, params = {}) { return new Promise((resolve, reject) => { // 使用导入的服务器配置 // 开发环境可以用 localhost:5000 测试 // const serverUrl = 'http://localhost:5000' // Windows本地测试 const serverUrl = config.API_BASE_URL console.log('[上传] 开始上传录音文件') console.log('[上传] 服务器地址:', serverUrl) console.log('[上传] 文件路径:', filePath) console.log('[上传] 参数:', params) uni.uploadFile({ url: `${serverUrl}/api/speech/recognize`, filePath: filePath, name: 'audio', formData: { ...params, format: 'wav', // 匹配录音格式 sampleRate: 16000 }, success: (uploadRes) => { console.log('[上传] 上传成功,状态码:', uploadRes.statusCode) console.log('[上传] 响应数据:', uploadRes.data) 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) { console.error('[上传] 解析响应失败:', e) reject(new Error('解析结果失败')) } } else { console.error('[上传] HTTP状态码错误:', uploadRes.statusCode) reject(new Error('上传失败')) } }, fail: (err) => { console.error('[上传] 上传请求失败:', err) reject(err) } }) }) } /** * 完整的语音评测流程 * @param {string} referenceText 参考文本(标准答案) * @param {number} duration 录音时长(毫秒) * @returns {Promise} 评测结果 */ 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 } } } /** * 异步评测方法(用于已停止录音的场景) * @param {string} referenceText 参考文本 * @param {number} questionId 题目ID(可选) * @returns {Promise} 评测结果 */ 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 || '评测失败' } } } } // 导出单例 export default new SpeechRecorder()