【问题标题】:How do I stream live audio from the browser to Google Cloud Speech via socket.io?如何通过 socket.io 将实时音频从浏览器流式传输到 Google Cloud Speech?
【发布时间】:2018-06-21 19:18:11
【问题描述】:

我有一个基于 React 的应用程序的情况,我有一个输入,我也想允许语音输入。我可以让它只与 Chrome 和 Firefox 兼容,所以我正在考虑使用getUserMedia。我知道我将使用 Google Cloud 的 Speech to Text API。但是,我有一些警告:

  1. 我希望它实时流式传输我的音频数据,而不仅仅是在我完成录制时。这意味着我发现的许多解决方案都不能很好地工作,因为仅保存文件然后将其发送到 Google Cloud Speech 是不够的。
  2. 我不相信我的前端有我的 Google Cloud API 信息。相反,我已经在具有我的凭据的后端运行了一项服务,并且我想将音频(实时)流式传输到该后端,然后从该后端流到 Google Cloud,然后将更新发送到我的成绩单他们回到前端。
  3. 我已经使用 socket.io 连接到该后端服务,我想完全通过套接字来管理它,而不必使用 Binary.js 或类似的东西。

似乎没有一个很好的教程来说明如何做到这一点。我该怎么办?

【问题讨论】:

    标签: javascript socket.io google-cloud-speech


    【解决方案1】:

    首先,功劳归于功劳:我在这里的大量解决方案是通过引用 vin-ni 的Google-Cloud-Speech-Node-Socket-Playground project 创建的。不过,我不得不为我的 React 应用程序调整一些内容,所以我分享了我所做的一些更改。

    我这里的解决方案由四个部分组成,两个在前端,两个在后端。

    我的前端解决方案分为两部分:

    1. 用于访问我的麦克风、将音频流式传输到后面的实用程序文件 end,从后端取回数据,分别运行一个回调函数 从后端接收数据的时间,然后清理 在完成流式传输或后端抛出时在其自身之后 一个错误。
    2. 包裹我的 React 的麦克风组件 功能。

    我的后端解决方案分为两部分:

    1. 处理实际语音识别流的实用程序文件
    2. 我的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 回调在转换后将 speechData 事件发送到带有数据的套接字它到 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();
        }
    }
    

    (我将我的实际数据处理程序作为道具传递给该组件)。

    然后在后端,我的服务处理了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 作为 const 放在文件顶部,具体取决于您获取凭据的方式;这正是我实现它的方式。)

    最后,这应该足以让你开始做这件事了。我鼓励您在重用或修改它之前尽最大努力理解此代码,因为它可能无法为您“开箱即用”,但与我发现的所有其他来源不同,这至少应该让您开始涉及项目的各个阶段。我希望这个答案能防止其他人像我一样遭受痛苦。

    【讨论】:

    • @vinni Ha,我没有意识到您已经对这种情况有自己的问题/答案。我可能仍然发布我的答案只是为了澄清基于 React 的使用,因为这确实有一些微妙之处(例如,如果您的组件可以反复卸载/重新安装,则删除侦听器很重要),但我很惊讶它没有t 出现在我的谷歌搜索中。再次感谢您的工作 - 如果没有它,我真的无法启动这个项目!
    • 太棒了!我对 Google Cloud 和 IBM Watson 等语音识别提供商有在 SAFARI 上运行的演示感到困惑。我曾尝试对 IBM 和 Google 的演示网站进行逆向工程,但没有成功,因为代码量很大。你的解决方案也适用于 Safari 吗?
    • @Josh 这是一个很好的问题,实际上我没有最模糊的想法!我可能需要一段时间才能有机会测试并看到它,但这可能会有所帮助吗? stackoverflow.com/questions/21015847/…
    • 我总是收到错误的数据。只发送一个配置,然后是音频 data.error。你能帮帮我吗?
    • 我也面临一个奇怪的问题,当我 console.log(speech.SpeechClient).. 它给我 null。我安装了@google-cloud/speech。所以我不知道为什么会这样。
    猜你喜欢
    • 1970-01-01
    • 2012-11-20
    • 2023-04-04
    • 2014-05-29
    • 2023-03-12
    • 1970-01-01
    • 2010-09-14
    • 2014-11-10
    • 2017-02-10
    相关资源
    最近更新 更多