2
2
3
3
var Duplex = require ( 'stream' ) . Duplex ;
4
4
var util = require ( 'util' ) ;
5
- var clone = require ( 'clone' ) ;
6
5
var defaults = require ( 'defaults' ) ;
7
6
var noTimestamps = require ( './no-timestamps' ) ;
8
7
@@ -15,7 +14,7 @@ var noTimestamps = require('./no-timestamps');
15
14
* @todo : fix TimingStream to work with the output of the SpeakerStream
16
15
*
17
16
* @param {Object } [opts]
18
- * @param {* } [opts.emitAtt =TimingStream.START] - set to TimingStream.END to only emit text that has been completely spoken.
17
+ * @param {* } [opts.emitAt =TimingStream.START] - set to TimingStream.END to only emit text that has been completely spoken.
19
18
* @param {Number } [opts.delay=0] - Additional delay (in seconds) to apply before emitting words, useful for precise syncing to audio tracks. May be negative
20
19
* @constructor
21
20
*/
@@ -29,12 +28,14 @@ function TimingStream(opts) {
29
28
Duplex . call ( this , opts ) ;
30
29
31
30
this . startTime = Date . now ( ) ;
32
- // buffer to store future results
33
- this . final = [ ] ;
34
- this . interim = [ ] ;
35
- this . speakerLabels = [ ] ;
31
+
32
+ // queue to store future messages
33
+ this . messages = [ ] ;
34
+
35
+ // setTimeout handle. if null, next tick will occur whenever new data arrives
36
36
this . nextTick = null ;
37
- this . nextSpeakerLabelsTick = null ;
37
+
38
+ // this stream cannot end until both the messages queue is empty and the source stream has ended
38
39
this . sourceEnded = false ;
39
40
40
41
var self = this ;
@@ -48,15 +49,21 @@ util.inherits(TimingStream, Duplex);
48
49
TimingStream . START = 1 ;
49
50
TimingStream . END = 2 ;
50
51
51
- TimingStream . prototype . _write = function ( data , encoding , next ) {
52
- if ( data instanceof Buffer ) {
52
+ TimingStream . prototype . _write = function ( msg , encoding , next ) {
53
+ if ( msg instanceof Buffer ) {
53
54
return this . emit ( 'error' , new Error ( 'TimingStream requires the source to be in objectMode' ) ) ;
54
55
}
55
- if ( Array . isArray ( data . results ) && data . results . length ) {
56
- this . handleResult ( data ) ;
56
+ if ( Array . isArray ( msg . results ) && msg . results . length && noTimestamps ( msg ) ) {
57
+ var err = new Error ( 'TimingStream requires timestamps' ) ;
58
+ err . name = noTimestamps . ERROR_NO_TIMESTAMPS ;
59
+ this . emit ( 'error' , err ) ;
60
+ return ;
57
61
}
58
- if ( Array . isArray ( data . speaker_labels ) && data . speaker_labels . length ) {
59
- this . handleSpeakerLabels ( data ) ;
62
+
63
+ this . messages . push ( msg ) ;
64
+
65
+ if ( ! this . nextTick ) {
66
+ this . scheduleNextTick ( ) ;
60
67
}
61
68
next ( ) ;
62
69
} ;
@@ -69,108 +76,73 @@ TimingStream.prototype.cutoff = function cutoff() {
69
76
return ( Date . now ( ) - this . startTime ) / 1000 - this . options . delay ;
70
77
} ;
71
78
72
- TimingStream . prototype . withinRange = function ( result , cutoff ) {
73
- return result . results [ 0 ] . alternatives . some ( function ( alt ) {
74
- // timestamp structure is ["word", startTime, endTime]
75
- // if the first timestamp ends before the cutoff, then it's at least partially within range
76
- var timestamp = alt . timestamps [ 0 ] ;
77
- return ! ! timestamp && timestamp [ this . options . emitAt ] <= cutoff ;
78
- } , this ) ;
79
- } ;
80
-
81
- TimingStream . prototype . completelyWithinRange = function ( result , cutoff ) {
82
- return result . results [ 0 ] . alternatives . every ( function ( alt ) {
83
- // timestamp structure is ["word", startTime, endTime]
84
- // if the last timestamp ends before the cutoff, then it's completely within range
85
- var timestamp = alt . timestamps [ alt . timestamps . length - 1 ] ;
86
- return timestamp [ this . options . emitAt ] <= cutoff ;
87
- } , this ) ;
88
- } ;
89
-
90
79
/**
91
- * Clones the given result and then crops out any words that occur later than the current cutoff
92
- * @param {Object } data
93
- * @param {Number } cutoff timestamp (in seconds)
94
- * @returns {Object }
80
+ * Grabs the appropriate timestamp from the given message, depending on options.emitAt and the type of message
81
+ *
82
+ * @private
83
+ * @param {Object } msg
84
+ * @returns {Number } timestamp
95
85
*/
96
- Duplex . prototype . crop = function crop ( data , cutoff ) {
97
- data = clone ( data ) ;
98
- var result = data . results [ 0 ] ;
99
- result . alternatives = result . alternatives . map ( function ( alt ) {
100
- var timestamps = [ ] ;
101
- for ( var i = 0 , timestamp ; i < alt . timestamps . length ; i ++ ) {
102
- timestamp = alt . timestamps [ i ] ;
103
- if ( timestamp [ this . options . emitAt ] <= cutoff ) {
104
- timestamps . push ( timestamp ) ;
105
- } else {
106
- break ;
107
- }
86
+ TimingStream . prototype . getMessageTime = function ( msg ) {
87
+ if ( this . options . emitAt === TimingStream . START ) {
88
+ if ( Array . isArray ( msg . results ) && msg . results . length ) {
89
+ return msg . results [ 0 ] . alternatives [ 0 ] . timestamps [ 0 ] [ TimingStream . START ] ;
90
+ } else if ( Array . isArray ( msg . speaker_labels ) && msg . speaker_labels . length ) {
91
+ return msg . speaker_labels [ 0 ] . from ;
108
92
}
109
- alt . timestamps = timestamps ;
110
- alt . transcript = timestamps . map ( function ( ts ) {
111
- return ts [ 0 ] ;
112
- } ) . join ( ' ' ) ;
113
- return alt ;
114
- } , this ) ;
115
- // "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.
116
- result . final = false ;
117
- return data ;
93
+ } else {
94
+ if ( Array . isArray ( msg . results ) && msg . results . length ) {
95
+ var timestamps = msg . results [ msg . results . length - 1 ] . alternatives [ 0 ] . timestamps ;
96
+ return timestamps [ timestamps . length - 1 ] [ TimingStream . END ] ;
97
+ } else if ( Array . isArray ( msg . speaker_labels ) && msg . speaker_labels . length ) {
98
+ return msg . speaker_labels [ msg . speaker_labels . length - 1 ] . to ;
99
+ }
100
+ }
101
+ return 0 ; // failsafe for unknown message types
118
102
} ;
119
103
120
104
/**
121
105
* Returns one of:
122
- * - undefined if the next result is completely later than the current cutoff
123
- * - a cropped clone of the next result if it's later than the current cutoff && in objectMode
124
- * - 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)
106
+ * - null if the next result is completely later than the current cutoff
107
+ * - the original next result object (removing it from the array) if it's completely earlier than the current cutoff
108
+ * (or it's partially within range and emitAt is set to start)
125
109
*
126
- * @param {Object } results
127
- * @param {Number } cutoff
128
- * @returns {Object|undefined }
110
+ * @private
111
+ * @returns {Object|null }
129
112
*/
130
- TimingStream . prototype . getCurrentResult = function getCurrentResult ( results , cutoff ) {
131
- if ( results . length && this . withinRange ( results [ 0 ] , cutoff ) ) {
132
- var completeResult = this . completelyWithinRange ( results [ 0 ] , cutoff ) ;
133
- if ( this . options . objectMode || this . options . readableObjectMode ) {
134
- // object mode: emit either a complete result or a cropped result
135
- return completeResult ? results . shift ( ) : this . crop ( results [ 0 ] , cutoff ) ;
136
- } else if ( completeResult || this . options . emitAt === TimingStream . START ) {
137
- // string mode: emit either a complete result or nothing
138
- return results . shift ( ) ;
139
- }
113
+ TimingStream . prototype . getCurrentResult = function getCurrentResult ( ) {
114
+ if ( ! this . messages . length ) {
115
+ return null ;
116
+ }
117
+ if ( this . getMessageTime ( this . messages [ 0 ] ) <= this . cutoff ( ) ) {
118
+ return this . messages . shift ( ) ;
140
119
}
141
120
} ;
142
121
143
122
144
123
/**
145
124
* Tick emits any buffered words that have a timestamp before the current time, then calls scheduleNextTick()
125
+ *
126
+ * @private
146
127
*/
147
128
TimingStream . prototype . tick = function tick ( ) {
148
- var cutoff = this . cutoff ( ) ;
149
-
150
- clearTimeout ( this . nextTick ) ;
151
- var result = this . getCurrentResult ( this . final , cutoff ) ;
152
-
153
- if ( ! result ) {
154
- result = this . getCurrentResult ( this . interim , cutoff ) ;
155
- }
156
-
157
- if ( result ) {
129
+ var msg ;
130
+ // eslint-disable-next-line no-cond-assign
131
+ while ( msg = this . getCurrentResult ( ) ) {
158
132
if ( this . options . objectMode || this . options . readableObjectMode ) {
159
- this . push ( result ) ;
160
- } else {
161
- this . push ( result . results [ 0 ] . alternatives [ 0 ] . transcript ) ;
162
- }
163
- if ( result . results [ 0 ] . final ) {
164
- 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
165
- return ;
133
+ this . push ( msg ) ;
134
+ } else if ( Array . isArray ( msg . results && msg . results . length ) ) {
135
+ this . push ( msg . results [ 0 ] . alternatives [ 0 ] . transcript ) ;
166
136
}
167
137
}
168
138
169
- this . scheduleNextTick ( cutoff ) ;
139
+ this . scheduleNextTick ( ) ;
170
140
} ;
171
141
172
142
/**
173
143
* Given a speaker labels message, returns the final to time
144
+ *
145
+ * @private
174
146
* @param {Object } msg
175
147
* @returns {Number }
176
148
*/
@@ -180,7 +152,7 @@ function getEnd(msg) {
180
152
181
153
TimingStream . prototype . tickSpeakerLables = function tickSpeakerLabels ( ) {
182
154
clearTimeout ( this . nextSpeakerLabelsTick ) ;
183
- while ( this . speakerLabels . length && getEnd ( this . speakerLabels [ 0 ] ) <= this . cutoff ( ) ) {
155
+ if ( this . speakerLabels . length && getEnd ( this . speakerLabels [ 0 ] ) <= this . cutoff ( ) ) {
184
156
this . push ( this . speakerLabels . shift ( ) ) ;
185
157
}
186
158
if ( this . speakerLabels . length ) {
@@ -194,31 +166,18 @@ TimingStream.prototype.tickSpeakerLables = function tickSpeakerLabels() {
194
166
} ;
195
167
196
168
/**
197
- * Schedules next tick if possible. Requires previous stream to emit recognize objects (objectMode or readableObjectMode)
198
- *
199
- * triggers the 'close' and 'end' events if the buffer is empty and no further results are expected
200
- *
201
- * @param {Number } cutoff
169
+ * Schedules next tick or checks for the end of the results
202
170
*
171
+ * @private
203
172
*/
204
- TimingStream . prototype . scheduleNextTick = function scheduleNextTick ( cutoff ) {
205
-
206
- // prefer final results over interim - when final results are added, any older interim ones are automatically deleted.
207
- var nextResult = this . final [ 0 ] || this . interim [ 0 ] ;
208
- if ( nextResult ) {
209
- // loop through the timestamps until we find one that comes after the current cutoff (there should always be one)
210
- var timestamps = nextResult . results [ 0 ] . alternatives [ 0 ] . timestamps ;
211
- for ( var i = 0 ; i < timestamps . length ; i ++ ) {
212
- var wordOffset = timestamps [ i ] [ this . options . emitAt ] ;
213
- if ( wordOffset > cutoff ) {
214
- var nextTime = this . startTime + ( wordOffset * 1000 ) ;
215
- this . nextTick = setTimeout ( this . tick . bind ( this ) , nextTime - Date . now ( ) ) ;
216
- return ;
217
- }
218
- }
219
- 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
173
+ TimingStream . prototype . scheduleNextTick = function scheduleNextTick ( ) {
174
+ clearTimeout ( this . nextTick ) ; // just in case
175
+ if ( this . messages . length ) {
176
+ var messageTime = this . getMessageTime ( this . messages [ 0 ] ) ;
177
+ var nextTickTime = this . startTime + ( messageTime * 1000 ) ; // ms since epoch
178
+ var nextTickOffset = Math . min ( 0 , nextTickTime - Date . now ( ) ) ; // ms from right now
179
+ this . nextTick = setTimeout ( this . tick . bind ( this ) , nextTickOffset ) ;
220
180
} else {
221
- // clear the next tick
222
181
this . nextTick = null ;
223
182
this . checkForEnd ( ) ;
224
183
}
@@ -228,79 +187,25 @@ TimingStream.prototype.scheduleNextTick = function scheduleNextTick(cutoff) {
228
187
* Triggers the 'close' and 'end' events if both pre-conditions are true:
229
188
* - the previous stream must have already emitted it's 'end' event
230
189
* - there must be no next tick scheduled, indicating that there are no results buffered for later delivery
190
+ *
191
+ * @private
231
192
*/
232
193
TimingStream . prototype . checkForEnd = function ( ) {
233
- if ( this . sourceEnded && ! this . nextTick && ! this . nextSpeakerLabelsTick ) {
194
+ if ( this . sourceEnded && ! this . nextTick ) {
234
195
this . emit ( 'close' ) ;
235
196
this . push ( null ) ;
236
197
}
237
198
} ;
238
199
239
200
240
- /**
241
- * Creates a new result with all transcriptions formatted
242
- *
243
- * @param {Object } data
244
- */
245
- TimingStream . prototype . handleResult = function handleResult ( data ) {
246
- if ( noTimestamps ( data ) ) {
247
- var err = new Error ( 'TimingStream requires timestamps' ) ;
248
- err . name = noTimestamps . ERROR_NO_TIMESTAMPS ;
249
- this . emit ( 'error' , err ) ;
250
- return ;
251
- }
252
-
253
- // http://www.ibm.com/watson/developercloud/speech-to-text/api/v1/#SpeechRecognitionEvent
254
- var index = data . result_index ;
255
-
256
- // process each result individually
257
- data . results . forEach ( function ( result ) {
258
- // additional alternatives do not include timestamps, so we can't process and emit them correctly
259
- if ( result . alternatives . length > 1 ) {
260
- result . alternatives . length = 1 ;
261
- }
262
-
263
- // loop through the buffer and delete any interim results with the same or lower index
264
- while ( this . interim . length && this . interim [ 0 ] . result_index <= index ) {
265
- this . interim . shift ( ) ;
266
- }
267
-
268
- // in case this data object had multiple results in it
269
- var newData = {
270
- results : [ result ] ,
271
- result_index : index
272
- } ;
273
-
274
- index ++ ;
275
-
276
- if ( result . final ) {
277
- // then add it to the final results array
278
- this . final . push ( newData ) ;
279
- // and reset the interim results array because anything there has now been superseded and should not be emitted.
280
- this . interim = [ ] ;
281
- } else {
282
- this . interim . push ( newData ) ;
283
- }
284
-
285
- } , this ) ;
286
-
287
- this . tick ( ) ;
288
- } ;
289
-
290
- TimingStream . prototype . handleSpeakerLabels = function handleSpeakerLabels ( data ) {
291
- this . speakerLabels . push ( data ) ;
292
- this . tickSpeakerLables ( ) ;
293
- } ;
294
-
295
201
TimingStream . prototype . promise = require ( './to-promise' ) ;
296
202
297
203
// when stop is called, immediately stop emitting results
298
204
TimingStream . prototype . stop = function stop ( ) {
299
205
this . emit ( 'stop' ) ;
300
- clearTimeout ( this . nextTick ) ;
301
- clearTimeout ( this . nextSpeakerLabelsTick ) ;
302
- this . handleResult = this . handleSpeakerLabels = function noop ( ) { } ; // RecognizeStream.stop() closes the connection gracefully, so we will usually see one more result
303
206
this . checkForEnd ( ) ; // in case the RecognizeStream already ended
207
+ clearTimeout ( this . nextTick ) ;
208
+ this . nextTick = - 1 ; // fake timer to prevent _write from scheduling new ticks
304
209
} ;
305
210
306
211
module . exports = TimingStream ;
0 commit comments