[英]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.