浏览代码

added the Transcriber module

dev1
nikvaessen 9 年前
父节点
当前提交
88235aaeed

+ 306
- 0
modules/transcription/audioRecorder.js 查看文件

@@ -0,0 +1,306 @@
1
+/* global APP, MediaRecorder, MediaStream, webkitMediaStream*/
2
+var RecordingResult = require("./recordingResult");
3
+
4
+/**
5
+ * Possible audio formats MIME types
6
+ */
7
+var AUDIO_WEBM = "audio/webm";    // Supported in chrome
8
+var AUDIO_OGG  = "audio/ogg";     // Supported in firefox
9
+
10
+/**
11
+ * A TrackRecorder object holds all the information needed for recording a
12
+ * single JitsiTrack (either remote or local)
13
+ * @param track The JitsiTrack the object is going to hold
14
+ */
15
+var TrackRecorder = function(track){
16
+    // The JitsiTrack holding the stream
17
+    this.track = track;
18
+    // The MediaRecorder recording the stream
19
+    this.recorder = null;
20
+    // The array of data chunks recorded from the stream
21
+    // acts as a buffer until the data is stored on disk
22
+    this.data = null;
23
+    //the name of the person of the JitsiTrack. This can be undefined and/or
24
+    //not unique
25
+    this.name = null;
26
+    //the time of the start of the recording
27
+    this.startTime = null;
28
+};
29
+
30
+/**
31
+ * Starts the recording of a JitsiTrack in a TrackRecorder object.
32
+ * This will also define the timestamp and try to update the name
33
+ * @param trackRecorder the TrackRecorder to start
34
+ */
35
+function startRecorder(trackRecorder) {
36
+    if(trackRecorder.recorder === undefined) {
37
+        throw new Error("Passed an object to startRecorder which is not a " +
38
+            "TrackRecorder object");
39
+    }
40
+    trackRecorder.recorder.start();
41
+    trackRecorder.startTime = new Date();
42
+}
43
+
44
+/**
45
+ * Stops the recording of a JitsiTrack in a TrackRecorder object.
46
+ * This will also try to update the name
47
+ * @param trackRecorder the TrackRecorder to stop
48
+ */
49
+function stopRecorder(trackRecorder){
50
+    if(trackRecorder.recorder === undefined) {
51
+        throw new Error("Passed an object to stopRecorder which is not a " +
52
+            "TrackRecorder object");
53
+    }
54
+    trackRecorder.recorder.stop();
55
+}
56
+
57
+/**
58
+ * Creates a TrackRecorder object. Also creates the MediaRecorder and
59
+ * data array for the trackRecorder.
60
+ * @param track the JitsiTrack holding the audio MediaStream(s)
61
+ */
62
+function instantiateTrackRecorder(track) {
63
+    var trackRecorder = new TrackRecorder(track);
64
+    // Create a new stream which only holds the audio track
65
+    var originalStream = trackRecorder.track.getOriginalStream();
66
+    var stream = createEmptyStream();
67
+    originalStream.getAudioTracks().forEach(function(track){
68
+        stream.addTrack(track);
69
+    });
70
+    // Create the MediaRecorder
71
+    trackRecorder.recorder = new MediaRecorder(stream,
72
+        {mimeType: audioRecorder.fileType});
73
+    //array for holding the recorder data. Resets it when
74
+    //audio already has been recorder once
75
+    trackRecorder.data = [];
76
+    // function handling a dataEvent, e.g the stream gets new data
77
+    trackRecorder.recorder.ondataavailable = function (dataEvent) {
78
+        if(dataEvent.data.size > 0) {
79
+            trackRecorder.data.push(dataEvent.data);
80
+        }
81
+    };
82
+
83
+    return trackRecorder;
84
+}
85
+
86
+/**
87
+ * Determines which kind of audio recording the browser supports
88
+ * chrome supports "audio/webm" and firefox supports "audio/ogg"
89
+ */
90
+function determineCorrectFileType() {
91
+    if(MediaRecorder.isTypeSupported(AUDIO_WEBM)) {
92
+        return AUDIO_WEBM;
93
+    }
94
+    else if(MediaRecorder.isTypeSupported(AUDIO_OGG)) {
95
+        return AUDIO_OGG;
96
+    }
97
+    else {
98
+        throw new Error("unable to create a MediaRecorder with the" +
99
+            "right mimetype!");
100
+    }
101
+}
102
+
103
+/**
104
+ * main exported object of the file, holding all
105
+ * relevant functions and variables for the outside world
106
+ * @param jitsiConference the jitsiConference which this object
107
+ * is going to record
108
+ */
109
+var audioRecorder = function(jitsiConference){
110
+    // array of TrackRecorders, where each trackRecorder
111
+    // holds the JitsiTrack, MediaRecorder and recorder data
112
+    this.recorders = [];
113
+
114
+    //get which file type is supported by the current browser
115
+    this.fileType = determineCorrectFileType();
116
+
117
+    //boolean flag for active recording
118
+    this.isRecording = false;
119
+
120
+    //the jitsiconference the object is recording
121
+    this.jitsiConference = jitsiConference;
122
+};
123
+
124
+/**
125
+ * Add the the exported module so that it can be accessed by other files
126
+ */
127
+audioRecorder.determineCorrectFileType = determineCorrectFileType;
128
+
129
+/**
130
+ * Adds a new TrackRecorder object to the array.
131
+ *
132
+ * @param track the track potentially holding an audio stream
133
+ */
134
+audioRecorder.prototype.addTrack = function (track) {
135
+    if(track.isAudioTrack()) {
136
+        //create the track recorder
137
+        var trackRecorder = instantiateTrackRecorder(track);
138
+        //push it to the local array of all recorders
139
+        this.recorders.push(trackRecorder);
140
+        //update the name of the trackRecorders
141
+        this.updateNames();
142
+        //if we're already recording, immediately start recording this new track
143
+        if(this.isRecording){
144
+            startRecorder(trackRecorder);
145
+        }
146
+    }
147
+};
148
+
149
+/**
150
+ * Notifies the module that a specific track has stopped, e.g participant left
151
+ * the conference.
152
+ * if the recording has not started yet, the TrackRecorder will be removed from
153
+ * the array. If the recording has started, the recorder will stop recording
154
+ * but not removed from the array so that the recorded stream can still be
155
+ * accessed
156
+ *
157
+ * @param jitsiTrack the JitsiTrack to remove from the recording session
158
+ */
159
+audioRecorder.prototype.removeTrack = function(jitsiTrack){
160
+    var array = this.recorders;
161
+    var i;
162
+    for(i = 0; i < array.length; i++) {
163
+        if(array[i].track.getParticipantId() === jitsiTrack.getParticipantId()){
164
+            var recorderToRemove = array[i];
165
+            if(this.isRecording){
166
+                stopRecorder(recorderToRemove);
167
+            }
168
+            else {
169
+                //remove the TrackRecorder from the array
170
+                array.splice(i, 1);
171
+            }
172
+        }
173
+    }
174
+
175
+    //make sure the names are up to date
176
+    this.updateNames();
177
+};
178
+
179
+/**
180
+ * Tries to update the name value of all TrackRecorder in the array.
181
+ * If it hasn't changed,it will keep the exiting name. If it changes to a
182
+ * undefined value, the old value will also be kept.
183
+ */
184
+audioRecorder.prototype.updateNames = function(){
185
+    var conference = this.jitsiConference;
186
+    this.recorders.forEach(function(trackRecorder){
187
+        if(trackRecorder.track.isLocal()){
188
+            trackRecorder.name = "the transcriber";
189
+        }
190
+        else {
191
+            var id = trackRecorder.track.getParticipantId();
192
+            var participant = conference.getParticipantById(id);
193
+            var newName = participant.getDisplayName();
194
+            if(newName !== 'undefined') {
195
+                trackRecorder.name = newName;
196
+            }
197
+        }
198
+    });
199
+};
200
+
201
+/**
202
+ * Starts the audio recording of every local and remote track
203
+ */
204
+audioRecorder.prototype.start = function () {
205
+    if(this.isRecording) {
206
+        throw new Error("audiorecorder is already recording");
207
+    }
208
+    // set boolean isRecording flag to true so if new participants join the
209
+    // conference, that track can instantly start recording as well
210
+    this.isRecording = true;
211
+    //start all the mediaRecorders
212
+    this.recorders.forEach(function(trackRecorder){
213
+        startRecorder(trackRecorder);
214
+    });
215
+    //log that recording has started
216
+    console.log("Started the recording of the audio. There are currently " +
217
+        this.recorders.length + " recorders active.");
218
+};
219
+
220
+/**
221
+ * Stops the audio recording of every local and remote track
222
+ */
223
+audioRecorder.prototype.stop = function() {
224
+    //set the boolean flag to false
225
+    this.isRecording = false;
226
+    //stop all recorders
227
+    this.recorders.forEach(function(trackRecorder){
228
+       stopRecorder(trackRecorder);
229
+    });
230
+    console.log("stopped recording");
231
+};
232
+
233
+/**
234
+ * link hacking to download all recorded audio streams
235
+ */
236
+audioRecorder.prototype.download = function () {
237
+    var t = this;
238
+    this.recorders.forEach(function (trackRecorder) {
239
+        var blob = new Blob(trackRecorder.data, {type: t.fileType});
240
+        var url = URL.createObjectURL(blob);
241
+        var a = document.createElement('a');
242
+        document.body.appendChild(a);
243
+        a.style = "display: none";
244
+        a.href = url;
245
+        a.download = 'test.' + t.fileType.split("/")[1];
246
+        a.click();
247
+        window.URL.revokeObjectURL(url);
248
+    });
249
+};
250
+
251
+/**
252
+ * returns the audio files of all recorders as an array of objects,
253
+ * which include the name of the owner of the track and the starting time stamp
254
+ * @returns {Array} an array of RecordingResult objects
255
+ */
256
+audioRecorder.prototype.getRecordingResults = function () {
257
+    if(this.isRecording) {
258
+        throw new Error("cannot get blobs because the AudioRecorder is still" +
259
+            "recording!");
260
+    }
261
+    //make sure the names are up to date before sending them off
262
+    this.updateNames();
263
+
264
+    var array = [];
265
+    var t = this;
266
+    this.recorders.forEach(function (recorder) {
267
+        array.push(
268
+            new RecordingResult(
269
+            new Blob(recorder.data, {type: t.fileType}),
270
+            recorder.name,
271
+            recorder.startTime)
272
+        );
273
+    });
274
+    return array;
275
+};
276
+
277
+/**
278
+ * Gets the mime type of the recorder audio
279
+ * @returns {String} the mime type of the recorder audio
280
+ */
281
+audioRecorder.prototype.getFileType = function () {
282
+    return this.fileType;
283
+};
284
+
285
+/**
286
+ * Creates a empty MediaStream object which can be used
287
+ * to add MediaStreamTracks to
288
+ * @returns MediaStream
289
+ */
290
+function createEmptyStream() {
291
+    // Firefox supports the MediaStream object, Chrome webkitMediaStream
292
+    if(typeof(MediaStream) !== 'undefined') {
293
+        return new MediaStream();
294
+    }
295
+    else if(typeof(webkitMediaStream) !== 'undefined') {
296
+        return new webkitMediaStream();
297
+    }
298
+    else {
299
+        throw new Error("cannot create a clean mediaStream");
300
+    }
301
+}
302
+
303
+/**
304
+ * export the main object audioRecorder
305
+ */
306
+module.exports = audioRecorder;

