使用MediaRecorder录制音频调用Google Speech-to-Text返回空转录问题
问题:MediaRecorder录制音频调用Google Speech-to-Text API返回空转录结果
我基于Next.js/React开发语音转文字功能,使用Google Speech-to-Text API。目前遇到的问题是:用浏览器MediaRecorder API录制的音频,调用API后返回空转录结果;但用Audacity录制的音频能正常得到转录内容。
客户端代码:
const startRecording = () => { navigator.mediaDevices .getUserMedia({ audio: true }) .then((stream) => { const recorder = new MediaRecorder(stream, { mimeType: "audio/webm; codecs=opus", bitsPerSecond: 128000, sampleRate: 48000, echoCancellation: true, noiseSuppression: true, channelCount: 1, }); const chunks = []; recorder.addEventListener("dataavailable", (event) => { chunks.push(event.data); }); recorder.addEventListener("stop", () => { const blob = new Blob(chunks, { type: "audio/wav" }); const url = URL.createObjectURL(blob); setAudioUrl(url); setRecording(false); setAudioBlob(blob); // Update the audioBlob state variable }); recorder.start(); setMediaRecorder(recorder); setRecording(true); }) .catch((error) => { console.log(error); }); };
服务端代码:
async function transcribeContextClasses() { const file = fs.readFileSync("public/audio/1680169074745_audio.wav"); const audioBytes = file.toString("base64"); const audio = { content: audioBytes, }; const speechContext = { phrases: ["$TIME"], }; const config = { encoding: "LINEAR16", sampleRateHertz: 48000, languageCode: "en-US", speechContexts: [speechContext], }; const request = { config: config, audio: audio, }; const [response] = await speechClient.recognize(request); const transcription = response.results .map((result) => result.alternatives[0].transcript) .join("\n"); console.log(`Transcription: ${transcription}`); }
问题原因
你在客户端犯了一个关键错误:MediaRecorder实际录制的是Opus编码的WebM音频,但停止录制时只是把Blob的type标记为audio/wav,并没有真正将音频编码转换为LINEAR16(PCM编码的WAV格式)。而Google Speech-to-Text API的配置中encoding: "LINEAR16",它无法解析Opus编码的音频,因此返回空转录结果。
Audacity导出的是标准LINEAR16编码的WAV文件,完全符合API的要求,所以能正常识别。
解决方案
需要在客户端将录制的Opus/WebM音频转换为LINEAR16编码的WAV格式,再传给服务端。可以通过Web Audio API实现编码转换:
修正后的客户端代码:
// 音频格式转换函数:Opus/WebM -> LINEAR16 WAV const convertOpusToLinear16Wav = async (opusBlob) => { return new Promise((resolve, reject) => { const audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 48000 }); const fileReader = new FileReader(); fileReader.onload = async (e) => { try { const arrayBuffer = e.target.result; const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); // 提取16位PCM数据 const length = audioBuffer.length * 2; // 单声道,每个采样占2字节 const buffer = new ArrayBuffer(length); const view = new DataView(buffer); let offset = 0; const channelData = audioBuffer.getChannelData(0); for (const sample of channelData) { // 将浮点数采样转换为16位整数 const intSample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF; view.setInt16(offset, intSample, true); // 小端字节序 offset += 2; } // 生成标准WAV文件头 const wavHeader = createWavHeader(48000, 1, length); const wavBlob = new Blob([wavHeader, view], { type: "audio/wav" }); resolve(wavBlob); } catch (err) { reject(err); } }; fileReader.onerror = reject; fileReader.readAsArrayBuffer(opusBlob); }); }; // 生成WAV文件头的辅助函数 const createWavHeader = (sampleRate, channels, dataByteLength) => { const buffer = new ArrayBuffer(44); const view = new DataView(buffer); // RIFF块 view.setUint8(0, 82); view.setUint8(1, 73); view.setUint8(2, 70); view.setUint8(3, 70); view.setUint32(4, dataByteLength + 36, true); // WAVE块 view.setUint8(8, 87); view.setUint8(9, 65); view.setUint8(10, 86); view.setUint8(11, 69); // fmt子块 view.setUint8(12, 102); view.setUint8(13, 109); view.setUint8(14, 116); view.setUint8(15, 32); view.setUint32(16, 16, true); // PCM格式 view.setUint16(20, 1, true); // 音频格式(PCM=1) view.setUint16(22, channels, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * channels * 2, true); // 字节率 view.setUint16(32, channels * 2, true); // 块对齐 view.setUint16(34, 16, true); // 位深 // data子块 view.setUint8(36, 100); view.setUint8(37, 97); view.setUint8(38, 116); view.setUint8(39, 97); view.setUint32(40, dataByteLength, true); return buffer; }; // 修改录制停止后的处理逻辑 const startRecording = () => { navigator.mediaDevices .getUserMedia({ audio: true }) .then((stream) => { const recorder = new MediaRecorder(stream, { mimeType: "audio/webm; codecs=opus", bitsPerSecond: 128000, echoCancellation: true, noiseSuppression: true, channelCount: 1, }); const chunks = []; recorder.addEventListener("dataavailable", (event) => { chunks.push(event.data); }); recorder.addEventListener("stop", async () => { const opusBlob = new Blob(chunks, { type: "audio/webm; codecs=opus" }); try { // 转换为LINEAR16编码的WAV const wavBlob = await convertOpusToLinear16Wav(opusBlob); const url = URL.createObjectURL(wavBlob); setAudioUrl(url); setRecording(false); setAudioBlob(wavBlob); // 保存转换后的WAV文件 } catch (err) { console.error("音频转换失败:", err); } }); recorder.start(); setMediaRecorder(recorder); setRecording(true); }) .catch((error) => { console.log(error); }); };
关键修改点:
- 新增
convertOpusToLinear16Wav函数:利用Web Audio API解码Opus音频,将浮点数采样转换为16位PCM整数数据。 - 新增
createWavHeader函数:生成符合标准的WAV文件头,确保输出的音频是LINEAR16编码格式。 - 调整录制停止逻辑:保留原始Opus格式Blob,转换为标准WAV后再更新状态变量。
处理后,传给服务端的音频完全符合Google Speech-to-Text API的要求,即可正常返回转录结果。
内容的提问来源于stack exchange,提问作者noyruto88
相关产品推荐
相关产品推荐

