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 = ''
|
2025-12-08 08:56:20 +08:00
|
|
|
|
this.lastRecordInfo = null // 保存最后一次录音信息
|
|
|
|
|
|
this.fileVerifyHistory = [] // 文件验证历史
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建备份到永久目录(防止临时文件被系统清理)
|
|
|
|
|
|
* @param {string} tempPath 临时文件路径
|
|
|
|
|
|
* @param {number} fileSize 文件大小
|
|
|
|
|
|
*/
|
|
|
|
|
|
createBackup(tempPath, fileSize) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// #ifdef APP-PLUS
|
|
|
|
|
|
// 在APP中保存到本地存储目录
|
|
|
|
|
|
const fs = plus.io.requestFileSystem(plus.io.PUBLIC_DOCUMENTS, (fs) => {
|
|
|
|
|
|
const fileName = 'voice_backup_' + Date.now() + '.mp3'
|
|
|
|
|
|
fs.root.getDirectory('recordings', { create: true }, (dirEntry) => {
|
|
|
|
|
|
// 复制文件
|
|
|
|
|
|
plus.io.resolveLocalFileSystemURL(tempPath, (entry) => {
|
|
|
|
|
|
entry.copyTo(dirEntry, fileName,
|
|
|
|
|
|
(newEntry) => {
|
|
|
|
|
|
console.log('[录音] 备份成功:', newEntry.fullPath, '大小:', fileSize)
|
|
|
|
|
|
// 保存备份路径,可用于恢复
|
|
|
|
|
|
this.lastBackupPath = newEntry.fullPath
|
|
|
|
|
|
},
|
|
|
|
|
|
(err) => {
|
|
|
|
|
|
console.warn('[录音] 备份失败(不影响使用):', err.message)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
|
|
|
|
// #ifndef APP-PLUS
|
|
|
|
|
|
console.log('[录音] 非APP环境,跳过备份')
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn('[录音] 创建备份异常(不影响使用):', err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 验证文件是否完整
|
|
|
|
|
|
* @param {string} filePath 文件路径
|
|
|
|
|
|
* @param {number} attemptNo 验证次数
|
|
|
|
|
|
*/
|
|
|
|
|
|
verifyFile(filePath, attemptNo = 1) {
|
|
|
|
|
|
uni.getFileInfo({
|
|
|
|
|
|
filePath: filePath,
|
|
|
|
|
|
success: (fileInfo) => {
|
|
|
|
|
|
const verifyInfo = {
|
|
|
|
|
|
attempt: attemptNo,
|
|
|
|
|
|
time: Date.now(),
|
|
|
|
|
|
size: fileInfo.size,
|
|
|
|
|
|
path: filePath
|
|
|
|
|
|
}
|
|
|
|
|
|
this.fileVerifyHistory.push(verifyInfo)
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`[录音] 第${attemptNo}次验证成功:`, {
|
|
|
|
|
|
size: fileInfo.size,
|
|
|
|
|
|
sizeKB: (fileInfo.size / 1024).toFixed(2) + 'KB',
|
|
|
|
|
|
digest: fileInfo.digest || '未知'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 检查文件大小增长(用于判断是否还在写入)
|
|
|
|
|
|
if (attemptNo > 1 && this.fileVerifyHistory.length >= 2) {
|
|
|
|
|
|
const prevSize = this.fileVerifyHistory[this.fileVerifyHistory.length - 2].size
|
|
|
|
|
|
const growth = fileInfo.size - prevSize
|
|
|
|
|
|
if (growth > 0) {
|
|
|
|
|
|
console.log(`[录音] 文件仍在增长: +${growth} bytes`)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log('[录音] 文件大小稳定,编码完成')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 最终验证:检查文件是否太小
|
|
|
|
|
|
if (attemptNo === 3) {
|
|
|
|
|
|
const expectedSize = this.lastRecordInfo?.duration ? this.lastRecordInfo.duration * 16 : 45000
|
|
|
|
|
|
if (fileInfo.size < expectedSize * 0.3) {
|
|
|
|
|
|
console.error('[录音] 警告:文件过小!实际=' + fileInfo.size + ', 预期>' + expectedSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
console.error(`[录音] 第${attemptNo}次验证失败:`, err)
|
|
|
|
|
|
this.fileVerifyHistory.push({
|
|
|
|
|
|
attempt: attemptNo,
|
|
|
|
|
|
time: Date.now(),
|
|
|
|
|
|
error: err.errMsg
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查录音权限
|
|
|
|
|
|
* @returns {Promise<boolean>} 是否有权限
|
|
|
|
|
|
*/
|
|
|
|
|
|
async checkPermission() {
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
// #ifdef APP-PLUS
|
|
|
|
|
|
const permissions = ['android.permission.RECORD_AUDIO']
|
|
|
|
|
|
|
|
|
|
|
|
// 检查权限
|
|
|
|
|
|
const hasPermission = plus.android.checkPermission(permissions[0])
|
|
|
|
|
|
if (hasPermission === 0) {
|
|
|
|
|
|
// 没有权限,请求权限
|
|
|
|
|
|
console.log('[录音] 请求录音权限...')
|
|
|
|
|
|
plus.android.requestPermissions(
|
|
|
|
|
|
permissions,
|
|
|
|
|
|
(result) => {
|
|
|
|
|
|
const granted = result.granted && result.granted.length > 0
|
|
|
|
|
|
console.log('[录音] 权限请求结果:', granted ? '已授权' : '被拒绝')
|
|
|
|
|
|
if (!granted) {
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '需要录音权限',
|
|
|
|
|
|
content: '请在设置中开启录音权限,否则无法使用语音功能',
|
|
|
|
|
|
showCancel: false
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
resolve(granted)
|
|
|
|
|
|
},
|
|
|
|
|
|
(error) => {
|
|
|
|
|
|
console.error('[录音] 权限请求失败', error)
|
|
|
|
|
|
resolve(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log('[录音] 已有录音权限')
|
|
|
|
|
|
resolve(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
// #ifndef APP-PLUS
|
|
|
|
|
|
// 非APP环境默认有权限
|
|
|
|
|
|
resolve(true)
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
})
|
2025-12-07 00:11:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 初始化录音管理器
|
|
|
|
|
|
*/
|
2025-12-08 08:56:20 +08:00
|
|
|
|
async init() {
|
|
|
|
|
|
// 先检查权限
|
|
|
|
|
|
const hasPermission = await this.checkPermission()
|
|
|
|
|
|
if (!hasPermission) {
|
|
|
|
|
|
console.error('[录音] 没有录音权限,初始化失败')
|
|
|
|
|
|
throw new Error('没有录音权限')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-07 00:11:06 +08:00
|
|
|
|
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
|
|
|
|
|
2025-12-08 08:56:20 +08:00
|
|
|
|
// 保存录音元数据,用于后续验证
|
|
|
|
|
|
this.lastRecordInfo = {
|
|
|
|
|
|
path: res.tempFilePath,
|
|
|
|
|
|
duration: res.duration,
|
|
|
|
|
|
fileSize: res.fileSize,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-07 08:40:26 +08:00
|
|
|
|
// 验证文件是否存在(异步,不阻塞)
|
|
|
|
|
|
if (res.tempFilePath) {
|
2025-12-08 08:56:20 +08:00
|
|
|
|
// 第一次验证(立即)
|
|
|
|
|
|
this.verifyFile(res.tempFilePath, 1)
|
|
|
|
|
|
|
|
|
|
|
|
// 第二次验证(500ms后)
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.verifyFile(res.tempFilePath, 2)
|
|
|
|
|
|
}, 500)
|
|
|
|
|
|
|
|
|
|
|
|
// 第三次验证(1500ms后,最终验证)
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.verifyFile(res.tempFilePath, 3)
|
|
|
|
|
|
}, 1500)
|
2025-12-07 08:40:26 +08:00
|
|
|
|
}
|
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, // 声道数(单声道)
|
2025-12-08 08:56:20 +08:00
|
|
|
|
encodeBitRate: 128000, // 编码码率(提高到128k,减少数据丢失)
|
|
|
|
|
|
format: 'mp3', // 音频格式(统一使用MP3,兼容性最好)
|
|
|
|
|
|
frameSize: 10 // 帧大小(值越小越频繁回调,数据越完整,默认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
|
|
|
|
|
2025-12-08 08:56:20 +08:00
|
|
|
|
// 等待1500ms让音频缓冲完全写入(MP3编码需要更长时间)
|
|
|
|
|
|
console.log('[录音] 等待音频缓冲写入和MP3编码...')
|
2025-12-07 08:40:26 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
// 等待录音停止完成
|
|
|
|
|
|
const timeout = setTimeout(() => {
|
|
|
|
|
|
console.error('[录音] 停止录音超时!')
|
|
|
|
|
|
reject(new Error('停止录音超时'))
|
2025-12-08 08:56:20 +08:00
|
|
|
|
}, 10000) // 增加超时时间
|
2025-12-07 08:40:26 +08:00
|
|
|
|
|
|
|
|
|
|
// 等待onStop事件(已在init中注册)
|
|
|
|
|
|
const checkInterval = setInterval(() => {
|
|
|
|
|
|
if (!this.isRecording && this.tempFilePath) {
|
|
|
|
|
|
clearTimeout(timeout)
|
|
|
|
|
|
clearInterval(checkInterval)
|
|
|
|
|
|
console.log('[录音] 停止成功,文件路径:', this.tempFilePath)
|
|
|
|
|
|
|
2025-12-08 08:56:20 +08:00
|
|
|
|
// 再等待1000ms确保MP3文件完全编码和写入磁盘
|
|
|
|
|
|
console.log('[录音] 等待MP3文件完全编码保存...')
|
2025-12-07 08:40:26 +08:00
|
|
|
|
setTimeout(() => {
|
2025-12-08 08:56:20 +08:00
|
|
|
|
// 验证文件并创建备份
|
|
|
|
|
|
uni.getFileInfo({
|
|
|
|
|
|
filePath: this.tempFilePath,
|
|
|
|
|
|
success: (fileInfo) => {
|
|
|
|
|
|
console.log('[录音] 文件已完全保存,大小:', fileInfo.size, 'bytes')
|
|
|
|
|
|
|
|
|
|
|
|
// 创建备份到永久目录(可选,降低丢失风险)
|
|
|
|
|
|
this.createBackup(this.tempFilePath, fileInfo.size)
|
|
|
|
|
|
|
|
|
|
|
|
if (fileInfo.size < 5000) {
|
|
|
|
|
|
console.warn('[录音] 警告:文件太小,可能数据丢失')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加文件元数据
|
|
|
|
|
|
const fileWithMeta = {
|
|
|
|
|
|
path: this.tempFilePath,
|
|
|
|
|
|
size: fileInfo.size,
|
|
|
|
|
|
duration: this.lastRecordInfo?.duration,
|
|
|
|
|
|
verifyCount: this.fileVerifyHistory.length,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resolve(this.tempFilePath)
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
console.error('[录音] 文件验证失败,但仍返回路径', err)
|
|
|
|
|
|
resolve(this.tempFilePath)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}, 1000) // 增加到1000ms
|
2025-12-07 08:40:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, 100)
|
2025-12-08 08:56:20 +08:00
|
|
|
|
}, 1500) // 增加到1500ms
|
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-08 08:56:20 +08:00
|
|
|
|
format: 'mp3', // 匹配录音格式
|
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()
|