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

Next.js调用Azure文本转语音API出现ERR_REQUEST_RANGE_NOT_SATISFIABLE错误

问题描述

我使用Microsoft Azure文本转语音API,需求是点击按钮时在浏览器播放合成语音。通过Next.js API路由向Azure发起请求,客户端按钮调用该路由播放音频时出现以下错误:

blob:http://localhost:3000/aab03e2a-14c1-48a7-9dae-4eac158325a5:1
GET blob:http://localhost:3000/aab03e2a-14c1-48a7-9dae-4eac158325a5
net::ERR_REQUEST_RANGE_NOT_SATISFIABLE

localhost/:1 Uncaught (in promise) DOMException: Failed to load because no supported source was found.

相关代码如下:

/pages/api/synthesizeSpeech.tsx

import { NextApiRequest, NextApiResponse } from "next";
import * as sdk from "microsoft-cognitiveservices-speech-sdk";

export default async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method !== "POST") {
    return res.status(405).end();
  }

  const speechConfig = sdk.SpeechConfig.fromSubscription(process.env.SPEECH_KEY, process.env.SPEECH_REGION);
  speechConfig.speechSynthesisVoiceName = "en-US-JennyNeural";

  // Create a pull stream
  const pullStream = sdk.AudioOutputStream.createPullStream();

  const audioConfig = sdk.AudioConfig.fromStreamOutput(pullStream);

  const synthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig);

  const text = req.body.text;

  synthesizer.speakTextAsync(
    text,
    (result) => {
      if (result.reason === sdk.ResultReason.SynthesizingAudioCompleted) {
        // Set the appropriate headers for audio data
        res.setHeader("Content-Type", "audio/wav");
        res.setHeader("Content-Disposition", "attachment; filename=speech.wav");

        // Read the audio data from the pull stream and write it to the response
        const audioBuffer = [];
        const bufferSize = 10240;
        const buffer = new ArrayBuffer(bufferSize);
        let bytesRead = 0;

        do {
          // @ts-ignore
          bytesRead = pullStream.read(buffer);
          for (let i = 0; i < bytesRead; i++) {
            // @ts-ignore
            audioBuffer.push(buffer[i]);
          }
        } while (bytesRead > 0);

        res.status(200).end(Buffer.from(audioBuffer));
      } else {
        res.status(500).json({
          error: `Speech synthesis canceled, ${result.errorDetails}\nDid you set the speech resource key and region values?`,
        });
      }
      synthesizer.close();
    },
    (err) => {
      res.status(500).json({ error: `Error - ${err}` });
      synthesizer.close();
    }
  );
};

pages/demo.tsx

const ButtonPanel = () => {
  const handleSynthesize = async (text: string) => {
    alert(text);
    try {
      const response = await fetch("/api/synthesizeSpeech", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ text }),
      });

      if (!response.ok) {
        throw new Error("Failed to synthesize speech");
      }

      const blob = await response.blob();
      const audioUrl = URL.createObjectURL(blob);
      const audio = new Audio(audioUrl);
      audio.play();
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <footer className="m-4 mt-0 w-[calc(100vw-2rem)] rounded-b-lg border-t-2 border-gray-200 bg-white shadow-lg">
      <div className="flex items-center justify-center space-x-4 p-4">
        <button
          onClick={() => {
            handleSynthesize("hello this is a test test hello");
          }}
          className="btn-solid w-32 disabled:cursor-not-allowed disabled:bg-gray-100"
        >
          <FaCircleArrowUp size={28} />
        </button>
      </div>
    </footer>
  );
}
解决方案

问题根源

  • 音频数据读取错误:手动读取pull stream并转换数组的方式会破坏WAV文件格式,导致浏览器无法识别。
  • 缺少必要响应头:未设置Content-Length,浏览器无法处理音频范围请求,触发ERR_REQUEST_RANGE_NOT_SATISFIABLE错误。
  • 异步逻辑不规范:Next.js API路由中使用回调式异步方法,未正确处理Promise,可能导致响应提前终止。

