diff --git a/backend/src/modules/tts/tts.controller.ts b/backend/src/modules/tts/tts.controller.ts
index 2a5ea62..2e796ae 100644
--- a/backend/src/modules/tts/tts.controller.ts
+++ b/backend/src/modules/tts/tts.controller.ts
@@ -69,29 +69,38 @@ export class TtsController {
}
try {
+ let text = ''
if (process.env.OPENAI_API_KEY) {
- const result = execSync(
- `curl -s -X POST https://api.openai.com/v1/audio/transcriptions \
- -H "Authorization: Bearer ${process.env.OPENAI_API_KEY}" \
- -H "Content-Type: multipart/form-data" \
- -F "file=@${wavPath}" \
- -F "model=whisper-1" \
- -F "language=zh"`,
- { encoding: 'utf8', timeout: 30000 },
+ try {
+ const result = execSync(
+ `curl -s -X POST https://api.openai.com/v1/audio/transcriptions \
+ -H "Authorization: Bearer ${process.env.OPENAI_API_KEY}" \
+ -H "Content-Type: multipart/form-data" \
+ -F "file=@${wavPath}" \
+ -F "model=whisper-1" \
+ -F "language=zh"`,
+ { encoding: 'utf8', timeout: 30000 },
+ )
+ const parsed = JSON.parse(result)
+ if (parsed.text) text = parsed.text.trim()
+ } catch (e: any) {
+ this.logger.warn(`OpenAI ASR failed, falling back to local: ${e.message}`)
+ }
+ }
+ if (!text) {
+ const whisperResult = execSync(
+ `python3 -c 'import sys, whisper; model = whisper.load_model("tiny"); print(model.transcribe(sys.argv[1], language="zh")["text"].strip())' "${wavPath}"`,
+ { encoding: 'utf8', timeout: 60000 },
)
- const parsed = JSON.parse(result)
- if (parsed.text) return { text: parsed.text.trim() }
- }
- const whisperResult = execSync(`python3 -c 'import sys, whisper; model = whisper.load_model("tiny"); print(model.transcribe(sys.argv[1], language="zh")["text"].strip())' "${wavPath}"`, { encoding: 'utf8', timeout: 60000 })
- if (whisperResult && whisperResult.trim()) {
- return { text: whisperResult.trim() }
+ if (whisperResult?.trim()) text = whisperResult.trim()
}
+ return { text }
} catch (e: any) {
this.logger.error(`ASR failed: ${e?.message || e}`)
+ return { text: '' }
+ } finally {
+ try { if (dest) fs.unlinkSync(dest) } catch {}
+ try { if (wavPath && wavPath !== dest) fs.unlinkSync(wavPath) } catch {}
}
- // 清理临时文件
- try { if (dest) fs.unlinkSync(dest) } catch {}
- try { if (wavPath && wavPath !== dest) fs.unlinkSync(wavPath) } catch {}
- return { text: '' }
}
}
diff --git a/zhiyin-app/src/pages/interview/interview.vue b/zhiyin-app/src/pages/interview/interview.vue
index 7c1dfb0..5bd9946 100644
--- a/zhiyin-app/src/pages/interview/interview.vue
+++ b/zhiyin-app/src/pages/interview/interview.vue
@@ -65,7 +65,7 @@
{{ isRecording ? recordingDuration + 's' : '按住' }}
-
+
{{ isRecording ? '◉' : '➤' }}
@@ -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()
}