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

基于OpenAI Whisper API的音视频转写应用报错求助:400与响应头错误

问题:基于OpenAI Whisper API的Web应用报错处理

我正在开发一款基于OpenAI Whisper API的简易Web应用,用于将音视频文件转换为文字转录内容。由于Whisper API仅支持小于25MB的文件,我对大文件进行了拆分处理,但运行时出现两个错误:

  • getTranscript函数中出现AxiosError: Request failed with status code 400
  • Error: Cannot set headers after they are sent to the client

以下是相关代码文件:


app.ts

import "dotenv/config";
import express, { NextFunction, Request, Response } from "express";
import scriptRoutes from "./routes/scripts";
import userRoutes from "./routes/users";
import uploadRoutes from "./routes/upload";
import morgan from "morgan";
import createHttpError, { isHttpError } from "http-errors";
import session from "express-session";
import env from "./util/validateEnv";
import MongoStore  from "connect-mongo";
import bodyParser from "body-parser";

const app = express();

app.use(bodyParser.urlencoded({ extended: false }));
app.use(express.json());
app.use(bodyParser.json());

app.use(morgan("dev"));

app.use(express.json());

app.use(session({
    secret: env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
        maxAge: 60 * 60 * 1000,
    },
    rolling: true,
    store: MongoStore.create({
        mongoUrl: env.MONGO_CONNECTION_STRING
    }),
}))

app.use("/api/users", userRoutes);
app.use("/api/scripts", scriptRoutes);
app.use("/api/upload",  uploadRoutes);

// eslint-disable-next-line @typescript-eslint/no-unused-vars
app.use((error: unknown, req: Request, res: Response, next: NextFunction) => {
    console.error(error);
    let errorMessage = "An unknown error occurred";
    let statusCode = 500;
    if (isHttpError(error)){
        statusCode = error.status;
        errorMessage = error.message;
    } 
    res.status(statusCode).json({ error: errorMessage });
});

//use for reference and testing purposes
app.use((req, res, next) => {
  next(createHttpError(404, "Endpoint not found"));
});
export default app;

upload.ts

import "dotenv/config";
import express from "express";
import cors from 'cors';
import * as UploadController from "../controllers/uploadC";
import multer from 'multer';

const router = express.Router();
router.use(cors());
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });

router.post('/', upload.array('file'), UploadController.uploadFile);

router.post('/chat',  UploadController.chatWithUser);

export default router;

uploadC.ts

import fs from "fs";
import path from "path";
import axios from "axios";
import FormData from "form-data";
import { NextFunction, Request, Response } from "express";

const MAX_CHUNK_SIZE = 25 * 1024 * 1024;

export const uploadFile = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  
  const files = req.files as Express.Multer.File[];
  if (!files || files.length === 0) {
    return res.status(400).json({ error: "No files uploaded." });
  }

  try {
    let transcript = "";
    for (let i = 0; i < files.length; i++) {
      const file = files[i];
      const fileSize = file.size;

      console.log("size is: " + fileSize);

      let datasize: number = fileSize;

      if (fileSize > MAX_CHUNK_SIZE) {
        const chunks = Math.ceil(fileSize / MAX_CHUNK_SIZE);
        const chunkSize = Math.ceil(fileSize / chunks);
        const tempFilePaths: string[] = [];

        for (let j = 0; j < chunks; j++) {
          const start = j * chunkSize;
          const end = Math.min(start + chunkSize, fileSize);
          console.log("start: " + start + " end: " + end);

          const chunkData = file.buffer.subarray(start, end);
          console.log("chunk size: " + chunkData.length);
          datasize = datasize - chunkData.length;
          console.log("datasize left: " + datasize);

          const tempFilePath = path.join(__dirname, `../result/temp_audio_${i + 1}_${j + 1}.mp4`);

          fs.writeFileSync(tempFilePath, chunkData);
          tempFilePaths.push(tempFilePath);
        }

        for (const tempFilePath of tempFilePaths) {
          console.log("getTranscript called");
          console.log("File path:", tempFilePath);
          const fileTranscript = await getTranscript(tempFilePath);
          transcript += fileTranscript;
          const transcriptFilePath = path.join(__dirname, "../result/transcript.txt");
          fs.writeFileSync(transcriptFilePath, transcript);
        }

        for (const tempFilePath of tempFilePaths) {
          fs.unlinkSync(tempFilePath);
        }
      } else {
        const filePath = path.join(__dirname, `../result/temp_audio_${i + 1}.mp4`);
        fs.writeFileSync(filePath, file.buffer);
        const fileTranscript = await getTranscript(filePath);
        transcript += fileTranscript;
      }
    }

    const transcriptFilePath = path.join(__dirname, "../result/transcript.txt");
    fs.writeFileSync(transcriptFilePath, transcript);

    const analysis = await getChatGPTAnalysis(transcript);

    res.status(200).json({ transcript, transcriptFilePath, analysis });
  } catch (error) {
    next(error);
    res.status(500).json({ error: "Error processing files" });
  }
};

