首先,要给vin-ni的
Google-Cloud-Speech-Node-Socket-Playground项目点赞,我的解决方案很大一部分是参考了他的项目。但我还需要对其进行一些改进以适应我的React应用程序,因此我分享了我所做的一些更改。
我的解决方案由四个部分组成,其中两个在前端,另外两个在后端。
我的前端解决方案包括以下两个部分:
- 一个实用程序文件,用于访问我的麦克风,将音频流传输到后端,从后端检索数据,每当从后端接收到数据时运行回调函数,然后在完成流式传输或后端抛出错误时清理自身。
- 一个包装了我的React功能的麦克风组件。
我的后端解决方案包括以下两个部分:
- 一个处理实际语音识别流的实用程序文件
- 我的
main.js
文件
(这些部分不需要被严格分开;我们的
main.js
文件已经足够庞大了。)
大部分代码只是选录,但我的实用程序将完整展示,因为我在所有涉及到的阶段都遇到了很多问题。我的前端实用程序文件如下:
let bufferSize = 2048,
AudioContext,
context,
processor,
input,
globalStream;
const constraints = {
audio: true,
video: false
};
let AudioStreamer = {
initRecording: function(onData, onError) {
socket.emit('startGoogleCloudStream', {
config: {
encoding: 'LINEAR16',
sampleRateHertz: 16000,
languageCode: 'en-US',
profanityFilter: false,
enableWordTimeOffsets: true
},
interimResults: true
});
AudioContext = window.AudioContext || window.webkitAudioContext;
context = new AudioContext();
processor = context.createScriptProcessor(bufferSize, 1, 1);
processor.connect(context.destination);
context.resume();
var handleSuccess = function (stream) {
globalStream = stream;
input = context.createMediaStreamSource(stream);
input.connect(processor);
processor.onaudioprocess = function (e) {
microphoneProcess(e);
};
};
navigator.mediaDevices.getUserMedia(constraints)
.then(handleSuccess);
if(onData) {
socket.on('speechData', (data) => {
onData(data);
});
}
socket.on('googleCloudStreamError', (error) => {
if(onError) {
onError('error');
}
closeAll();
});
},
stopRecording: function() {
socket.emit('endGoogleCloudStream', '');
closeAll();
}
}
export default AudioStreamer;
function microphoneProcess(e) {
var left = e.inputBuffer.getChannelData(0);
var left16 = convertFloat32ToInt16(left);
socket.emit('binaryAudioData', left16);
}
function convertFloat32ToInt16(buffer) {
let l = buffer.length;
let buf = new Int16Array(l / 3);
while (l--) {
if (l % 3 === 0) {
buf[l / 3] = buffer[l] * 0xFFFF;
}
}
return buf.buffer
}
function closeAll() {
socket.off('speechData');
socket.off('googleCloudStreamError');
let tracks = globalStream ? globalStream.getTracks() : null;
let track = tracks ? tracks[0] : null;
if(track) {
track.stop();
}
if(processor) {
if(input) {
try {
input.disconnect(processor);
} catch(error) {
console.warn('Attempt to disconnect input failed.')
}
}
processor.disconnect(context.destination);
}
if(context) {
context.close().then(function () {
input = null;
processor = null;
context = null;
AudioContext = null;
});
}
}
除了getUserMedia配置(本身有点棘手)之外,此代码的主要亮点是处理器的onaudioprocess回调通过将数据转换为Int16后向套接字发出speechData事件。我在这里对我的链接参考进行了一些主要更改,包括用回调函数(由我的React组件使用)替换所有实际更新DOM的功能,并添加了一些源中未包含的错误处理。
然后,我只需使用以下内容在我的React组件中访问它:
onStart() {
this.setState({
recording: true
});
if(this.props.onStart) {
this.props.onStart();
}
speechToTextUtils.initRecording((data) => {
if(this.props.onUpdate) {
this.props.onUpdate(data);
}
}, (error) => {
console.error('Error when recording', error);
this.setState({recording: false});
});
}
onStop() {
this.setState({recording: false});
speechToTextUtils.stopRecording();
if(this.props.onStop) {
this.props.onStop();
}
}
我将我的实际数据处理程序作为属性传递给了这个组件。
然后在后端,我的服务处理了main.js
中的三个主要事件:
socket.on('startGoogleCloudStream', function(request) {
speechToTextUtils.startRecognitionStream(socket, GCSServiceAccount, request);
});
socket.on('binaryAudioData', function(data) {
speechToTextUtils.receiveData(data);
});
socket.on('endGoogleCloudStream', function() {
speechToTextUtils.stopRecognitionStream();
});
我的 speechToTextUtils 看起来像这样:
const speech = require('@google-cloud/speech');
let speechClient = null;
let recognizeStream = null;
module.exports = {
startRecognitionStream: function (client, GCSServiceAccount, request) {
if(!speechClient) {
speechClient = new speech.SpeechClient({
projectId: 'Insert your project ID here',
credentials: GCSServiceAccount
});
}
recognizeStream = speechClient.streamingRecognize(request)
.on('error', (err) => {
console.error('Error when processing audio: ' + (err && err.code ? 'Code: ' + err.code + ' ' : '') + (err && err.details ? err.details : ''));
client.emit('googleCloudStreamError', err);
this.stopRecognitionStream();
})
.on('data', (data) => {
client.emit('speechData', data);
if (data.results[0] && data.results[0].isFinal) {
this.stopRecognitionStream();
this.startRecognitionStream(client, GCSServiceAccount, request);
}
});
},
stopRecognitionStream: function () {
if (recognizeStream) {
recognizeStream.end();
}
recognizeStream = null;
},
receiveData: function (data) {
if (recognizeStream) {
recognizeStream.write(data);
}
}
};
再次说明,您并不一定需要这个工具文件,根据您获取凭据的方式,您可以将speechClient
作为常量放在文件顶部。这只是我实现的方式。
最后,以上内容应该足以让您开始使用了。我鼓励您在重用或修改代码之前尽力理解它,因为它可能对您来说并不是“开箱即用”的,但与我找到的所有其他来源不同,这应该至少让您开始涉及项目的所有阶段。我希望这个答案能够防止其他人像我一样遭受痛苦。