如何通过 socket.io 将实时音频从浏览器流式传输到 Google Cloud Speech?
How do I stream live audio from the browser to Google Cloud Speech via socket.io?
我有一个基于 React 的应用程序的情况,我有一个输入,我也想允许语音输入。我可以让它仅与 Chrome 和 Firefox 兼容,所以我在考虑使用 getUserMedia。我知道我将使用 Google Cloud 的 Speech to Text API。但是,我有几点警告:
- 我希望它能够流式传输我的音频数据 live,而不仅仅是在我完成录制后。这意味着我发现的很多解决方案都不会很好地工作,因为保存文件然后将其发送到 Google Cloud Speech 是不够的。
- 我不信任我的 Google 云 API 信息的前端。相反,我已经在后端有一个服务 运行,它有我的凭据,我想将音频(实时)流式传输到该后端,然后从该后端流式传输到 Google 云,然后在我的成绩单返回前端时发出更新。
- 我已经使用 socket.io 连接到该后端服务,我想完全通过套接字来管理它,而不必使用 Binary.js 或类似的东西。
似乎没有关于如何执行此操作的好教程。我该怎么办?
首先,归功于应得的功劳:我这里的大量解决方案是通过引用 vin-ni 的 Google-Cloud-Speech-Node-Socket-Playground project 创建的。但是,我必须针对我的 React 应用程序对此进行一些调整,因此我将分享我所做的一些更改。
我这里的解决方案由四部分组成,两个在前端,两个在后端。
我的前端解决方案分为两部分:
- 一个实用程序文件,用于访问我的麦克风,将音频流式传输到后面
end,从后端取数据,运行每一个回调函数
从后端接收数据的时间,然后清理
在完成流式传输或后端抛出后
一个错误。
- 包裹我的 React 的麦克风组件
功能。
我的后端解决方案分为两部分:
- 处理实际语音识别流的实用程序文件
- 我的
main.js
文件
(这些不需要以任何方式分开;我们的 main.js
文件没有它就已经是一个庞然大物了。)
我的大部分代码将只是摘录,但我的实用程序将完整显示,因为我在涉及的所有阶段都遇到了很多问题。我的前端实用程序文件如下所示:
// Stream Audio
let bufferSize = 2048,
AudioContext,
context,
processor,
input,
globalStream;
//audioStream constraints
const constraints = {
audio: true,
video: false
};
let AudioStreamer = {
/**
* @param {function} onData Callback to run on data each time it's received
* @param {function} onError Callback to run on an error if one is emitted.
*/
initRecording: function(onData, onError) {
socket.emit('startGoogleCloudStream', {
config: {
encoding: 'LINEAR16',
sampleRateHertz: 16000,
languageCode: 'en-US',
profanityFilter: false,
enableWordTimeOffsets: true
},
interimResults: true // If you want interim results, set this to true
}); //init socket Google Speech Connection
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);
// Bind the data handler callback
if(onData) {
socket.on('speechData', (data) => {
onData(data);
});
}
socket.on('googleCloudStreamError', (error) => {
if(onError) {
onError('error');
}
// We don't want to emit another end stream event
closeAll();
});
},
stopRecording: function() {
socket.emit('endGoogleCloudStream', '');
closeAll();
}
}
export default AudioStreamer;
// Helper functions
/**
* Processes microphone data into a data stream
*
* @param {object} e Input from the microphone
*/
function microphoneProcess(e) {
var left = e.inputBuffer.getChannelData(0);
var left16 = convertFloat32ToInt16(left);
socket.emit('binaryAudioData', left16);
}
/**
* Converts a buffer from float32 to int16. Necessary for streaming.
* sampleRateHertz of 1600.
*
* @param {object} buffer Buffer being converted
*/
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
}
/**
* Stops recording and closes everything down. Runs on error or on stop.
*/
function closeAll() {
// Clear the listeners (prevents issue if opening and closing repeatedly)
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 后与数据进行套接字。我对上面链接参考的主要更改是用回调函数(由我的 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});
// No further action needed, as this already closes itself on error
});
}
onStop() {
this.setState({recording: false});
speechToTextUtils.stopRecording();
if(this.props.onStop) {
this.props.onStop();
}
}
(我将我的实际数据处理程序作为 prop 传递给了这个组件)。
然后在后端,我的服务处理了 main.js
中的三个主要事件:
// Start the stream
socket.on('startGoogleCloudStream', function(request) {
speechToTextUtils.startRecognitionStream(socket, GCSServiceAccount, request);
});
// Receive audio data
socket.on('binaryAudioData', function(data) {
speechToTextUtils.receiveData(data);
});
// End the audio stream
socket.on('endGoogleCloudStream', function() {
speechToTextUtils.stopRecognitionStream();
});
我的 speechToTextUtils 看起来像:
// Google Cloud
const speech = require('@google-cloud/speech');
let speechClient = null;
let recognizeStream = null;
module.exports = {
/**
* @param {object} client A socket client on which to emit events
* @param {object} GCSServiceAccount The credentials for our google cloud API access
* @param {object} request A request object of the form expected by streamingRecognize. Variable keys and setup.
*/
startRecognitionStream: function (client, GCSServiceAccount, request) {
if(!speechClient) {
speechClient = new speech.SpeechClient({
projectId: 'Insert your project ID here',
credentials: GCSServiceAccount
}); // Creates a client
}
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 end of utterance, let's restart stream
// this is a small hack. After 65 seconds of silence, the stream will still throw an error for speech length limit
if (data.results[0] && data.results[0].isFinal) {
this.stopRecognitionStream();
this.startRecognitionStream(client, GCSServiceAccount, request);
// console.log('restarted stream serverside');
}
});
},
/**
* Closes the recognize stream and wipes it
*/
stopRecognitionStream: function () {
if (recognizeStream) {
recognizeStream.end();
}
recognizeStream = null;
},
/**
* Receives streaming data and writes it to the recognizeStream for transcription
*
* @param {Buffer} data A section of audio data
*/
receiveData: function (data) {
if (recognizeStream) {
recognizeStream.write(data);
}
}
};
(同样,您并不严格需要此 util 文件,您当然可以将 speechClient
作为常量放在文件顶部,具体取决于您获取凭据的方式;这就是我的方式实现了。)
最后,这应该足以让您开始使用它。我鼓励您在重用或修改这段代码之前尽最大努力理解它,因为它可能对您不起作用 'out of the box',但与我发现的所有其他来源不同,这应该至少让您开始所有相关工作项目的阶段。我希望这个答案能防止其他人像我一样遭受痛苦。
我有一个基于 React 的应用程序的情况,我有一个输入,我也想允许语音输入。我可以让它仅与 Chrome 和 Firefox 兼容,所以我在考虑使用 getUserMedia。我知道我将使用 Google Cloud 的 Speech to Text API。但是,我有几点警告:
- 我希望它能够流式传输我的音频数据 live,而不仅仅是在我完成录制后。这意味着我发现的很多解决方案都不会很好地工作,因为保存文件然后将其发送到 Google Cloud Speech 是不够的。
- 我不信任我的 Google 云 API 信息的前端。相反,我已经在后端有一个服务 运行,它有我的凭据,我想将音频(实时)流式传输到该后端,然后从该后端流式传输到 Google 云,然后在我的成绩单返回前端时发出更新。
- 我已经使用 socket.io 连接到该后端服务,我想完全通过套接字来管理它,而不必使用 Binary.js 或类似的东西。
似乎没有关于如何执行此操作的好教程。我该怎么办?
首先,归功于应得的功劳:我这里的大量解决方案是通过引用 vin-ni 的 Google-Cloud-Speech-Node-Socket-Playground project 创建的。但是,我必须针对我的 React 应用程序对此进行一些调整,因此我将分享我所做的一些更改。
我这里的解决方案由四部分组成,两个在前端,两个在后端。
我的前端解决方案分为两部分:
- 一个实用程序文件,用于访问我的麦克风,将音频流式传输到后面 end,从后端取数据,运行每一个回调函数 从后端接收数据的时间,然后清理 在完成流式传输或后端抛出后 一个错误。
- 包裹我的 React 的麦克风组件 功能。
我的后端解决方案分为两部分:
- 处理实际语音识别流的实用程序文件
- 我的
main.js
文件
(这些不需要以任何方式分开;我们的 main.js
文件没有它就已经是一个庞然大物了。)
我的大部分代码将只是摘录,但我的实用程序将完整显示,因为我在涉及的所有阶段都遇到了很多问题。我的前端实用程序文件如下所示:
// Stream Audio
let bufferSize = 2048,
AudioContext,
context,
processor,
input,
globalStream;
//audioStream constraints
const constraints = {
audio: true,
video: false
};
let AudioStreamer = {
/**
* @param {function} onData Callback to run on data each time it's received
* @param {function} onError Callback to run on an error if one is emitted.
*/
initRecording: function(onData, onError) {
socket.emit('startGoogleCloudStream', {
config: {
encoding: 'LINEAR16',
sampleRateHertz: 16000,
languageCode: 'en-US',
profanityFilter: false,
enableWordTimeOffsets: true
},
interimResults: true // If you want interim results, set this to true
}); //init socket Google Speech Connection
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);
// Bind the data handler callback
if(onData) {
socket.on('speechData', (data) => {
onData(data);
});
}
socket.on('googleCloudStreamError', (error) => {
if(onError) {
onError('error');
}
// We don't want to emit another end stream event
closeAll();
});
},
stopRecording: function() {
socket.emit('endGoogleCloudStream', '');
closeAll();
}
}
export default AudioStreamer;
// Helper functions
/**
* Processes microphone data into a data stream
*
* @param {object} e Input from the microphone
*/
function microphoneProcess(e) {
var left = e.inputBuffer.getChannelData(0);
var left16 = convertFloat32ToInt16(left);
socket.emit('binaryAudioData', left16);
}
/**
* Converts a buffer from float32 to int16. Necessary for streaming.
* sampleRateHertz of 1600.
*
* @param {object} buffer Buffer being converted
*/
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
}
/**
* Stops recording and closes everything down. Runs on error or on stop.
*/
function closeAll() {
// Clear the listeners (prevents issue if opening and closing repeatedly)
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 后与数据进行套接字。我对上面链接参考的主要更改是用回调函数(由我的 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});
// No further action needed, as this already closes itself on error
});
}
onStop() {
this.setState({recording: false});
speechToTextUtils.stopRecording();
if(this.props.onStop) {
this.props.onStop();
}
}
(我将我的实际数据处理程序作为 prop 传递给了这个组件)。
然后在后端,我的服务处理了 main.js
中的三个主要事件:
// Start the stream
socket.on('startGoogleCloudStream', function(request) {
speechToTextUtils.startRecognitionStream(socket, GCSServiceAccount, request);
});
// Receive audio data
socket.on('binaryAudioData', function(data) {
speechToTextUtils.receiveData(data);
});
// End the audio stream
socket.on('endGoogleCloudStream', function() {
speechToTextUtils.stopRecognitionStream();
});
我的 speechToTextUtils 看起来像:
// Google Cloud
const speech = require('@google-cloud/speech');
let speechClient = null;
let recognizeStream = null;
module.exports = {
/**
* @param {object} client A socket client on which to emit events
* @param {object} GCSServiceAccount The credentials for our google cloud API access
* @param {object} request A request object of the form expected by streamingRecognize. Variable keys and setup.
*/
startRecognitionStream: function (client, GCSServiceAccount, request) {
if(!speechClient) {
speechClient = new speech.SpeechClient({
projectId: 'Insert your project ID here',
credentials: GCSServiceAccount
}); // Creates a client
}
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 end of utterance, let's restart stream
// this is a small hack. After 65 seconds of silence, the stream will still throw an error for speech length limit
if (data.results[0] && data.results[0].isFinal) {
this.stopRecognitionStream();
this.startRecognitionStream(client, GCSServiceAccount, request);
// console.log('restarted stream serverside');
}
});
},
/**
* Closes the recognize stream and wipes it
*/
stopRecognitionStream: function () {
if (recognizeStream) {
recognizeStream.end();
}
recognizeStream = null;
},
/**
* Receives streaming data and writes it to the recognizeStream for transcription
*
* @param {Buffer} data A section of audio data
*/
receiveData: function (data) {
if (recognizeStream) {
recognizeStream.write(data);
}
}
};
(同样,您并不严格需要此 util 文件,您当然可以将 speechClient
作为常量放在文件顶部,具体取决于您获取凭据的方式;这就是我的方式实现了。)
最后,这应该足以让您开始使用它。我鼓励您在重用或修改这段代码之前尽最大努力理解它,因为它可能对您不起作用 'out of the box',但与我发现的所有其他来源不同,这应该至少让您开始所有相关工作项目的阶段。我希望这个答案能防止其他人像我一样遭受痛苦。