export const chatWithUser = async (
  req: Request,
  res: Response,
  next: NextFunction) => {
  console.log("trying to chat")
  try{
    const transcriptFilePath = path.join(__dirname, "../result/transcript.txt");
    const transcript = fs.readFileSync(transcriptFilePath, "utf8");

    const data = {
      model: "gpt-3.5-turbo",
      messages: [
        { role: "system", content: "You are a helpful assistant." },
        { role: "user", content: "Please answer the question based on the transcripts, which is form video or audio:" },
        { role: "user", content: transcript },
        { role: "user", content: req.body.question },
      ],
    };

    const response = await axios.post(
      "https://api.openai.com/v1/chat/completions",
      data,
      {
        headers: {
          Authorization: `Bearer ${process.env.OPEN_AI_KEY}`,
          'Content-Type': 'application/json',
        },
      }
    );

    const answer = response.data.choices[0].message.content;
    res.status(200).json({ answer })
  }
  catch(error){
    next(error);
    res.status(500).json({ error: "Error passing chat" });
  }


}

async function getTranscript(audioFilePath: string) {
  try {
    const formData = new FormData();
    formData.append("file", fs.createReadStream(audioFilePath));
    formData.append("model", "whisper-1");
    formData.append("response_format", "vtt");

    const response = await axios.post(
      "https://api.openai.com/v1/audio/translations",
      formData,
      {
        headers: {
          Authorization: `Bearer ${process.env.OPEN_AI_KEY}`,
          ...formData.getHeaders(),
        },
      }
    );

    return response.data;
  } catch (error) {
    console.error("Error in getTranscript:", error);
    throw new Error("Error in getTranscript");
  }
}

async function getChatGPTAnalysis(transcript: string) {
  try {
    const data = {
      model: "gpt-3.5-turbo",
      messages: [
        { role: "system", content: "You are a helpful assistant." },
        { role: "user", content: "Please summarize the following transcript:" },
        { role: "user", content: transcript },
      ],
    };

    const response = await axios.post(
      "https://api.openai.com/v1/chat/completions",
      data,
      {
        headers: {
          Authorization: `Bearer ${process.env.OPEN_AI_KEY}`,
          'Content-Type': 'application/json',
        },
      }
    );

    const summary = response.data.choices[0].message.content;
    return summary;
  } catch (error) {
    console.error("Error in getChatGPTAnalysis:", error);
    throw new Error("Error in getChatGPTAnalysis");
  }
}

错误修复方案

1. AxiosError: Request failed with status code 400(Whisper API请求失败)

核心原因及修复:

  • 错误的API端点:当前调用的是/v1/audio/translations(翻译接口,用于将其他语言转为英文),如果你的文件是中文或需保留原语言转录,应该用/v1/audio/transcriptions(转录接口)。修改getTranscript中的请求URL:
    const response = await axios.post(
      "https://api.openai.com/v1/audio/transcriptions", // 替换为转录接口
      formData,
      {
        headers: {
          Authorization: `Bearer ${process.env.OPEN_AI_KEY}`,
          ...formData.getHeaders(),
        },
      }
    );
    
  • 文件拆分方式错误:直接切割二进制文件会破坏音视频的编码结构,导致API无法识别。改用fluent-ffmpeg这类专业媒体处理库分割文件:
    import ffmpeg from 'fluent-ffmpeg';
    
    // 示例:按时长分割视频
    const splitMedia = (inputPath: string, outputPrefix: string, chunkDuration: number) => {
      return new Promise((resolve) => {
        ffmpeg(inputPath)
          .outputOptions('-f', 'segment')
          .outputOptions('-segment_time', chunkDuration.toString())
          .outputOptions('-c', 'copy')
          .output(`${outputPrefix}_%d.mp4`)
          .on('end', resolve)
          .run();
      });
    };
    
  • 检查API密钥:确认process.env.OPEN_AI_KEY已正确加载,且账户有足够额度调用Whisper API。

2. Error: Cannot set headers after they are sent to the client

核心原因及修复:

  • 重复发送响应:在uploadFile和chatWithUser的catch块中,同时调用了next(error)和res.status(500).json(...),导致Express尝试两次发送响应头。移除其中一种方式,推荐交给全局错误处理器处理:
    // 修改uploadFile的catch块
    catch (error) {
      next(error);
      // 移除 res.status(500).json({ error: "Error processing files" });
    }
    
    // 修改chatWithUser的catch块
    catch(error){
      next(error);
      // 移除 res.status(500).json({ error: "Error passing chat" });
    }
    
  • 调整中间件顺序:将404中间件移到路由之后、全局错误处理器之前,避免响应头冲突:
    // 路由定义
    app.use("/api/users", userRoutes);
    app.use("/api/scripts", scriptRoutes);
    app.use("/api/upload",  uploadRoutes);
    
    // 404中间件
    app.use((req, res, next) => {
      next(createHttpError(404, "Endpoint not found"));
    });
    
    // 全局错误处理器
    app.use((error: unknown, req: Request, res: Response, next: NextFunction) => {
      console.error(error);
      let errorMessage = "An unknown error occurred";
      let statusCode = 500;
      if (isHttpError(error)){
        statusCode = error.status;
        errorMessage = error.message;
      } 
      res.status(statusCode).json({ error: errorMessage });
    });
    

内容的提问来源于stack exchange,提问作者940

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 17:12:02