You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 09:58:12