guoyu/fronted_uniapp/utils/speech-recorder.js
2025-12-07 08:40:26 +08:00

263 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 语音录音工具类
* 使用 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<string>} 录音文件临时路径
*/
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<Object>} 识别结果
*/
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<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
}
}
}
/**
* 异步评测方法(用于已停止录音的场景)
* @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 || '评测失败'
}
}
}
}
// 导出单例
export default new SpeechRecorder()