fix: mp-weixin 录音失败 - 权限声明+authorize+移除TS注解+空音频守卫
- interview.vue: 移除 `let recorder: any` TS 类型注解(小程序构建器不解析 TS,上次修改直接破坏构建从未部署) - interview.vue: 合并两个 onMounted 调用,删除凭空引入的 refreshState() 未定义函数引用 - interview.vue: startRecord 增加 uni.authorize scope.record 权限预请求,拒绝时引导 openSetting - interview.vue: recorder.start 改 wav/16kHz/128kbps(旧 aac/22050/16kbps 产生全 0 损坏文件) - manifest.json: mp-weixin.permission 增加 scope.record 录音权限声明 - tts.controller.ts: ASR 拒绝 <500B 空音频文件,避免 whisper 调 ffmpeg 解码失败刷错日志
This commit is contained in:
@@ -47,6 +47,14 @@ export class TtsController {
|
|||||||
const dest = path.join(uploadDir, file.filename + ext)
|
const dest = path.join(uploadDir, file.filename + ext)
|
||||||
fs.renameSync(file.path, dest)
|
fs.renameSync(file.path, dest)
|
||||||
|
|
||||||
|
// 拒绝损坏/空音频文件,避免 whisper 调用 ffmpeg 解码失败刷错误日志
|
||||||
|
const stat = fs.statSync(dest)
|
||||||
|
if (!stat.size || stat.size < 500) {
|
||||||
|
this.logger.warn(`ASR: empty audio upload (size=${stat.size}), ext=${ext}`)
|
||||||
|
try { fs.unlinkSync(dest) } catch {}
|
||||||
|
return { text: '' }
|
||||||
|
}
|
||||||
|
|
||||||
let wavPath = dest
|
let wavPath = dest
|
||||||
if (ext.toLowerCase() !== '.wav') {
|
if (ext.toLowerCase() !== '.wav') {
|
||||||
const potentialWav = dest.replace(/\.[^.]+$/, '.wav')
|
const potentialWav = dest.replace(/\.[^.]+$/, '.wav')
|
||||||
|
|||||||
@@ -16,6 +16,11 @@
|
|||||||
"urlCheck": false,
|
"urlCheck": false,
|
||||||
"__usePrivacyCheck__": true
|
"__usePrivacyCheck__": true
|
||||||
},
|
},
|
||||||
"usingComponents": true
|
"usingComponents": true,
|
||||||
|
"permission": {
|
||||||
|
"scope.record": {
|
||||||
|
"desc": "用于语音输入回答面试问题"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,8 +148,50 @@ const dhRef = ref(null)
|
|||||||
const isRecording = ref(false)
|
const isRecording = ref(false)
|
||||||
let recorder = null
|
let recorder = null
|
||||||
|
|
||||||
let timerSeconds = 0
|
// 录音初始化(一次性创建 recorder 并设置事件)
|
||||||
let timerInterval = null
|
function initRecorder() {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
if (recorder) return // 已初始化
|
||||||
|
recorder = uni.getRecorderManager()
|
||||||
|
recorder.onStart(() => {
|
||||||
|
console.log('[Recorder] start recording')
|
||||||
|
})
|
||||||
|
recorder.onError((err) => {
|
||||||
|
console.error('[Recorder] error:', err)
|
||||||
|
isRecording.value = false
|
||||||
|
uni.showToast({ title: '录音失败,请重试', icon: 'none' })
|
||||||
|
})
|
||||||
|
recorder.onStop(async (res) => {
|
||||||
|
isRecording.value = false
|
||||||
|
console.log('[Recorder] stop, tempFilePath:', res.tempFilePath)
|
||||||
|
if (!res.tempFilePath) {
|
||||||
|
uni.showToast({ title: '录音文件为空', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const uploadRes = await uni.uploadFile({
|
||||||
|
url: api(API_ENDPOINTS.TTS.ASR),
|
||||||
|
filePath: res.tempFilePath,
|
||||||
|
name: 'audio',
|
||||||
|
header: { 'Authorization': `Bearer ${token()}` },
|
||||||
|
})
|
||||||
|
if (uploadRes.statusCode === 200 && uploadRes.data) {
|
||||||
|
const data = typeof uploadRes.data === 'string' ? JSON.parse(uploadRes.data) : uploadRes.data
|
||||||
|
if (data.text) {
|
||||||
|
inputText.value = data.text
|
||||||
|
uni.vibrateShort({ type: 'light' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (checkAuth(uploadRes)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[ASR] upload error:', e?.message || e)
|
||||||
|
}
|
||||||
|
uni.showToast({ title: '语音识别失败,请手动输入', icon: 'none' })
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
let MAX_QUESTIONS = 10
|
let MAX_QUESTIONS = 10
|
||||||
const progressPercent = computed(() => Math.min((answeredCount.value / MAX_QUESTIONS) * 100, 100))
|
const progressPercent = computed(() => Math.min((answeredCount.value / MAX_QUESTIONS) * 100, 100))
|
||||||
@@ -190,9 +232,9 @@ const selectPosition = (pos) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
initRecorder()
|
||||||
timerInterval = setInterval(() => timerSeconds++, 1000)
|
timerInterval = setInterval(() => timerSeconds++, 1000)
|
||||||
if (!position.value) {
|
if (!position.value) {
|
||||||
// 未传入岗位,展示选择弹窗(无论是否登录)
|
|
||||||
loadPositions()
|
loadPositions()
|
||||||
showPositionPicker.value = true
|
showPositionPicker.value = true
|
||||||
} else if (token()) {
|
} else if (token()) {
|
||||||
@@ -356,43 +398,45 @@ const confirmExit = () => {
|
|||||||
function startRecord() {
|
function startRecord() {
|
||||||
if (aiLoading.value || isComplete.value) return
|
if (aiLoading.value || isComplete.value) return
|
||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
isRecording.value = true
|
if (isRecording.value) return
|
||||||
recorder = uni.getRecorderManager()
|
// 先确保已获得 scope.record 授权,避免没权限时产生全 0 的 corrupted 音频
|
||||||
recorder.onStart(() => {})
|
uni.authorize({
|
||||||
recorder.onError(() => { isRecording.value = false; uni.showToast({ title: '录音失败', icon: 'none' }) })
|
scope: 'scope.record',
|
||||||
recorder.onStop(async (res) => {
|
success: () => doStartRecorder(),
|
||||||
if (!res.tempFilePath) return
|
fail: () => {
|
||||||
const audioPath = res.tempFilePath
|
uni.showModal({
|
||||||
try {
|
title: '需要录音权限',
|
||||||
const uploadRes = await uni.uploadFile({
|
content: '请在「设置」中开启「麦克风」权限后再使用语音输入',
|
||||||
url: api(API_ENDPOINTS.TTS.ASR),
|
confirmText: '去设置',
|
||||||
filePath: audioPath,
|
success: (r) => { if (r.confirm) uni.openSetting({}) },
|
||||||
name: 'audio',
|
|
||||||
header: { 'Authorization': `Bearer ${token()}` },
|
|
||||||
})
|
})
|
||||||
if (uploadRes.statusCode === 200 && uploadRes.data) {
|
},
|
||||||
const data = typeof uploadRes.data === 'string' ? JSON.parse(uploadRes.data) : uploadRes.data
|
|
||||||
if (data.text) {
|
|
||||||
inputText.value = data.text
|
|
||||||
uni.vibrateShort({ type: 'light' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if (checkAuth(uploadRes)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[ASR] upload error:', e?.message || e)
|
|
||||||
}
|
|
||||||
uni.showToast({ title: '语音识别失败,请手动输入', icon: 'none' })
|
|
||||||
})
|
})
|
||||||
recorder.start({ format: 'aac', sampleRate: 22050, numberOfChannels: 1, encodeBitRate: 16000 })
|
|
||||||
uni.vibrateShort({ type: 'medium' })
|
|
||||||
// #endif
|
// #endif
|
||||||
// #ifndef MP-WEIXIN
|
// #ifndef MP-WEIXIN
|
||||||
uni.showToast({ title: '语音输入仅支持小程序', icon: 'none' })
|
uni.showToast({ title: '语音输入仅支持小程序', icon: 'none' })
|
||||||
// #endif
|
// #endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
function doStartRecorder() {
|
||||||
|
isRecording.value = true
|
||||||
|
if (!recorder) initRecorder()
|
||||||
|
if (!recorder) {
|
||||||
|
isRecording.value = false
|
||||||
|
uni.showToast({ title: '录音初始化失败', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recorder.start({
|
||||||
|
format: 'wav',
|
||||||
|
sampleRate: 16000,
|
||||||
|
numberOfChannels: 1,
|
||||||
|
encodeBitRate: 128000,
|
||||||
|
})
|
||||||
|
uni.vibrateShort({ type: 'medium' })
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
function stopRecord() {
|
function stopRecord() {
|
||||||
if (!recorder || !isRecording.value) return
|
if (!recorder || !isRecording.value) return
|
||||||
isRecording.value = false
|
isRecording.value = false
|
||||||
|
|||||||
Reference in New Issue
Block a user