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

211 lines
5.4 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
* 支持内网环境,录音后上传到服务器进行识别
*/
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()