UNPKG

watson-speech

Version:

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

252 lines (206 loc) 8.45 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: 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: timing-stream.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>'use strict'; var Transform = require('stream').Transform; var util = require('util'); var clone = require('clone'); /** * Applies some basic formating to transcriptions: * - Capitalize the first word of each sentence * - Add a period to the end * - Fix any "cruft" in the transcription * - etc. * * @param opts * @param opts.model - some models / languages need special handling * @param [opts.hesitation='\u2026'] - what to put down for a "hesitation" event, defaults to an ellipsis (...) * @constructor */ function TimingStream(opts) { this.opts = util._extend({ emitAt: TimingStream.WORD_START // WORD_START = emit the word as it's beginning to be spoken, WORD_END = once it's completely spoken }, opts); Transform.call(this, opts); this.startTime = Date.now(); // buffer to store future results this.final = []; this.interim = []; this.nextTick = null; var self = this; this.on('pipe', function(source) { source.on('result', self.handleResult.bind(self)); if(source.stop) { self.stop = source.stop.bind(source); } }); } util.inherits(TimingStream, Transform); TimingStream.WORD_START = 1; TimingStream.WORD_END = 2; TimingStream.prototype._transform = function(chunk, encoding, next) { // ignore - we'll emit our own final text based on the result events next(); }; TimingStream.prototype.cutoff = function cutoff() { return (Date.now() - this.startTime)/1000; }; 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.opts.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.opts.emitAt] &lt;= cutoff; }, this); }; /** * Clones the given result and then crops out any words that occur later than the current cutoff * @param result */ Transform.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.opts.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 * - the original next result object (removing it from the array) if it's completely earlier than the current cutoff * * @param results * @returns {*} */ TimingStream.prototype.getCurrentResult = function getCurrentResult(results, cutoff) { if (results.length &amp;&amp; this.withinRange(results[0], cutoff)) { return this.completelyWithinRange(results[0], cutoff) ? results.shift() : this.crop(results[0], cutoff); } }; /** * try to figure out when we'll emit the next word * @param lastResultWasFinal * @param numCurrentTimestamps * @returns {*} */ TimingStream.prototype.getNextWordOffset = function getNextWordOffset(lastResultWasFinal, numCurrentTimestamps) { if (lastResultWasFinal) { // if the current result is final, then grab the first timestamp of the next one var nextResult = this.final[0] || this.interim[0]; return nextResult &amp;&amp; nextResult.alternatives[0].timestamps[0][this.opts.emitAt]; } else { // if the current result wasn't final, then we just want the next word from the current result (assuming there is one) var currentResultSource = this.final[0] || this.interim[0]; var nextTimestamp = currentResultSource &amp;&amp; currentResultSource.alternatives[0].timestamps[numCurrentTimestamps]; return nextTimestamp &amp;&amp; nextTimestamp[this.opts.emitAt]; } }; /** * Tick occurs every half second, or when results are received if we're behind schedule. */ TimingStream.prototype.tick = function tick() { var cutoff = this.cutoff(); this.nextTick = null; var result = this.getCurrentResult(this.final, cutoff); if (!result) { result = this.getCurrentResult(this.interim, cutoff); } if(result) { this.emit('result', result); if (result.final) { this.push(result.alternatives[0].transcript); } var nextWordOffset = this.getNextWordOffset(result.final, result.alternatives[0].timestamps.length); // if we have a next word, set a timeout to emit it. Otherwise the next call to handleResult() will trigger a tick. if (nextWordOffset) { this.nextTick = setTimeout(this.tick.bind(this), this.startTime + (nextWordOffset*1000)); } } }; 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 result */ TimingStream.prototype.handleResult = function handleResult(result) { if (noTimestamps(result)) { throw new Error('TimingStream requires timestamps'); } // 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 interiml 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); } if (!this.nextTick) { this.tick(); } }; TimingStream.prototype.stop = function(){}; // usually overwritten during the `pipe` event module.exports = TimingStream; </code></pre> </article> </section> </div> <nav> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="FormatStream.html">FormatStream</a></li><li><a href="MediaElementAudioStream.html">MediaElementAudioStream</a></li><li><a href="RecognizeStream.html">RecognizeStream</a></li><li><a href="TimingStream.html">TimingStream</a></li></ul><h3>Events</h3><ul><li><a href="MicrophoneStream.html#event:data">data</a></li><li><a href="MicrophoneStream.html#event:raw">raw</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:results">results</a></li></ul><h3>Global</h3><ul><li><a href="global.html#MAX_WAV">MAX_WAV</a></li></ul> </nav> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.4.0</a> on Mon Feb 08 2016 16:11:17 GMT+0000 (UTC) </footer> <script> prettyPrint(); </script> <script src="scripts/linenumber.js"> </script> </body> </html>