修复代码

1. 修正API路由(/pages/api/synthesizeSpeech.tsx)

直接使用SDK返回的完整音频Buffer,规范异步处理并补充响应头:

import { NextApiRequest, NextApiResponse } from "next";
import * as sdk from "microsoft-cognitiveservices-speech-sdk";

export default async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method !== "POST") {
    return res.status(405).end();
  }

  let synthesizer: sdk.SpeechSynthesizer | undefined;
  try {
    const speechConfig = sdk.SpeechConfig.fromSubscription(
      process.env.SPEECH_KEY!,
      process.env.SPEECH_REGION!
    );
    speechConfig.speechSynthesisVoiceName = "en-US-JennyNeural";

    synthesizer = new sdk.SpeechSynthesizer(speechConfig, undefined);
    const text = req.body.text;

    // 用Promise封装SDK异步方法,适配Next.js async/await
    const result = await new Promise<sdk.SpeechSynthesisResult>((resolve, reject) => {
      synthesizer!.speakTextAsync(text, resolve, reject);
    });

    if (result.reason === sdk.ResultReason.SynthesizingAudioCompleted) {
      const audioBuffer = result.audioData;
      // 设置必要响应头
      res.setHeader("Content-Type", "audio/wav");
      res.setHeader("Content-Length", audioBuffer.length.toString());
      // 设为inline让浏览器直接播放而非下载
      res.setHeader("Content-Disposition", "inline; filename=speech.wav");

      res.status(200).send(audioBuffer);
    } else {
      res.status(500).json({
        error: `语音合成取消:${result.errorDetails}。请检查你的语音服务密钥和区域配置。`,
      });
    }
  } catch (err) {
    res.status(500).json({ error: `合成失败:${(err as Error).message}` });
  } finally {
    synthesizer?.close();
  }
};

2. 优化客户端播放逻辑(pages/demo.tsx)

指定Blob类型并添加错误、内存回收处理:

const ButtonPanel = () => {
  const handleSynthesize = async (text: string) => {
    try {
      const response = await fetch("/api/synthesizeSpeech", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ text }),
      });

      if (!response.ok) {
        throw new Error(`请求失败,状态码:${response.status}`);
      }

      const blob = await response.blob();
      // 明确指定Blob的MIME类型
      const audioBlob = new Blob([blob], { type: "audio/wav" });
      const audioUrl = URL.createObjectURL(audioBlob);
      
      const audio = new Audio(audioUrl);
      
      // 添加错误监听
      audio.addEventListener("error", (e) => {
        console.error("音频播放错误:", e);
      });
      
      await audio.play();
      
      // 播放结束后释放Blob URL,避免内存泄漏
      audio.addEventListener("ended", () => {
        URL.revokeObjectURL(audioUrl);
      });
    } catch (error) {
      console.error("合成或播放失败:", error);
    }
  };

  return (
    <footer className="m-4 mt-0 w-[calc(100vw-2rem)] rounded-b-lg border-t-2 border-gray-200 bg-white shadow-lg">
      <div className="flex items-center justify-center space-x-4 p-4">
        <button
          onClick={() => handleSynthesize("hello this is a test test hello")}
          className="btn-solid w-32 disabled:cursor-not-allowed disabled:bg-gray-100"
        >
          <FaCircleArrowUp size={28} />
        </button>
      </div>
    </footer>
  );
};

关键说明

  • 直接使用result.audioData获取SDK生成的完整音频Buffer,避免手动读取流导致的数据损坏。
  • 添加Content-Length头,让浏览器知晓音频文件大小,支持范围请求。
  • 将Content-Disposition设为inline,告知浏览器直接播放音频而非触发下载。
  • 用Promise封装SDK异步回调,适配Next.js的async/await模式,避免回调逻辑混乱。
  • 客户端明确指定Blob的MIME类型,并在播放结束后释放Blob URL,减少内存占用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 21:35:59