From f46f3e1e638dbbf15dfe935e50c65ba79742b536 Mon Sep 17 00:00:00 2001 From: Denys Vitali Date: Sat, 16 Jan 2016 19:07:14 +0100 Subject: [PATCH 1/3] Code cleanup and a new lang option to recognize text in a non en-us language --- README.md | 9 +- index.js | 498 ++++++++++++++++++++++++++++++------------------------ test.js | 15 ++ 3 files changed, 297 insertions(+), 225 deletions(-) create mode 100644 test.js diff --git a/README.md b/README.md index 0a8ec61..f7ecfb0 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ For clips under 60 seconds, usage is simple: ```javascript var gspeech = require('gspeech-api'); -gspeech.recognize('path/to/my/file', function(err, data) { +var GS = new gspeech({lang: 'fr-FR'}); //Default recognition is in en-US +GS.recognize('path/to/my/file', function(err, data) { if (err) console.error(err); console.log("Final transcript is:\n" + data.transcript); @@ -36,7 +37,7 @@ All other dependencies should be automatically handled by npm. ## Documentation -This package exposes one main method, `gspeech.recognize(options, callback)` for taking a file and returning an array of timed captions along with a final transcript. +This package exposes one main method, `GS.recognize(options, callback)` for taking a file and returning an array of timed captions along with a final transcript. ### Arguments @@ -64,7 +65,7 @@ Callback is a function which will be called after all requests to Google speech #### Getting a timed transcript ```javascript -gspeech.recognize('path/to/my/file', function(err, data) { +GS.recognize('path/to/my/file', function(err, data) { if (err) console.error(err); for (var i = 0; i < data.timedTranscript.length; i++) { @@ -81,7 +82,7 @@ If you would like to generate a timed transcript, and know where fragments start ```javascript var segTimes = [0, 15, 20, 30]; -gspeech.recognize({ +GS.recognize({ 'file': 'path/to/my/file', 'segments': segTimes, }, diff --git a/index.js b/index.js index a33b35a..6efcca5 100644 --- a/index.js +++ b/index.js @@ -1,221 +1,277 @@ -(function() { - var request = require("request"); - var fs = require("fs"); - var temp = require("temp").track(); - var ffmpeg = require('fluent-ffmpeg'); - var async = require('async'); - var http = require('http'); - - var API_KEY = "AIzaSyBOti4mM-6x9WDnZIjIeyEU21OpBXqWBgw"; - - var MAX_CONCURRENT = 20; - var MAX_SEG_DUR = 15; - var POST_SAMPLE_RATE = 44100; - - var gspeech = {}; - - function urlEncode(obj) { - var str = []; - for(var p in obj) - if (obj.hasOwnProperty(p)) { - str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); - } - return str.join("&"); - } - - function genPair() { - return parseInt(Math.random() * Math.pow(2, 32)).toString(16); - } - - function getRequestOptions() { - var pair = genPair(); - - var upstreamUrl = 'https://www.google.com/speech-api/full-duplex/v1/up?'; - var upstreamParams = urlEncode({ - 'output': 'json', - 'lang': 'en-us', - 'pfilter': 2, - 'key': API_KEY, - 'client': 'chromium', - 'maxAlternatives': 1, - 'pair': pair - }); - var otherOpts = ['continuous', 'interim']; - var upstreamOpts = { - 'url': upstreamUrl + upstreamParams + '&' + otherOpts.join("&"), - 'headers': { - 'content-type': 'audio/x-flac; rate=' + POST_SAMPLE_RATE - }, - }; - var downstreamUrl = 'https://www.google.com/speech-api/full-duplex/v1/down'; - var downstreamParams = urlEncode({ - 'pair': pair - }); - var downstreamOpts = { - 'url': downstreamUrl + '?' + downstreamParams, - }; - return [upstreamOpts, downstreamOpts]; - } - - function fullText(timedTranscript) { - var full = ''; - for (var i = 0; i < timedTranscript.length; i++) { - full += timedTranscript[i].text + ' '; - } - return full; - } - - gspeech.recognize = function(options, callback) { - function getTranscriptFromServer(params, onfinish) { - /* - * Called by processAudioSegment - * Takes a file specified in params, sends it to Google speech recognition server - * and runs callback on the returned transcript data - */ - - if (!params.file) { - onfinish(new Error("No file is specified. Please specify a file path through params.file")); - } - var file_name = params.file; - var source = fs.createReadStream(file_name); - source.on('error', function (err) {onfinish(err);}); - var opts = getRequestOptions(); - var upstreamOpts = opts[0]; - var downstreamOpts = opts[1]; - var postReq = request.post(upstreamOpts, function(error, res, body) { - if (error) { - onfinish(error); - } - }); - - - source.pipe(postReq); - - var getReq = request.get(downstreamOpts, function(error, res, body) { - if (error) { - onfinish(error); - } - try { - var results = body.split('\n'); - var last_result = JSON.parse(results[results.length-2]); // last result is always an empty Array. Second to last is final transcript. - var text = last_result.result[0].alternative[0].transcript; - onfinish(null, { - 'text': text, - 'start': params.start, - 'duration': params.duration - }); - } - catch (e) { - // If there is an error, the server posts HTML instead of JSON, which can't be parsed - params.retries = params.retries | 0; - if (params.retries < maxRetries) { - params.retries++; - - getTranscriptFromServer(params, onfinish); - } - else { - onfinish(new Error("Could not get valid response from Google servers " - + "for segment starting at second " + params.start)); - } - } - }); - - } - - function processAudioSegment(data, onfinish) { - /* - * Processes a segment of audio from file by using ffmpeg to convert - * a segment of specified start time and duration, save it as a temporary .flac file - * and send it to getTranscriptFromServer - */ - - var start = data.start; - var dur = data.duration; - var tmpFile = temp.path({suffix: '.flac'}); - - // Convert segment of audio file into .flac file - ffmpeg() - .on('error', function (err) { - onfinish(err); - }) - .on('end', function () { - // After conversion has finished, get the transcript - getTranscriptFromServer({ - 'file': tmpFile, - 'start': start, - 'duration': dur - }, onfinish); - }) - .input(file) - .setStartTime(start) - .duration(dur) - .output(tmpFile) - .audioFrequency(POST_SAMPLE_RATE) - .toFormat('flac') - .run(); - } - - var file = options.file || options; - var segments = options.segments; - var maxDuration = options.maxDuration | MAX_SEG_DUR; - var maxRetries = options.maxRetries | 1; - var limitConcurrent = options.limitConcurrent | MAX_CONCURRENT; - var retries = 0; - - // Get file information and divide into segments - // Then process each of these segments individually, and combine results at the end - ffmpeg.ffprobe(file, function (err, info) { - var audioSegments = [] - var totalDuration = info.format.duration; - if (segments) { - for (var i = 0; i < segments.length; i++) { - var duration = (i == segments.length-1) ? totalDuration-segments[i]: segments[i+1]-segments[i]; - if (duration < 0) { - callback(new Error("segments must be a sorted array of start times, \ - each less than the total length of your audio")); - } - var curStart = segments[i]; - while (duration > maxDuration + .001) { - audioSegments.push({ - 'start': curStart, - 'duration': maxDuration - }); - duration -= maxDuration; - curStart += maxDuration; - } - audioSegments.push({ - 'start': curStart, - 'duration': duration - }); - } - } - else { - var numSegments = Math.ceil(totalDuration/ maxDuration); - for (var i = 0; i < numSegments; i++) { - audioSegments.push({ - 'start': maxDuration * i, - 'duration': maxDuration - }); - } - } - - async.mapLimit(audioSegments, limitConcurrent, processAudioSegment, function(err, results) { - // After all transcripts have been returned, process them - if (err) - callback(err); - var timedTranscript = results.sort(function(a,b) { - if (a.start < b.start) return -1; - if (a.start > b.start) return 1; - return 0; - }); - - callback(null, { - 'timedTranscript': timedTranscript, - 'transcript': fullText(timedTranscript) - }); - }); - }); - }; - - module.exports = gspeech; -}()); \ No newline at end of file +'use strict'; +/* jshint node:true */ +var request = require('request'); +var extend = require('extend'); +var fs = require('fs'); +var temp = require('temp').track(); +var ffmpeg = require('fluent-ffmpeg'); +var async = require('async'); + + +var gspeech = function (options) +{ + var defaults = { + MAX_CONCURRENT: 20, + MAX_SEG_DUR: 15, + POST_SAMPLE_RATE: 44100, + lang: 'en-us', + key: 'AIzaSyBOti4mM-6x9WDnZIjIeyEU21OpBXqWBgw' + }; + + this.options = extend(defaults, options); + + if (this.options.key === '') + { + throw ('Provide an API Key to continue.'); + } +}; + +function urlEncode(obj) +{ + var str = []; + for (var p in obj) + if (obj.hasOwnProperty(p)) + { + str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p])); + } + return str.join('&'); +} + +function genPair() +{ + return parseInt(Math.random() * Math.pow(2, 32)).toString(16); +} + +function getRequestOptions(options) +{ + var pair = genPair(); + + var upstreamUrl = 'https://www.google.com/speech-api/full-duplex/v1/up?'; + var upstreamParams = urlEncode( + { + 'output': 'json', + 'lang': options.lang, + 'pfilter': 2, + 'key': options.key, + 'client': 'chromium', + 'maxAlternatives': 1, + 'pair': pair + }); + var otherOpts = ['continuous', 'interim']; + var upstreamOpts = { + 'url': upstreamUrl + upstreamParams + '&' + otherOpts.join('&'), + 'headers': + { + 'content-type': 'audio/x-flac; rate=' + options.POST_SAMPLE_RATE + }, + }; + var downstreamUrl = 'https://www.google.com/speech-api/full-duplex/v1/down'; + var downstreamParams = urlEncode( + { + 'pair': pair + }); + var downstreamOpts = { + 'url': downstreamUrl + '?' + downstreamParams, + }; + return [upstreamOpts, downstreamOpts]; +} + +function fullText(timedTranscript) +{ + var full = ''; + for (var i = 0; i < timedTranscript.length; i++) + { + full += timedTranscript[i].text + ' '; + } + return full; +} + +gspeech.prototype.recognize = function (options, callback) +{ + var gs = this; + + function getTranscriptFromServer(params, onfinish) + { + /* + * Called by processAudioSegment + * Takes a file specified in params, sends it to Google speech recognition server + * and runs callback on the returned transcript data + */ + + if (!params.file) + { + onfinish(new Error('No file is specified. Please specify a file path through params.file')); + } + var file_name = params.file; + var source = fs.createReadStream(file_name); + source.on('error', function (err) + { + onfinish(err); + }); + var opts = getRequestOptions(gs.options); + var upstreamOpts = opts[0]; + var downstreamOpts = opts[1]; + var postReq = request.post(upstreamOpts, function (err) + { + if (err) + { + onfinish(err); + } + }); + + + source.pipe(postReq); + + request.get(downstreamOpts, function (error, res, body) + { + if (error) + { + onfinish(error); + } + try + { + var results = body.split('\n'); + var last_result = JSON.parse(results[results.length - 2]); // last result is always an empty Array. Second to last is final transcript. + var text = last_result.result[0].alternative[0].transcript; + onfinish(null, + { + 'text': text, + 'start': params.start, + 'duration': params.duration + }); + } + catch (e) + { + // If there is an error, the server posts HTML instead of JSON, which can't be parsed + params.retries = params.retries | 0; + if (params.retries < maxRetries) + { + params.retries++; + getTranscriptFromServer(params, onfinish); + } + else + { + onfinish(new Error('Could not get valid response from Google servers ' + 'for segment starting at second ' + params.start)); + } + } + }); + + } + + function processAudioSegment(data, onfinish) + { + /* + * Processes a segment of audio from file by using ffmpeg to convert + * a segment of specified start time and duration, save it as a temporary .flac file + * and send it to getTranscriptFromServer + */ + + var start = data.start; + var dur = data.duration; + var tmpFile = temp.path( + { + suffix: '.flac' + }); + + // Convert segment of audio file into .flac file + ffmpeg() + .on('error', function (err) + { + onfinish(err); + }) + .on('end', function () + { + // After conversion has finished, get the transcript + getTranscriptFromServer( + { + 'file': tmpFile, + 'start': start, + 'duration': dur + }, onfinish); + }) + .input(file) + .setStartTime(start) + .duration(dur) + .output(tmpFile) + .audioFrequency(gs.options.POST_SAMPLE_RATE) + .toFormat('flac') + .run(); + } + + var file = options.file || options; + var segments = options.segments; + + var maxDuration = options.maxDuration | gs.options.MAX_SEG_DUR; + var maxRetries = options.maxRetries | 1; + var limitConcurrent = options.limitConcurrent | gs.options.MAX_CONCURRENT; + + var i; + + // Get file information and divide into segments + // Then process each of these segments individually, and combine results at the end + ffmpeg.ffprobe(file, function (err, info) + { + var audioSegments = []; + var totalDuration = info.format.duration; + if (segments) + { + for (i = 0; i < segments.length; i++) + { + var duration = (i == segments.length - 1) ? totalDuration - segments[i] : segments[i + 1] - segments[i]; + if (duration < 0) + { + callback(new Error('segments must be a sorted array of start times, each less than the total length of your audio')); + } + var curStart = segments[i]; + while (duration > maxDuration + 0.001) + { + audioSegments.push( + { + 'start': curStart, + 'duration': maxDuration + }); + duration -= maxDuration; + curStart += maxDuration; + } + audioSegments.push( + { + 'start': curStart, + 'duration': duration + }); + } + } + else + { + var numSegments = Math.ceil(totalDuration / maxDuration); + for (i = 0; i < numSegments; i++) + { + audioSegments.push( + { + 'start': maxDuration * i, + 'duration': maxDuration + }); + } + } + + async.mapLimit(audioSegments, limitConcurrent, processAudioSegment, function (err, results) + { + // After all transcripts have been returned, process them + if (err) + callback(err); + var timedTranscript = results.sort(function (a, b) + { + if (a.start < b.start) return -1; + if (a.start > b.start) return 1; + return 0; + }); + + callback(null, + { + 'timedTranscript': timedTranscript, + 'transcript': fullText(timedTranscript) + }); + }); + }); +}; + +module.exports = gspeech; \ No newline at end of file diff --git a/test.js b/test.js new file mode 100644 index 0000000..37878aa --- /dev/null +++ b/test.js @@ -0,0 +1,15 @@ +'use strict'; +/* jshint node:true */ + +var gspeech = require('gspeech-api'); + +var GS = new gspeech({ + lang: 'it-ch' +}); + +GS.recognize(__dirname + '/clip.mp4', function (err, data) +{ + if (err) + console.error(err); + console.log('Final transcript is:\n' + data.transcript); +}); \ No newline at end of file From 7d1c68214fdd5bb073b545ca5f25df4c1fafd8bd Mon Sep 17 00:00:00 2001 From: Denys Vitali Date: Sun, 17 Jan 2016 14:47:13 +0100 Subject: [PATCH 2/3] Fixed an 'undefined' error when the text recognized is null --- index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 6efcca5..cbb5d0c 100644 --- a/index.js +++ b/index.js @@ -79,9 +79,15 @@ function getRequestOptions(options) function fullText(timedTranscript) { var full = ''; + if(timedTranscript === undefined) + { + return full; + } + for (var i = 0; i < timedTranscript.length; i++) { - full += timedTranscript[i].text + ' '; + if(timedTranscript[i] !== undefined) + full += timedTranscript[i].text + ' '; } return full; } From 959e5ebb472918288322db463411b89f5cd1bdfc Mon Sep 17 00:00:00 2001 From: Denys Vitali Date: Sat, 23 Jan 2016 11:29:21 +0100 Subject: [PATCH 3/3] =?UTF-8?q?Pollution=20removal=20=F0=9F=9A=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index cbb5d0c..f47b2f2 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,4 @@ +(function(){ 'use strict'; /* jshint node:true */ var request = require('request'); @@ -280,4 +281,5 @@ gspeech.prototype.recognize = function (options, callback) }); }; -module.exports = gspeech; \ No newline at end of file +module.exports = gspeech; +})();