UNPKG

watson-speech

Version:

IBM Watson Speech to Text and Text to Speech SDK for web browsers.

308 lines (259 loc) 11.9 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: speech-to-text/timing-stream.js</title> <script src="scripts/prettify/prettify.js"> </script> <script src="scripts/prettify/lang-css.js"> </script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <div id="main"> <h1 class="page-title">Source: speech-to-text/timing-stream.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>'use strict'; var Duplex = require('stream').Duplex; var util = require('util'); var clone = require('clone'); var defaults = require('defaults'); /** * Slows results down to no faster than real time. * * Useful when running recognizeFile because the text can otherwise appear before the words are spoken * * @param {Object} [opts] * @param {*} [opts.emitAtt=TimingStream.START] - set to TimingStream.END to only emit text that has been completely spoken. * @param {Number} [opts.delay=0] - Additional delay (in seconds) to apply before emitting words, useful for precise syncing to audio tracks. May be negative * @constructor */ function TimingStream(opts) { this.options = defaults(opts, { emitAt: TimingStream.START, delay: 0, allowHalfOpen: true, // keep the readable side open after the source closes writableObjectMode: true }); Duplex.call(this, opts); this.startTime = Date.now(); // buffer to store future results this.final = []; this.interim = []; this.nextTick = null; this.sourceEnded = false; var self = this; this.on('pipe', function(source) { source.on('end', function() { self.sourceEnded = true; // todo: see if there's anything built-in that does this for us self.checkForEnd(); }); }); } util.inherits(TimingStream, Duplex); TimingStream.START = 1; TimingStream.END = 2; TimingStream.prototype._write = function(result, encoding, next) { if (result instanceof Buffer) { return this.emit('error', new Error('TimingStream requires the source to be in objectMode')); } this.handleResult(result); next(); }; TimingStream.prototype._read = function(/* size*/) { // ignore - we'll emit results once the time has come }; TimingStream.prototype.cutoff = function cutoff() { return (Date.now() - this.startTime) / 1000 - this.options.delay; }; TimingStream.prototype.withinRange = function(result, cutoff) { return result.alternatives.some(function(alt) { // timestamp structure is ["word", startTime, endTime] // if the first timestamp ends before the cutoff, then it's at least partially within range var timestamp = alt.timestamps[0]; return !!timestamp &amp;&amp; timestamp[this.options.emitAt] &lt;= cutoff; }, this); }; TimingStream.prototype.completelyWithinRange = function(result, cutoff) { return result.alternatives.every(function(alt) { // timestamp structure is ["word", startTime, endTime] // if the last timestamp ends before the cutoff, then it's completely within range var timestamp = alt.timestamps[alt.timestamps.length - 1]; return timestamp[this.options.emitAt] &lt;= cutoff; }, this); }; /** * Clones the given result and then crops out any words that occur later than the current cutoff * @param {Object} result * @param {Number} cutoff timestamp (in seconds) * @returns {Object} */ Duplex.prototype.crop = function crop(result, cutoff) { result = clone(result); result.alternatives = result.alternatives.map(function(alt) { var timestamps = []; for (var i = 0, timestamp; i &lt; alt.timestamps.length; i++) { timestamp = alt.timestamps[i]; if (timestamp[this.options.emitAt] &lt;= cutoff) { timestamps.push(timestamp); } else { break; } } alt.timestamps = timestamps; alt.transcript = timestamps.map(function(ts) { return ts[0]; }).join(' '); return alt; }, this); // "final" signifies both that the text won't change, and that we're at the end of a sentence. Only one of those is true here. result.final = false; return result; }; /** * Returns one of: * - undefined if the next result is completely later than the current cutoff * - a cropped clone of the next result if it's later than the current cutoff &amp;&amp; in objectMode * - the original next result object (removing it from the array) if it's completely earlier than the current cutoff (or we're in string mode with emitAt set to start) * * @param {Object} results * @param {Number} cutoff * @returns {Object|undefined} */ TimingStream.prototype.getCurrentResult = function getCurrentResult(results, cutoff) { if (results.length &amp;&amp; this.withinRange(results[0], cutoff)) { var completeResult = this.completelyWithinRange(results[0], cutoff); if (this.options.objectMode || this.options.readableObjectMode) { // object mode: emit either a complete result or a cropped result return completeResult ? results.shift() : this.crop(results[0], cutoff); } else if (completeResult || this.options.emitAt === TimingStream.START) { // string mode: emit either a complete result or nothing return results.shift(); } } }; /** * Tick emits any buffered words that have a timestamp before the current time, then calls scheduleNextTick() */ TimingStream.prototype.tick = function tick() { var cutoff = this.cutoff(); clearTimeout(this.nextTick); var result = this.getCurrentResult(this.final, cutoff); if (!result) { result = this.getCurrentResult(this.interim, cutoff); } if (result) { if (this.options.objectMode || this.options.readableObjectMode) { this.push(result); } else { this.push(result.alternatives[0].transcript); } if (result.final) { this.nextTick = setTimeout(this.tick.bind(this), 0); // in case we are multiple results behind - don't schedule until we are out of final results that are due now return; } } this.scheduleNextTick(cutoff); }; /** * Schedules next tick if possible. Requires previous stream to emit recognize objects (objectMode or readableObjectMode) * * triggers the 'close' and 'end' events if the buffer is empty and no further results are expected * * @param {Number} cutoff * */ TimingStream.prototype.scheduleNextTick = function scheduleNextTick(cutoff) { // prefer final results over interim - when final results are added, any older interim ones are automatically deleted. var nextResult = this.final[0] || this.interim[0]; if (nextResult) { // loop through the timestamps until we find one that comes after the current cutoff (there should always be one) var timestamps = nextResult.alternatives[0].timestamps; for (var i = 0; i &lt; timestamps.length; i++) { var wordOffset = timestamps[i][this.options.emitAt]; if (wordOffset > cutoff) { var nextTime = this.startTime + (wordOffset * 1000); this.nextTick = setTimeout(this.tick.bind(this), nextTime - Date.now()); return; } } throw new Error('No future words found'); // this shouldn't happen ever - getCurrentResult should automatically delete the result from the buffer if all of it's words are consumed } else { // clear the next tick this.nextTick = null; this.checkForEnd(); } }; /** * Triggers the 'close' and 'end' events if both pre-conditions are true: * - the previous stream must have already emitted it's 'end' event * - there must be no next tick scheduled, indicating that there are no results buffered for later delivery */ TimingStream.prototype.checkForEnd = function() { if (this.sourceEnded &amp;&amp; !this.nextTick) { this.emit('close'); this.push(null); } }; /** * Returns true if the result is missing it's timestamps * @param {Object} result * @returns {Boolean} */ function noTimestamps(result) { var alt = result.alternatives &amp;&amp; result.alternatives[0]; return !!(alt &amp;&amp; alt.transcript.trim() &amp;&amp; !alt.timestamps || !alt.timestamps.length); } /** * Creates a new result with all transcriptions formatted * * @param {Object} result */ TimingStream.prototype.handleResult = function handleResult(result) { if (noTimestamps(result)) { this.emit('error', new Error('TimingStream requires timestamps')); return; } // additional alternatives do not include timestamps, so we can't process and emit them correctly if (result.alternatives.length > 1) { result.alternatives.length = 1; } // loop through the buffer and delete any interim results with the same or lower index while (this.interim.length &amp;&amp; this.interim[0].index &lt;= result.index) { this.interim.shift(); } if (result.final) { // then add it to the final results array this.final.push(result); } else { this.interim.push(result); } this.tick(); }; TimingStream.prototype.promise = require('./to-promise'); // when stop is called, immediately stop emitting results TimingStream.prototype.stop = function stop() { this.emit('stop'); clearTimeout(this.nextTick); this.handleResult = function noop(){}; // RecognizeStream.stop() closes the connection gracefully, so we will usually see one more result this.checkForEnd(); // in case the RecognizeStream already ended }; module.exports = TimingStream; </code></pre> </article> </section> </div> <nav> <h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-watson-speech.html">watson-speech</a></li><li><a href="module-watson-speech_speech-to-text.html">watson-speech/speech-to-text</a></li><li><a href="module-watson-speech_speech-to-text_get-models.html">watson-speech/speech-to-text/get-models</a></li><li><a href="module-watson-speech_speech-to-text_recognize-file.html">watson-speech/speech-to-text/recognize-file</a></li><li><a href="module-watson-speech_speech-to-text_recognize-microphone.html">watson-speech/speech-to-text/recognize-microphone</a></li><li><a href="module-watson-speech_text-to-speech.html">watson-speech/text-to-speech</a></li><li><a href="module-watson-speech_text-to-speech_get-voices.html">watson-speech/text-to-speech/get-voices</a></li><li><a href="module-watson-speech_text-to-speech_synthesize.html">watson-speech/text-to-speech/synthesize</a></li></ul><h3>Classes</h3><ul><li><a href="FilePlayer.html">FilePlayer</a></li><li><a href="FormatStream.html">FormatStream</a></li><li><a href="RecognizeStream.html">RecognizeStream</a></li><li><a href="TimingStream.html">TimingStream</a></li><li><a href="WebAudioL16Stream.html">WebAudioL16Stream</a></li><li><a href="WritableElementStream.html">WritableElementStream</a></li></ul><h3>Events</h3><ul><li><a href="RecognizeStream.html#event:close">close</a></li><li><a href="RecognizeStream.html#event:connection-close">connection-close</a></li><li><a href="RecognizeStream.html#event:data">data</a></li><li><a href="RecognizeStream.html#event:error">error</a></li><li><a href="RecognizeStream.html#event:receive-json">receive-json</a></li><li><a href="RecognizeStream.html#event:results">results</a></li><li><a href="RecognizeStream.html#event:send-json">send-json</a></li></ul><h3>Global</h3><ul><li><a href="global.html#getContentTypeFromFile">getContentTypeFromFile</a></li><li><a href="global.html#noTimestamps">noTimestamps</a></li><li><a href="global.html#playFile">playFile</a></li></ul> </nav> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.4.0</a> on Wed Jul 20 2016 21:29:03 GMT+0000 (UTC) </footer> <script> prettyPrint(); </script> <script src="scripts/linenumber.js"> </script> </body> </html>