Vue+Node生产环境解决Uncaught (in promise) DOMException音频加载错误
语音转文本+ChatGPT+AWS Polly Web应用部署到Vercel/Netlify时的404错误问题
我开发了一款将语音转换为文本,再将文本发送至OpenAI(ChatGPT)获取回答,并通过AWS Polly生成语音输出的Web应用。该应用在本地运行正常,但部署到Vercel和Netlify生产环境时,出现以下错误:
/voice/66154949.mp3:1 GET https://jarvis-ruby-rho.vercel.app/voice/66154949.mp3 net::ERR_ABORTED 404 (Not Found) Uncaught (in promise) DOMException: Failed to load because no supported source was found.
后端Node(Express)代码
const express = require("express"); const app = express(); const cors = require("cors"); const bodyParser = require("body-parser"); const fs = require("fs"); const dotenv = require("dotenv") dotenv.config() const PORT = process.env.PORT || 3001; const { Configuration, OpenAIApi } = require("openai"); const configuration = new Configuration({ apiKey: process.env.OPEN_AI_KEY}); const openai = new OpenAIApi(configuration); app.use(bodyParser.json()); app.use(cors()); const AWS = require("aws-sdk"); AWS.config.loadFromPath("AWS_Credentials.json"); app.post('/api/TTS', async (req, res) => { const completion = await openai.createCompletion({ model: "text-davinci-003", prompt: req.body.text, max_tokens: 100, temperature: 0.5 }) console.log(completion) let num = (Math.random() * 100000000).toFixed(0); const awsPolly = new AWS.Polly({ region: "us-east-1" }) const params = { OutputFormat: "mp3", Text: completion.data.choices[0].text, VoiceId: "Matthew" } awsPolly.synthesizeSpeech(params, (err, data) => { if (err) { console.error(err); return; } let filePath = "../public/voice/"; let fileName = num + ".mp3"; if (num) fs.writeFileSync(filePath + fileName, data.AudioStream) }) setTimeout(() => { res.status(200).json(num) }, 4500) }) app.listen(PORT, () => { console.log(`Listening at ${PORT}`); });
前端Vue代码
<script setup> import { ref } from 'vue'; import { useAVLine } from 'vue-audio-visual'; const player = ref(null); const canvas = ref(null); let mySource = ref(null); let action = ref(''); let output = ref(''); useAVLine(player, canvas, { src: mySource, canvHeight: 300, canvWidth: 1000, lineColor: 'orange' }); const runSpeechRecognition = () => { var SpeechRecognition = SpeechRecognition || webkitSpeechRecognition; var recognition = new SpeechRecognition(); recognition.onstart = () => { action.value = "I'm Listening"; }; recognition.onspeechend = () => { action.value = 'Done'; recognition.stop(); }; recognition.onresult = async (event) => { var transcript = event.results[0][0].transcript; output.value = transcript; try { const baseURL = import.meta.env.VITE_BASE_SERVER_URL || 'http://localhost:3001' const response = await fetch(`${baseURL}/api/TTS`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ text: event.results[0][0].transcript, }), }); if (response.ok) { const data = await response.json(); console.log(data) if (data) { mySource.value = `${import.meta.env.BASE_URL}voice/${data}.mp3`; setTimeout(() => { player.value.play(); }, 500); } } else { console.log('Request failed with status:', response.status); } } catch (err) { console.log(err); } }; recognition.start(); }; </script> <template> <div class="show"> <div class="action" v-if="action">{{ action }}</div> <div class="synthesizedSpeech" v-if="output"><b>Question</b>: {{ output }}</div> </div> <div class="audio-wrapper"> <audio id="player" ref="player" :src="mySource" type="audio/mp3" controls hidden></audio> <canvas ref="canvas" /> <div class="button-section"> <button type="button" @click="runSpeechRecognition()">Ask Me Anything</button> </div> </div> </template> <style> body { background-color: rgb(23, 23, 23); } .audio-wrapper{ height: 80vh; display: flex; flex-direction: column; justify-content: center; align-items: center; } canvas { display: block; width: 800px; } .button-section { display: flex; justify-content: center; margin-top: 30px; } button { padding: 8px 13px; border-radius: 5px; background-color: orange; color: white; font-weight: 700; font-size: 18px; border: none; cursor: pointer; } .show { width: 100%; text-align: center; color: white; } .action { margin-top: 10px; margin-bottom: 10px; } .synthesizedSpeech { max-width: 500px; padding: 20px; border-radius: 10px; display: inline-block; background: #313131; font-weight: bold; } </style>
附:后端代码嵌套在Vue项目的根目录下。
问题原因与解决方案
核心问题
Vercel、Netlify这类平台采用无服务器架构,后端函数运行的文件系统是临时的,函数执行结束后生成的本地文件会被立即销毁;同时前端无法直接访问后端函数生成的本地文件路径,这就是导致404错误的根本原因。
解决方案1:直接返回音频流给前端(推荐)
修改后端接口,跳过本地文件生成步骤,将AWS Polly返回的音频流直接作为响应返回给前端,前端接收后转换成Blob播放:
后端代码修改
app.post('/api/TTS', async (req, res) => { try { const completion = await openai.createCompletion({ model: "text-davinci-003", prompt: req.body.text, max_tokens: 100, temperature: 0.5 }) const awsPolly = new AWS.Polly({ region: "us-east-1" }) const params = { OutputFormat: "mp3", Text: completion.data.choices[0].text, VoiceId: "Matthew" } awsPolly.synthesizeSpeech(params, (err, data) => { if (err) { console.error(err); return res.status(500).json({ error: err.message }); } // 设置响应头,返回音频流 res.setHeader('Content-Type', 'audio/mpeg'); res.send(data.AudioStream); }) } catch (err) { res.status(500).json({ error: err.message }); } })
前端代码修改(在recognition.onresult方法内)
if (response.ok) { // 接收音频Blob const blob = await response.blob(); // 创建临时URL用于播放 mySource.value = URL.createObjectURL(blob); setTimeout(() => { player.value.play(); }, 500); }
解决方案2:使用云存储存储音频文件
如果需要长期保留音频文件,可以将生成的mp3上传到AWS S3或其他云存储服务,然后返回存储的公开URL给前端:
- 配置AWS S3存储桶权限,允许后端函数上传文件
- 修改后端代码,将Polly生成的音频流上传到S3,返回S3的公开访问URL
- 前端使用该URL播放音频
其他注意事项
- AWS凭证安全:不要将
AWS_Credentials.json文件上传到代码仓库,在Vercel/Netlify的环境变量中配置AWS_ACCESS_KEY_ID和AWS_SECRET_ACCESS_KEY,然后修改AWS初始化代码:AWS.config.update({ accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, region: "us-east-1" }); - 移除不必要的异步延迟:后端的
setTimeout是为了等待文件写入完成,改用直接返回音频流后可以删除该延迟,避免异步问题 - 本地目录清理:部署时不需要
public/voice目录,因为不再生成本地文件
内容的提问来源于stack exchange,提问作者George Marwanqana
相关产品推荐
相关产品推荐

