source: main/trunk/model-interfaces-dev/atea/js/asr/asr-controller.js@ 35282

Last change on this file since 35282 was 35282, checked in by davidb, 3 years ago

Start transition to Vue for layout composition

File size: 10.0 KB
Line 
1/**
2 * @file Controller logic for asr.xsl.
3 * @author Carl Stephens
4 */
5
6/** @type {HTMLAudioElement} */
7// @ts-ignore
8const TRANSCRIPTION_AUDIO_ELEMENT = document.getElementById("transcriptionAudio");
9
10/** @type {HTMLSourceElement} */
11// @ts-ignore
12const TRANSCRIPTION_AUDIO_SOURCE_ELEMENT = document.getElementById("transcriptionAudioSource");
13
14/** @type {HTMLUListElement} */
15// @ts-ignore
16const TRANSCRIPTIONS_LIST = document.getElementById("transcriptionsList");
17
18/** @type {HTMLTemplateElement} */
19// @ts-ignore
20const TRANSCRIPTION_TEMPLATE = document.getElementById("transcriptionTemplate");
21
22/** @type {HTMLTemplateElement} */
23// @ts-ignore
24const ERROR_TEMPLATE = document.getElementById("errorTemplate");
25
26let cachedAudioFileList = new Map();
27
28/**
29* @callback loadScriptCallback
30* @param {Event} ev The event
31* @returns {void}
32*/
33
34// Get the size of each character in our monospace font
35var MONOSPACE_CHAR_SIZE;
36window.addEventListener("load", function()
37{
38 const monoCharSizeTestElement = document.querySelector(".monospace-font-size");
39 if (monoCharSizeTestElement == null || monoCharSizeTestElement.textContent == null) {
40 MONOSPACE_CHAR_SIZE = 8; // Slightly over-estimated guess for size 16 font.
41 return;
42 }
43
44 MONOSPACE_CHAR_SIZE = monoCharSizeTestElement.clientWidth / monoCharSizeTestElement.textContent.length;
45});
46
47/**
48* Loads a remote script.
49* Found at https://stackoverflow.com/questions/950087/how-do-i-include-a-javascript-file-in-another-javascript-file
50*
51* @param {string} url The URL from which to load the script.
52*/
53function loadScript(url, callback = () => {})
54{
55 var body = document.body;
56
57 var script = document.createElement('script');
58 script.type = 'text/javascript';
59 script.src = url;
60
61 script.addEventListener("load", callback);
62
63 body.appendChild(script);
64}
65
66var transcribeService;// = new TranscribeService();
67loadScript("./interfaces/atea/js/asr/TranscribeService.js", () => {
68 transcribeService = new TranscribeService();
69});
70
71// @ts-ignore
72const AudioUploadComponent = Vue.createApp(
73{
74 data()
75 {
76 return {
77 files: undefined,
78 canTranscribe: false,
79 isTranscribing: false
80 }
81 },
82 methods:
83 {
84 openFilePicker() {
85 document.getElementById("audioFileInput")?.click();
86 },
87 onFilesChanged()
88 {
89 /** @type {HTMLInputElement} */
90 // @ts-ignore | Object could be null
91 const audioFileInput = document.getElementById("audioFileInput");
92
93 if (audioFileInput.files?.length != undefined && audioFileInput.files?.length > 0) {
94 this.canTranscribe = true;
95 this.files = audioFileInput?.files
96 }
97 else {
98 this.canTranscribe = false;
99 this.files = undefined;
100 }
101 },
102 async doTranscription() {
103 await getTranscriptions();
104 }
105 }
106});
107const AudioUploadVM = AudioUploadComponent.mount("#audioUploadContainer");
108
109/**
110 * When awaited, creates a delay for the given number of milliseconds.
111 *
112 * @param {Number} delayInms The number of milliseconds to delay for.
113 * @returns A promise that will resolve when the delay has finished.
114 */
115function delay(delayInms)
116{
117 return new Promise(
118 resolve =>
119 {
120 setTimeout(
121 () => {
122 resolve(2);
123 },
124 delayInms
125 );
126 }
127 );
128}
129
130/**
131 * Gets the transcription of each submitted audio file.
132 */
133async function getTranscriptions()
134{
135 /** @type {FileList} */
136 const files = AudioUploadVM.files;
137
138 AudioUploadVM.isTranscribing = true;
139 await delay(1000); // TODO: Remove - UI testing purposes only
140 // Cache the file list so that we can playback audio in the future
141 for (const file of files) {
142 cachedAudioFileList.set(file.name, file)
143 }
144
145 // Transcribe each audio file in batches.
146 try
147 {
148 for await (const batch of transcribeService.batchTranscribeFiles(files))
149 {
150 for (const t of batch)
151 {
152 if (!t.success) {
153 insertError(t.file_name, t.log);
154 }
155 else {
156 insertTranscription(t);
157 }
158 }
159 }
160 }
161 catch (e)
162 {
163 console.error("Failed to transcribe files");
164 console.error(e);
165 insertError("all", e.statusMessage);
166 }
167
168 AudioUploadVM.isTranscribing = false;
169}
170
171/**
172 * Removes any transcriptions from the UI list.
173 */
174function clearTranscriptions()
175{
176 cachedAudioFileList = new Map();
177 while (TRANSCRIPTIONS_LIST.lastChild) {
178 TRANSCRIPTIONS_LIST.removeChild(TRANSCRIPTIONS_LIST.lastChild);
179 }
180}
181
182/**
183* Inserts a transcription object into the DOM.
184* Adapted from https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template
185*
186* @param {TranscriptionModel} transcription The transcription to insert.
187*/
188function insertTranscription(transcription)
189{
190 // Create a new transcription row
191 /** @type {HTMLLIElement} */
192 // @ts-ignore
193 const clone = TRANSCRIPTION_TEMPLATE.content.firstElementChild.cloneNode(true);
194
195 // @ts-ignore
196 clone.querySelector(".transcription__text").textContent = transcription.transcription;
197 // @ts-ignore
198 clone.querySelector(".transcription__file-name").textContent = transcription.file_name;
199
200 // Hook up the remove button
201 /** @type {HTMLButtonElement} */
202 // @ts-ignore
203 const removeButton = clone.querySelector(".transcription__remove-button");
204 removeButton.addEventListener("click", onDeleteTranscription);
205
206 loadAudioFile(transcription.file_name);
207
208 // Prepare the audio slider
209 /** @type {HTMLInputElement} */
210 // @ts-ignore
211 const audioSlider = clone.querySelector(".audio-slider");
212 audioSlider.max = TRANSCRIPTION_AUDIO_ELEMENT.duration.toString();
213 audioSlider.step = "0.01";
214
215 // Set the filename data property on every element in the clone's DOM node
216 recurseAddData(clone, "file-name", transcription.file_name);
217
218 // Insert the entry. This could occur first (if you need to make calculations based off of the rendered element)
219 // But it is preferable to do it last in order to avoid multiple render passes.
220 TRANSCRIPTIONS_LIST.appendChild(clone);
221
222 const TranscriptionWordListComponent = Vue.createApp(
223 {
224 data()
225 {
226 return {
227 lines: []
228 }
229 }
230 });
231 const TranscriptionWordListVM = TranscriptionWordListComponent.mount("#transcriptionWordList");
232
233 transcription.transcription.replace('', "\u00A0"); // This helps with formatting, as whitespace is trimmed.
234
235 // Get the amount of space that we have to align words within
236 /** @type {HTMLDivElement} */
237 // @ts-ignore
238 const transcriptionTextList = clone.querySelector(".transcription__word-list");
239 const charsPerLine = Math.floor(transcriptionTextList.clientWidth / MONOSPACE_CHAR_SIZE);
240
241 // Insert words with correct line alignment
242 const tText = transcription.transcription;
243
244 for (let i = 0; i < tText.length; i += 0)
245 {
246 /** @type {String} */
247 let slice;
248
249 if (i + charsPerLine < tText.length)
250 {
251 slice = tText.slice(i, charsPerLine);
252
253 if (tText.charAt(i) != ' ' && tText.charAt(i + 1) != ' ') {
254 let lastSpacePos = slice.lastIndexOf(' ');
255 let decrement = slice.length - lastSpacePos;
256 slice = slice.slice(0, lastSpacePos);
257 i -= decrement;
258 }
259 }
260 else
261 {
262 slice = tText.slice(i);
263 }
264
265 TranscriptionWordListVM.lines.push({ words: slice.split(' ') });
266 i += charsPerLine;
267 }
268}
269
270/**
271 * Loads an audio file.
272 * @param {String} requestedAudioFile The name of the requested audio file.
273 */
274function loadAudioFile(requestedAudioFile)
275{
276 const currentAudioFile = TRANSCRIPTION_AUDIO_SOURCE_ELEMENT.dataset.fileName;
277
278 // Load the appropiate audio if necessary.
279 if (currentAudioFile != requestedAudioFile)
280 {
281 // If an audio file is already loaded we can revoke it.
282 if (currentAudioFile) {
283 URL.revokeObjectURL(currentAudioFile);
284 }
285
286 const urlObject = URL.createObjectURL(cachedAudioFileList.get(requestedAudioFile));
287 TRANSCRIPTION_AUDIO_SOURCE_ELEMENT.src = urlObject;
288 TRANSCRIPTION_AUDIO_SOURCE_ELEMENT.dataset.fileName = requestedAudioFile;
289 TRANSCRIPTION_AUDIO_ELEMENT.load();
290 }
291}
292
293/**
294 * Removes a transcription from the DOM.
295 *
296 * @param {MouseEvent} ev The mouse click event.
297 */
298function onDeleteTranscription(ev)
299{
300 if (ev == null || ev.target == null) {
301 return;
302 }
303
304 const fileName = ev.target.dataset.fileName;
305 const child = document.querySelector(`.transcription__container[data-file-name='${fileName}']`);
306
307 TRANSCRIPTIONS_LIST.removeChild(child);
308 cachedAudioFileList.delete(fileName);
309}
310
311/**
312 * Recurses through the entire tree of a parent and adds the given data attribute.
313 *
314 * @param {Element} parent The parent node.
315 * @param {String} dataElementName The name of the data element. Must use hyphen notation, rather than camel case.
316 * @param {String} value The value of the data element.
317 */
318function recurseAddData(parent, dataElementName, value)
319{
320 parent.setAttribute("data-" + dataElementName, value);
321
322 for (const child of parent.children) {
323 recurseAddData(child, dataElementName, value);
324 }
325}
326
327/**
328* Inserts a transcription error into the DOM.
329*
330* @param {String} fileName The file for which an error occured.
331* @param {String | null} statusMessage An informative error message to display.
332*/
333function insertError(fileName, statusMessage)
334{
335 /** @type {HTMLLIElement} */
336 // @ts-ignore
337 const clone = ERROR_TEMPLATE.content.firstElementChild.cloneNode(true);
338
339 /** @type {NodeListOf<HTMLSpanElement>} */
340 const spans = clone.querySelectorAll("span");
341 spans[0].textContent = statusMessage;
342 spans[1].textContent = fileName;
343
344 TRANSCRIPTIONS_LIST.appendChild(clone);
345}
Note: See TracBrowser for help on using the repository browser.