繁体   English   中英

Google Cloud Speech-to-Text 无法在某些 iDevice 上正确转录流式音频

[英]Google Cloud Speech-to-Text doesn't transcribe streamed audio correctly on some iDevices

在过去的几周里,我使用实时流音频实现了 Google Cloud Speech to Text API。 虽然最初一切看起来都很好,但我最近在更多设备上测试了该产品,并在某些 iDevice 上发现了一些真正奇怪的违规行为。 首先,这里是相关的代码片段:

前端(反应组件)

constructor(props) {
  super(props);
  this.audio = props.audio;
  this.socket = new SocketClient();
  this.bufferSize = 2048;
}

/**
* Initializes the users microphone and the audio stream.
*
* @return {void}
*/
startAudioStream = async () => {
  const AudioContext = window.AudioContext || window.webkitAudioContext;
  this.audioCtx = new AudioContext();
  this.processor = this.audioCtx.createScriptProcessor(this.bufferSize, 1, 1);
  this.processor.connect(this.audioCtx.destination);
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  /* Debug through instant playback:
  this.audio.srcObject = stream;
  this.audio.play();
  return; */

  this.globalStream = stream;
  this.audioCtx.resume();
  this.input = this.audioCtx.createMediaStreamSource(stream);
  this.input.connect(this.processor);

  this.processor.onaudioprocess = (e) => {
    this.microphoneProcess(e);
  };
  this.setState({ streaming: true });
}

/**
 * Processes microphone input and passes it to the server via the open socket connection.
 *
 * @param {AudioProcessingEvent} e
 * @return {void}
 */
microphoneProcess = (e) => {
  const { speaking, askingForConfirmation, askingForErrorConfirmation } = this.state;
  const left = e.inputBuffer.getChannelData(0);
  const left16 = Helpers.downsampleBuffer(left, 44100, 16000);
  if (speaking === false) {
    this.socket.emit('stream', {
      audio: left16,
      context: askingForConfirmation || askingForErrorConfirmation ? 'zip_code_yes_no' : 'zip_code',
      speechContext: askingForConfirmation || askingForErrorConfirmation ? ['ja', 'nein', 'ne', 'nö', 'falsch', 'neu', 'korrektur', 'korrigieren', 'stopp', 'halt', 'neu'] : ['$OPERAND'],
    });
  }
}

助手(下采样缓冲区)

/**
 * Downsamples a given audio buffer from sampleRate to outSampleRate.
 * @param {Array} buffer The audio buffer to downsample.
 * @param {number} sampleRate The original sample rate.
 * @param {number} outSampleRate The new sample rate.
 * @return {Array} The downsampled audio buffer.
 */
static downsampleBuffer(buffer, sampleRate, outSampleRate) {
  if (outSampleRate === sampleRate) {
    return buffer;
  }
  if (outSampleRate > sampleRate) {
    throw new Error('Downsampling rate show be smaller than original sample rate');
  }
  const sampleRateRatio = sampleRate / outSampleRate;
  const newLength = Math.round(buffer.length / sampleRateRatio);
  const result = new Int16Array(newLength);
  let offsetResult = 0;
  let offsetBuffer = 0;
  while (offsetResult < result.length) {
    const nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
    let accum = 0;
    let count = 0;
    for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
      accum += buffer[i];
      count++;
    }

    result[offsetResult] = Math.min(1, accum / count) * 0x7FFF;
    offsetResult++;
    offsetBuffer = nextOffsetBuffer;
  }
  return result.buffer;
}

后端(套接字服务器)

io.on('connection', (socket) => {
  logger.debug('New client connected');
  const speechClient = new SpeechService(socket);

  socket.on('stream', (data) => {
    const audioData = data.audio;
    const context = data.context;
    const speechContext = data.speechContext;
    speechClient.transcribe(audioData, context, speechContext);
  });
});

后端(语音客户端/转录 Function 数据发送到 GCloud)

async transcribe(data, context, speechContext, isFile = false) {
  if (!this.recognizeStream) {
    logger.debug('Initiating new Google Cloud Speech client...');
    let waitingForMoreData = false;
    // Create new stream to the Google Speech client
    this.recognizeStream = this.speechClient
      .streamingRecognize({
        config: {
          encoding: 'LINEAR16',
          sampleRateHertz: 16000,
          languageCode: 'de-DE',
          speechContexts: speechContext ? [{ phrases: speechContext }] : undefined,
        },
        interimResults: false,
        singleUtterance: true,
      })
      .on('error', (error) => {
        if (error.code === 11) {
          this.recognizeStream.destroy();
          this.recognizeStream = null;
          return;
        }
        this.socket.emit('error');
        this.recognizeStream.destroy();
        this.recognizeStream = null;
        logger.error(`Received error from Google Cloud Speech client: ${error.message}`);
      })
      .on('data', async (gdata) => {
        if ((!gdata.results || !gdata.results[0]) && gdata.speechEventType === 'END_OF_SINGLE_UTTERANCE') {
          logger.debug('Received END_OF_SINGLE_UTTERANCE - waiting 300ms for more data before restarting stream');
          waitingForMoreData = true;
          setTimeout(() => {
            if (waitingForMoreData === true) {
              // User was silent for too long - restart stream
              this.recognizeStream.destroy();
              this.recognizeStream = null;
            }
          }, 300);
          return;
        }
        waitingForMoreData = false;
        const transcription = gdata.results[0].alternatives[0].transcript;
        logger.debug(`Transcription: ${transcription}`);

        // Emit transcription and MP3 file of answer
        this.socket.emit('transcription', transcription);
        const filename = await ttsClient.getAnswerFromTranscription(transcription, 'fairy', context); // TODO-Final: Dynamic character
        if (filename !== null) this.socket.emit('speech', `${config.publicScheme}://${config.publicHost}:${config.publicPort}/${filename}`);

        // Restart stream
        if (this.recognizeStream) this.recognizeStream.destroy();
        this.recognizeStream = null;
      });
  }
  // eslint-disable-next-line security/detect-non-literal-fs-filename
  if (isFile === true) fs.createReadStream(data).pipe(this.recognizeStream);
  else this.recognizeStream.write(data);
}