+ 18
- 0
modules/transcription/recordingResult.js 查看文件

@@ -0,0 +1,18 @@
1
+/**
2
+ * This object stores variables needed around the recording of an audio stream
3
+ * and passing this recording along with additional information along to
4
+ * different processes
5
+ * @param blob the recording audio stream as a single blob
6
+ * @param name the name of the person of the audio stream
7
+ * @param startTime the time in UTC when recording of the audiostream started
8
+ * @param wordArray the recorder audio stream transcribed as an array of Word
9
+ *                  objects
10
+ */
11
+var RecordingResult = function(blob, name, startTime, wordArray){
12
+    this.blob = blob;
13
+    this.name = name;
14
+    this.startTime = startTime;
15
+    this.wordArray = wordArray;
16
+};
17
+
18
+module.exports = RecordingResult;

+ 317
- 0
modules/transcription/transcriber.js 查看文件

@@ -0,0 +1,317 @@
1
+var AudioRecorder = require( './audioRecorder');
2
+var SphinxService = require(
3
+    './transcriptionServices/SphinxTranscriptionService');
4
+
5
+var BEFORE_STATE = "before";
6
+var RECORDING_STATE = "recording";
7
+var TRANSCRIBING_STATE = "transcribing";
8
+var FINISHED_STATE = "finished";
9
+
10
+//the amount of characters each line in the transcription will have
11
+var MAXIMUM_SENTENCE_LENGTH = 80;
12
+
13
+/**
14
+ * This is the main object for handing the Transcription. It interacts with
15
+ * the audioRecorder to record every person in a conference and sends the
16
+ * recorder audio to a transcriptionService. The returned speech-to-text result
17
+ * will be merged to create a transcript
18
+ * @param {AudioRecorder} audioRecorder An audioRecorder recording a conference
19
+ */
20
+var transcriber = function(audioRecorder) {
21
+    //the object which can record all audio in the conference
22
+    this.audioRecorder = audioRecorder;
23
+    //this object can send the recorder audio to a speech-to-text service
24
+    this.transcriptionService =  new SphinxService();
25
+    //holds a counter to keep track if merging can start
26
+    this.counter = null;
27
+    //holds the date when transcription started which makes it possible
28
+    //to calculate the offset between recordings
29
+    this.startTime = null;
30
+    //will hold the transcription once it is completed
31
+    this.transcription = null;
32
+    //this will be a method which will be called once the transcription is done
33
+    //with the transcription as parameter
34
+    this.callback =  null;
35
+    //stores all the retrieved speech-to-text results to merge together
36
+    //this value will store an Array<Word> object
37
+    this.results = [];
38
+    // Stores the current state of the transcription process
39
+    this.state = BEFORE_STATE;
40
+    //Used in the updateTranscription method to add a new line when the
41
+    //sentence becomes to long
42
+    this.lineLength = 0;
43
+};
44
+
45
+/**
46
+ * Method to start the transcription process. It will tell the audioRecorder
47
+ * to start storing all audio streams and record the start time for merging
48
+ * purposes
49
+ */
50
+transcriber.prototype.start = function start() {
51
+    if(this.state !== BEFORE_STATE){
52
+        throw new Error("The transcription can only start when it's in the" +
53
+            "\"" + BEFORE_STATE + "\" state. It's currently in the " +
54
+            "\"" + this.state + "\" state");
55
+    }
56
+    this.state = RECORDING_STATE;
57
+    this.audioRecorder.start();
58
+    this.startTime = new Date();
59
+};
60
+
61
+/**
62
+ * Method to stop the transcription process. It will tell the audioRecorder to
63
+ * stop, and get all the recorded audio to send it to the transcription service
64
+
65
+ * @param callback a callback which will receive the transcription
66
+ */
67
+transcriber.prototype.stop = function stop(callback) {
68
+    if(this.state !== RECORDING_STATE){
69
+        throw new Error("The transcription can only stop when it's in the" +
70
+            "\"" + RECORDING_STATE + "\" state. It's currently in the " +
71
+            "\"" + this.state + "\" state");
72
+    }
73
+    //stop the recording
74
+    console.log("stopping recording and sending audio files");
75
+    this.audioRecorder.stop();
76
+    //and send all recorded audio the the transcription service
77
+    var t = this;
78
+
79
+    var callBack = blobCallBack.bind(this);
80
+    this.audioRecorder.getRecordingResults().forEach(function(recordingResult){
81
+        t.transcriptionService.send(recordingResult, callBack);
82
+        t.counter++;
83
+    });
84
+    //set the state to "transcribing" so that maybeMerge() functions correctly
85
+    this.state = TRANSCRIBING_STATE;
86
+    //and store the callback for later
87
+    this.callback = callback;
88
+};
89
+
90
+/**
91
+ * This method gets the answer from the transcription service, calculates the
92
+ * offset and adds is to every Word object. It will also start the merging
93
+ * when every send request has been received
94
+ *
95
+ * note: Make sure to bind this as a Transcription object
96
+ *
97
+ * @param {RecordingResult} answer a RecordingResult object with a defined
98
+ * WordArray
99
+ */
100
+var blobCallBack = function(answer){
101
+    console.log("retrieved an answer from the transcription service. The" +
102
+        " answer has an array of length: " + answer.wordArray.length);
103
+    //first add the offset between the start of the transcription and
104
+    //the start of the recording to all start and end times
105
+    if(answer.wordArray.length > 0) {
106
+        var offset = answer.startTime.getUTCMilliseconds() -
107
+            this.startTime.getUTCMilliseconds();
108
+        //transcriber time will always be earlier
109
+        if (offset < 0) {
110
+            offset = 0; //presume 0 if it somehow not earlier
111
+        }
112
+
113
+        var array = "[";
114
+        answer.wordArray.forEach(function(wordObject) {
115
+            wordObject.begin += offset;
116
+            wordObject.end += offset;
117
+            array += wordObject.word+",";
118
+        });
119
+        array += "]";
120
+        console.log(array);
121
+        //give a name value to the Array object so that the merging can access
122
+        //the name value without having to use the whole recordingResult object
123
+        //in the algorithm
124
+        answer.wordArray.name = answer.name;
125
+    }
126
+    //then store the array and decrease the counter
127
+    this.results.push(answer.wordArray);
128
+    this.counter--;
129
+    console.log("current counter: " + this.counter);
130
+    //and check if all results have been received.
131
+    this.maybeMerge();
132
+};
133
+
134
+/**
135
+ * this method will check if the counter is zero. If it is, it will call
136
+ * the merging method
137
+ */
138
+transcriber.prototype.maybeMerge = function(){
139
+    if(this.state === TRANSCRIBING_STATE && this.counter === 0){
140
+        //make sure to include the events in the result arrays before
141
+        //merging starts
142
+        this.merge();
143
+    }
144
+};
145
+
146
+/**
147
+ * This method will merge all speech-to-text arrays together in one
148
+ * readable transcription string
149
+ */
150
+transcriber.prototype.merge = function() {
151
+    console.log("starting merge process!\n The length of the array: " +
152
+        this.results.length);
153
+    this.transcription = "";
154
+    //the merging algorithm will look over all Word objects who are at pos 0 in
155
+    //every array. It will then select the one closest in time to the
156
+    //previously placed word, while removing the selected word from its array
157
+    //note: words can be skipped the skipped word's begin and end time somehow
158
+    //end up between the closest word start and end time
159
+    var arrays = this.results;
160
+    //arrays of Word objects
161
+    var potentialWords = []; //array of the first Word objects
162
+    //check if any arrays are already empty and remove them
163
+    hasPopulatedArrays(arrays);
164
+
165
+    //populate all the potential Words for a first time
166
+    arrays.forEach(function (array){
167
+        pushWordToSortedArray(potentialWords, array);
168
+    });
169
+
170
+    //keep adding words to transcription until all arrays are exhausted
171
+    var lowestWordArray;
172
+    var wordToAdd;
173
+    var foundSmaller;
174
+    while(hasPopulatedArrays(arrays)){
175
+        //first select the lowest array;
176
+        lowestWordArray = arrays[0];
177
+        arrays.forEach(function(wordArray){
178
+           if(wordArray[0].begin < lowestWordArray[0].begin){
179
+               lowestWordArray = wordArray;
180
+           }
181
+        });
182
+        //put the word in the transcription
183
+        wordToAdd = lowestWordArray.shift();
184
+        this.updateTranscription(wordToAdd,lowestWordArray.name);
185
+
186
+        //keep going until a word in another array has a smaller time
187
+        //or the array is empty
188
+        while(!foundSmaller && lowestWordArray.length > 0){
189
+            arrays.forEach(function(wordArray){
190
+                if(wordArray[0].begin < lowestWordArray[0].begin){
191
+                    foundSmaller = true;
192
+                }
193
+            });
194
+            //add next word if no smaller time has been found
195
+            if(!foundSmaller){
196
+                wordToAdd = lowestWordArray.shift();
197
+                this.updateTranscription(wordToAdd, null);
198
+            }
199
+        }
200
+
201
+    }
202
+
203
+    //set the state to finished and do the necessary left-over tasks
204
+    this.state = FINISHED_STATE;
205
+    if(this.callback){
206
+        this.callback(this.transcription);
207
+    }
208
+};
209
+
210
+/**
211
+ * Appends a word object to the transcription. It will make a new line with a
212
+ * name if a name is specified
213
+ * @param {Word} word the Word object holding the word to append
214
+ * @param {String|null} name the name of a new speaker. Null if not applicable
215
+ */
216
+transcriber.prototype.updateTranscription = function(word, name){
217
+    if(name !== undefined && name !== null){
218
+        this.transcription += "\n" + name + ":";
219
+        this.lineLength = name.length + 1; //+1 for the semi-colon
220
+    }
221
+    if(this.lineLength + word.word.length > MAXIMUM_SENTENCE_LENGTH){
222
+        this.transcription += "\n    ";
223
+        this.lineLength = 4; //because of the 4 spaces after the new line
224
+    }
225
+    this.transcription += " " + word.word;
226
+    this.lineLength += word.word.length + 1; //+1 for the space
227
+};
228
+
229
+/**
230
+ * Check if the given 2 dimensional array has any non-zero Word-arrays in them.
231
+ * All zero-element arrays inside will be removed
232
+ * If any non-zero-element arrays are found, the method will return true.
233
+ * otherwise it will return false
234
+ * @param {Array<Array>} twoDimensionalArray the array to check
235
+ * @returns {boolean} true if any non-zero arrays inside, otherwise false
236
+ */
237
+var hasPopulatedArrays = function(twoDimensionalArray){
238
+    var i;
239
+    for(i = 0; i < twoDimensionalArray.length; i++){
240
+        if(twoDimensionalArray[i].length === 0){
241
+            twoDimensionalArray.splice(i, 1);
242
+        }
243
+    }
244
+    return twoDimensionalArray.length > 0;
245
+};
246
+
247
+/**
248
+ * Push a word to the right location in a sorted array. The array is sorted
249
+ * from lowest to highest start time. Every word is stored in an object which
250
+ * includes the name of the person saying the word.
251
+ *
252
+ * @param {Array<Word>} array the sorted array to push to
253
+ * @param {Word} word the word to push into the array
254
+ */
255
+var pushWordToSortedArray = function(array, word){
256
+    if(array.length === 0) {
257
+        array.push(word);
258
+    }
259
+    else{
260
+        if(array[array.length - 1].begin <= word.begin){
261
+            array.push(word);
262
+            return;
263
+        }
264
+        var i;
265
+        for(i = 0; i < array.length; i++){
266
+            if(word.begin < array[i].begin){
267
+                array.splice(i, 0, word);
268
+                return;
269
+            }
270
+        }
271
+        array.push(word); //fail safe
272
+    }
273
+};
274
+
275
+/**
276
+ * Returns the AudioRecorder module to add and remove tracks to
277
+ */
278
+transcriber.getAudioRecorder = function getAudioRecorder() {
279
+    return this.audioRecorder;
280
+};
281
+
282
+/**
283
+ * Will return the created transcription if it's avialable or throw an error
284
+ * when it's not done yet
285
+ * @returns {String} the transcription as a String
286
+ */
287
+transcriber.prototype.getTranscription = function(){
288
+    if(this.state !== FINISHED_STATE){
289
+        throw new Error("The transcription can only be retrieved when it's in" +
290
+            " the\"" + FINISHED_STATE + "\" state. It's currently in the " +
291
+            "\"" + this.state + "\" state");
292
+    }
293
+    return this.transcription;
294
+};
295
+
296
+/**
297
+ * Returns the current state of the transcription process
298
+ */
299
+transcriber.prototype.getState = function(){
300
+    return this.state;
301
+};
302
+
303
+/**
304
+ * Resets the state to the "before" state, such that it's again possible to
305
+ * call the start method
306
+ */
307
+transcriber.prototype.reset = function() {
308
+    this.state = BEFORE_STATE;
309
+    this.counter = null;
310
+    this.transcription = null;
311
+    this.startTime = null;
312
+    this.callback = null;
313
+    this.results = [];
314
+    this.lineLength = 0;
315
+};
316
+
317
+module.exports = transcriber;

+ 77
- 0
modules/transcription/transcriptionServices/AbstractTranscriptionService.js 查看文件

@@ -0,0 +1,77 @@
1
+/**
2
+ * Abstract class representing an interface to implement a speech-to-text
3
+ * service on.
4
+ */
5
+var TranscriptionService = function() {
6
+    throw new Error("TranscriptionService is abstract and cannot be" +
7
+        "created");
8
+};
9
+
10
+/**
11
+ * This method can be used to send the recorder audio stream and
12
+ * retrieve the answer from the transcription service from the callback
13
+ *
14
+ * @param {RecordingResult} recordingResult a recordingResult object which
15
+ * includes the recorded audio stream as a blob
16
+ * @param {Function} callback  which will retrieve the a RecordingResult with
17
+ *        the answer as a WordArray
18
+ */
19
+TranscriptionService.prototype.send = function send(recordingResult, callback){
20
+    var t = this;
21
+    this.sendRequest(recordingResult.blob, function(response){
22
+        if(!t.verify(response)){
23
+               console.log("the retrieved response from the server" +
24
+                   " is not valid!");
25
+            recordingResult.wordArray = [];
26
+            callback(recordingResult);
27
+        }
28
+        else{
29
+            recordingResult.wordArray = t.formatResponse(response);
30
+            callback(recordingResult);
31
+        }
32
+    });
33
+};
34
+
35
+/**
36
+ * Abstract method which will rend the recorder audio stream to the implemented
37
+ * transcription service and will retrieve an answer, which will be
38
+ * called on the given callback method
39
+ *
40
+ * @param {Blob} audioBlob the recorded audio stream as a single Blob
41
+ * @param {function} callback function which will retrieve the answer
42
+ *                            from the service
43
+ */
44
+TranscriptionService.prototype.sendRequest = function(audioBlob, callback) {
45
+    throw new Error("TranscriptionService.sendRequest is abstract");
46
+};
47
+
48
+/**
49
+ * Abstract method which will parse the output from the implemented
50
+ * transcription service to the expected format
51
+ *
52
+ * The transcriber class expect an array of word objects, where each word
53
+ * object is one transcribed word by the service.
54
+ *
55
+ * The expected output of this method is an array of word objects, in
56
+ * the correct order. That is, the first object in the array is the first word
57
+ * being said, and the last word in the array is the last word being said
58
+ *
59
+ * @param response the answer from the speech-to-text server which needs to be
60
+ *                 formatted
61
+ * @return {Array<Word>} an array of Word objects
62
+ */
63
+TranscriptionService.prototype.formatResponse = function(response){
64
+    throw new Error("TranscriptionService.format is abstract");
65
+};
66
+
67
+/**
68
+ * Abstract method which will verify that the response from the server is valid
69
+ *
70
+ * @param response the response from the server
71
+ * @return {boolean} true if response is valid, false otherwise
72
+ */
73
+TranscriptionService.prototype.verify = function(response){
74
+      throw new Error("TranscriptionService.verify is abstract");
75
+};
76
+
77
+module.exports = TranscriptionService;

+ 130
- 0
modules/transcription/transcriptionServices/SphinxTranscriptionService.js 查看文件

@@ -0,0 +1,130 @@
1
+/* global config,  XMLHttpRequest, console, APP, JSON */
2
+
3
+var TranscriptionService = require("./AbstractTranscriptionService");
4
+var Word = require( "../word");
5
+var audioRecorder = require("./../audioRecorder");
6
+
7
+/**
8
+ * Implements a TranscriptionService for a Sphinx4 http server
9
+ */
10
+var SphinxService = function() {
11
+    //set the correct url
12
+    this.url = getURL();
13
+};
14
+
15
+/**
16
+ * Subclass of AbstractTranscriptionService
17
+ */
18
+SphinxService.prototype = Object.create(TranscriptionService.prototype);
19
+
20
+/**
21
+ * Set the right constructor
22
+ */
23
+SphinxService.constructor = SphinxService;
24
+
25
+/**
26
+ * Overrides the sendRequest method from AbstractTranscriptionService
27
+ * it will send the audio stream the a Sphinx4 server to get the transcription
28
+ *
29
+ * @param audioFileBlob the recorder audio stream an a single Blob
30
+ * @param callback the callback function retrieving the server response
31
+ */
32
+SphinxService.prototype.sendRequest = function(audioFileBlob, callback) {
33
+    console.log("sending an audio file  to " + this.url);
34
+    console.log("the audio file being sent: " + audioFileBlob);
35
+    var request = new XMLHttpRequest();
36
+    request.onreadystatechange = function() {
37
+        if(request.readyState === XMLHttpRequest.DONE && request.status === 200)
38
+        {
39
+            callback(request.responseText);
40
+        }
41
+        else if (request.readyState === XMLHttpRequest.DONE) {
42
+            throw new Error("unable to accept response from sphinx server." +
43
+                "status: " + request.status);
44
+        }
45
+        //if not ready no point to throw an error
46
+    };
47
+    request.open("POST", this.url);
48
+    request.setRequestHeader("Content-Type",
49
+        audioRecorder.determineCorrectFileType());
50
+    request.send(audioFileBlob);
51
+    console.log("send " + audioFileBlob);
52
+};
53
+
54
+/**
55
+ * Overrides the formatResponse method from AbstractTranscriptionService
56
+ * It will parse the answer from the server in the expected format
57
+ *
58
+ * @param response the JSON body retrieved from the Sphinx4 server
59
+ */
60
+SphinxService.prototype.formatResponse = function(response) {
61
+    var result = JSON.parse(response).objects;
62
+    //make sure to delete the session id object, which is always
63
+    //the first value in the JSON array
64
+    result.shift();
65
+    var array = [];
66
+    result.forEach(function(word){
67
+        if(!word.filler) {
68
+            array.push(new Word(word.word, word.start, word.end));
69
+        }
70
+    });
71
+    return array;
72
+};
73
+
74
+/**
75
+ * checks wether the reply is empty, or doesn't contain a correct JSON object
76
+ * @param response the server response
77
+ * @return {boolean} whether the response is valid
78
+ */
79
+SphinxService.prototype.verify = function(response){
80
+    console.log("response from server:" + response.toString());
81
+    //test if server responded with a string object
82
+    if(typeof(response) !== "string"){
83
+        return false;
84
+    }
85
+    //test if the string can be parsed into valid JSON
86
+    var json;
87
+    try{
88
+        json = JSON.parse(response);
89
+    }
90
+    catch (error){
91
+        console.log(error);
92
+        return false;
93
+    }
94
+    //check if the JSON has a "objects" value
95
+    if(json.objects === undefined){
96
+        return false;
97
+    }
98
+    //get the "objects" value and check for a session ID
99
+    var array = json.objects;
100
+    if(!(array[0] && array[0]["session-id"])){
101
+        return false;
102
+    }
103
+    //everything seems to be in order
104
+    return true;
105
+};
106
+
107
+/**
108
+ * Gets the URL to the Sphinx4 server from the config file. If it's not there,
109
+ * it will throw an error
110
+ *
111
+ * @returns {string} the URL to the sphinx4 server
112
+ */
113
+function getURL() {
114
+    var message = "config does not contain an url to a " +
115
+    "Sphinx4 https server";
116
+    if(config.sphinxURL === undefined){
117
+        throw new Error(message);
118
+    }
119
+    else {
120
+        var toReturn = config.sphinxURL;
121
+        if(toReturn.includes !== undefined && toReturn.includes("https://")){
122
+            return toReturn;
123
+        }
124
+        else{
125
+            throw new Error(message);
126
+        }
127
+    }
128
+}
129
+
130
+module.exports = SphinxService;

+ 37
- 0
modules/transcription/word.js 查看文件

@@ -0,0 +1,37 @@
1
+/**
2
+ * An object representing a transcribed word, with some additional information
3
+ * @param word the word 
4
+ * @param begin the time the word was started being uttered
5
+ * @param end the tome the word stopped being uttered
6
+ */
7
+var Word = function (word, begin, end, name) {
8
+    this.word = word;
9
+    this.begin = begin;
10
+    this.end = end;
11
+};
12
+
13
+/**
14
+ * Get the string representation of the word
15
+ * @returns {*} the word as a string
16
+ */
17
+Word.prototype.getWord = function() {
18
+    return this.word;  
19
+};
20
+
21
+/**
22
+ * Get the time the word started being uttered
23
+ * @returns {*} the start time as an integer
24
+ */
25
+Word.prototype.getBeginTime = function () {
26
+    return this.begin;
27
+};
28
+
29
+/**
30
+ * Get the time the word stopped being uttered
31
+ * @returns {*} the end time as an integer
32
+ */
33
+Word.prototype.getEndTime = function () {
34
+    return this.end;
35
+};
36
+
37
+module.exports = Word;

正在加载...
取消
保存