211 lines
5.4 KiB
JavaScript
211 lines
5.4 KiB
JavaScript
/**
|
||
* 语音录音工具类
|
||
* 使用 uni-app 原生录音 API
|
||
* 支持内网环境,录音后上传到服务器进行识别
|
||
*/
|
||
|
||
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('[录音] 录音结束', res)
|
||
this.isRecording = false
|
||
this.tempFilePath = res.tempFilePath
|
||
})
|
||
|
||
// 录音错误监听
|
||
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, // 采样率
|
||
numberOfChannels: 1, // 声道数
|
||
encodeBitRate: 96000, // 编码码率
|
||
format: 'mp3' // 音频格式
|
||
}
|
||
|
||
const config = { ...defaultOptions, ...options }
|
||
|
||
this.recorderManager.start(config)
|
||
}
|
||
|
||
/**
|
||
* 停止录音
|
||
* @returns {Promise<string>} 录音文件临时路径
|
||
*/
|
||
stop() {
|
||
return new Promise((resolve, reject) => {
|
||
if (!this.isRecording) {
|
||
// 如果已经停止了,但有临时文件,返回该文件
|
||
if (this.tempFilePath) {
|
||
resolve(this.tempFilePath)
|
||
} else {
|
||
reject(new Error('未在录音中'))
|
||
}
|
||
return
|
||
}
|
||
|
||
// 注册一次性监听器
|
||
const onStopHandler = (res) => {
|
||
this.tempFilePath = res.tempFilePath
|
||
this.isRecording = false
|
||
resolve(res.tempFilePath)
|
||
}
|
||
|
||
this.recorderManager.onStop(onStopHandler)
|
||
this.recorderManager.stop()
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 上传录音到服务器进行识别
|
||
* @param {string} filePath 录音文件路径
|
||
* @param {Object} params 附加参数(如题目文本等)
|
||
* @returns {Promise<Object>} 识别结果
|
||
*/
|
||
uploadAndRecognize(filePath, params = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
// 获取服务器配置
|
||
const config = require('./config.js').default
|
||
// 开发环境可以用 localhost:5000 测试
|
||
// const serverUrl = 'http://localhost:5000' // Windows本地测试
|
||
const serverUrl = config.API_BASE_URL
|
||
|
||
uni.uploadFile({
|
||
url: `${serverUrl}/api/speech/recognize`,
|
||
filePath: filePath,
|
||
name: 'audio',
|
||
formData: {
|
||
...params,
|
||
format: 'mp3',
|
||
sampleRate: 16000
|
||
},
|
||
success: (uploadRes) => {
|
||
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) {
|
||
reject(new Error('解析结果失败'))
|
||
}
|
||
} else {
|
||
reject(new Error('上传失败'))
|
||
}
|
||
},
|
||
fail: (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()
|