From a9c6b03c67512b9e1e89d8f86a020949e70e2398 Mon Sep 17 00:00:00 2001 From: yuzhiran Date: Sat, 4 Jul 2026 13:13:32 +0800 Subject: [PATCH] fix: ASR audio format conversion + share.vue deduplicate share creation - TTS /tts/asr: convert non-WAV (AAC/MP3) to WAV before whisper transcription - share.vue: only create share record if no cached shareCode exists (prevents duplicate share records on every page load) --- backend/src/modules/tts/tts.controller.ts | 21 ++++++++++++++--- zhiyin-app/src/pages/share/share.vue | 28 ++++++++++++----------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/backend/src/modules/tts/tts.controller.ts b/backend/src/modules/tts/tts.controller.ts index f0374e7..5834de2 100644 --- a/backend/src/modules/tts/tts.controller.ts +++ b/backend/src/modules/tts/tts.controller.ts @@ -46,13 +46,27 @@ export class TtsController { const ext = file.originalname ? path.extname(file.originalname) || '.aac' : '.aac' const dest = path.join(uploadDir, file.filename + ext) fs.renameSync(file.path, dest) + + let wavPath = dest + if (ext.toLowerCase() !== '.wav') { + const potentialWav = dest.replace(/\.[^.]+$/, '.wav') + try { + execSync(`ffmpeg -y -i "${dest}" -ar 16000 -ac 1 -c:a pcm_s16le "${potentialWav}"`, { + timeout: 30000, encoding: 'utf8', stdio: 'pipe', + }) + wavPath = potentialWav + } catch (e: any) { + this.logger.warn(`FFmpeg conversion failed: ${e.message}, using original format`) + } + } + try { 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=@${dest}" \ + -F "file=@${wavPath}" \ -F "model=whisper-1" \ -F "language=zh"`, { encoding: 'utf8', timeout: 30000 }, @@ -60,7 +74,7 @@ export class TtsController { 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())' "${dest}"`, { encoding: 'utf8', timeout: 60000 }) + 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() } } @@ -68,7 +82,8 @@ export class TtsController { this.logger.error(`ASR failed: ${e?.message || e}`) } // 清理临时文件 - try { fs.unlinkSync(dest) } 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/share/share.vue b/zhiyin-app/src/pages/share/share.vue index 704998a..3a03d14 100644 --- a/zhiyin-app/src/pages/share/share.vue +++ b/zhiyin-app/src/pages/share/share.vue @@ -178,20 +178,22 @@ async function loadData() { if (!token) { uni.showToast({ title: '请先登录', icon: 'none' }); return } const header = { Authorization: `Bearer ${token}` } - // 先创建分享链接,缓存下来供复制使用 - try { - const res = await uni.request({ - url: api('/share/create'), method: 'POST', - data: { type: 'app', title: '我在AI磁场·职引练习面试', description: 'AI模拟面试+简历优化,快来一起提升吧' }, - header, - }) - if (res.statusCode >= 200 && res.statusCode < 300) { - const data = res.data?.data || res.data - if (data.shareCode) { - shareUrlCached.value = `https://zhiyinwx.yzrcloud.cn/api/share/${data.shareCode}` + // 仅在无缓存分享链接时创建新分享记录 + if (!shareUrlCached.value) { + try { + const res = await uni.request({ + url: api('/share/create'), method: 'POST', + data: { type: 'app', title: '我在AI磁场·职引练习面试', description: 'AI模拟面试+简历优化,快来一起提升吧' }, + header, + }) + if (res.statusCode >= 200 && res.statusCode < 300) { + const data = res.data?.data || res.data + if (data.shareCode) { + shareUrlCached.value = `https://zhiyinwx.yzrcloud.cn/api/share/${data.shareCode}` + } } - } - } catch (e) { /* create share is best-effort */ } + } catch (e) { /* create share is best-effort */ } + } try { const [statsRes, recordsRes, visitorsRes] = await Promise.all([