fix: 录音可靠性修复 + ASR 后端优化

前端:
- recorder.onError 时重置 recorder = null,避免错误后录音永久失效
- 增加 asrLoading 状态,ASR 处理中输入框 disabled 并显示'语音识别中...'
- 增加 skipAsr 标志,<1s 的录音跳过 ASR 上传
- uploadFile 增加 30s timeout

后端:
- ASR: OpenAI 失败后 fallback 到本地 whisper,日志级别降为 warn
- try/catch/finally 重构,确保临时文件在所有路径都清理
- OpenAI 和 whisper 结果分别 try,互不影响
This commit is contained in:
yuzhiran
2026-07-05 17:45:03 +08:00
parent 8152278b86
commit 48354e634b
2 changed files with 43 additions and 20 deletions
+16 -2
View File
@@ -65,7 +65,7 @@
<text class="mic-label">{{ isRecording ? recordingDuration + 's' : '按住' }}</text>
</view>
<view class="input-box">
<textarea class="input-area" v-model="inputText" placeholder="输入你的回答..." :auto-height="true" :maxlength="2000" :disabled="aiLoading || isRecording" @confirm="sendAnswer" />
<textarea class="input-area" v-model="inputText" :placeholder="asrLoading ? '语音识别中...' : '输入你的回答...'" :auto-height="true" :maxlength="2000" :disabled="aiLoading || isRecording || asrLoading" @confirm="sendAnswer" />
</view>
<view class="send-btn" :class="{ disabled: (!inputText.trim() && !isRecording) || aiLoading }" @click="sendAnswer">
<text class="send-icon">{{ isRecording ? '◉' : '➤' }}</text>
@@ -153,8 +153,10 @@ const isSpeaking = ref(false)
const dhRef = ref(null)
const isRecording = ref(false)
const recordingDuration = ref(0)
const asrLoading = ref(false)
let recorder = null
let recordTimer = null
let skipAsr = false
function initRecorder() {
// #ifdef MP-WEIXIN
@@ -169,24 +171,29 @@ function initRecorder() {
console.error('[Recorder] error:', JSON.stringify(err))
isRecording.value = false
recordingDuration.value = 0
asrLoading.value = false
if (recordTimer) { clearInterval(recordTimer); recordTimer = null }
const msg = err?.errMsg?.includes('permission') ? '请允许录音权限' : (err?.errMsg || '录音失败,请重试')
uni.showToast({ title: msg, icon: 'none' })
recorder = null
})
recorder.onStop(async (res) => {
isRecording.value = false
recordingDuration.value = 0
if (recordTimer) { clearInterval(recordTimer); recordTimer = null }
if (skipAsr) { skipAsr = false; return }
if (!res.tempFilePath) {
uni.showToast({ title: '录音文件为空', icon: 'none' })
return
}
asrLoading.value = true
try {
const uploadRes = await uni.uploadFile({
url: api(API_ENDPOINTS.TTS.ASR),
filePath: res.tempFilePath,
name: 'audio',
header: { 'Authorization': `Bearer ${token()}` },
timeout: 30000,
})
if (uploadRes.statusCode === 200 && uploadRes.data) {
const data = typeof uploadRes.data === 'string' ? JSON.parse(uploadRes.data) : uploadRes.data
@@ -200,6 +207,8 @@ function initRecorder() {
}
} catch (e) {
console.error('[ASR] upload error:', e?.message || e)
} finally {
asrLoading.value = false
}
uni.showToast({ title: '语音识别失败,请手动输入', icon: 'none' })
})
@@ -418,7 +427,7 @@ const confirmExit = () => {
}
function startRecord() {
if (aiLoading.value || isComplete.value) return
if (aiLoading.value || isComplete.value || asrLoading.value) return
// #ifdef MP-WEIXIN
if (isRecording.value) return
isRecording.value = true
@@ -445,6 +454,11 @@ function stopRecord() {
if (!isRecording.value) return
isRecording.value = false
if (!recorder) return
if (recordingDuration.value < 1) {
skipAsr = true
if (recordTimer) { clearInterval(recordTimer); recordTimer = null }
recordingDuration.value = 0
}
recorder.stop()
}
</script>