现在,在我测试的设备中,行为差异很大。 我最初是在使用 Google Chrome 作为浏览器的 iMac 2017 上开发的。 奇迹般有效。 然后,在 iPhone 11 Pro 和 iPad Air 4 上进行了测试,同时在 Safari 和全屏 web 应用程序上进行了测试。 再次,像魅力一样工作。

之后,我尝试使用 iPad Pro 12.9" 2017。突然之间,Google Cloud 有时根本不返回转录内容,有时它返回的内容只是使用了非常多的幻想,听起来像是实际口述的文本。同样的行为在 iPad 5 和 iPhone 6 Plus 上。

我真的不知道 go 从这里到哪里。 到目前为止,我至少读到的是 iPhone 6s(不幸的是不知道 iPad)的硬件采样率从 44.1khz 更改为 48khz。 所以我想,这可能是它,在代码中到处玩弄采样率,没有成功。 此外,我注意到我的带有 Google Chrome 的 iMac 也可以在 44.1khz 上运行,就像无法进行转录的“旧”iPad 一样。 同样,新 iPad 以 48khz 运行 - 在这里一切正常。 所以这不可能。

我也注意到了:当我将一些 AirPods 连接到“损坏”的设备并将它们用作音频输入时,一切都恢复了。 所以这一定与这些设备的内部麦克风的处理有关。 我只是不知道究竟是什么。

谁能引导我走向正确的方向? 在音频和麦克风方面,这些设备世代之间发生了哪些变化?

更新 1 :我现在实现了一个快速的 function,它使用 node-wav 将来自前端的流式 PCM 数据写入后端的文件。 我想,我现在越来越接近了——在语音识别变得疯狂的设备上,我听起来像花栗鼠(非常高音)。 我还注意到二进制音频数据的流动速度比在一切正常的设备上慢。 所以这可能与采样/比特率、编码或其他有关。 不幸的是,我不是音频专家,所以不知道下一步该做什么。

更新 2 :经过大量试用结束错误后,我发现如果我在 Google Cloud RecognizeConfig中将采样率设置为 9500 到 10000 左右,一切正常。 当我将其设置为 node-wav 文件 output 的采样率时,听起来也不错 如果我再次将“传出”采样率重置为 GCloud 到 16000,并将音频输入从 44100 降低到大约 25000 而不是前端的 16000(请参阅“microphoneProcess”函数中的“前端(React 组件)”),它的工作原理是出色地。 因此,采样率差异似乎存在某种 ~0.6 因素。 但是,我仍然不知道这种行为来自哪里:工作 iMac 上的 Chrome 和“损坏”iPad 上的audioContext.sampleRate的 audioContext.sampleRate 为 44100。因此,当我在代码中将它们下采样到 16000 时,我'会假设两者都应该工作,而只有 iMac 工作。 似乎 iPad 在内部使用不同的采样率?

经过大量的反复试验,我找到了问题(和解决方案)。 似乎“较旧”的 iDevice 型号(例如 2017 iPad Pro)具有一些奇怪的特性,可以自动将麦克风采样率调整为播放音频的速率。 即使这些设备的硬件采样率设置为 44.1khz,只要播放一些音频,采样率就会发生变化。 这可以通过以下方式观察到:

const audioCtx = new webkitAudioContext();
console.log(`Current sample rate: ${audioCtx.sampleRate}`); // 44100
const audio = new Audio();
audio.src = 'some_audio.mp3';
await audio.play();
console.log(`Current sample rate: ${audioCtx.sampleRate}`); // Sample rate of the played audio

在我的情况下,我在打开语音转录套接字之前播放了一些来自 Google Text-to-Speech 的合成语音。 这些声音文件的采样率为 24khz - 正是 Google Cloud 接收我的音频输入的采样率。

因此,解决方案是——无论如何我都应该做的事情——将所有内容下采样到 16khz(请参阅问题中的我的助手 function),但不是来自硬编码的 44.1khz,而是来自音频上下文的当前采样率。 所以我改变了我的microphoneProcess() function 像这样:

const left = e.inputBuffer.getChannelData(0);
const left16 = Helpers.downsampleBuffer(left, this.audioCtx.sampleRate, 16000);

结论:不要相信 Safari 的页面加载采样率。 它可能会改变。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM