You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

transcriber.js 12KB

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