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

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

UI improvements

File size: 8.9 KB
Line 
1/**
2 * @file Controller logic for asr.xsl.
3 * @author Carl Stephens
4 */
5// @ts-nocheck
6
7/**
8* The name of the file input that audio files can be uploaded to.
9*/
10const FILE_UPLOAD_INPUT_NAME = "#audioFileUpload";
11
12/**
13* The name of the container that holds the progress display for the audio upload.
14*/
15const FILE_UPLOAD_PROGRESS_CONTAINER_NAME = "#prgFileUploadContainer";
16
17/**
18* @callback loadScriptCallback
19* @param {Event} ev The event
20* @returns {void}
21*/
22
23/**
24* Loads a remote script.
25* Found at https://stackoverflow.com/questions/950087/how-do-i-include-a-javascript-file-in-another-javascript-file
26*
27* @param {string} url The URL from which to load the script.
28*/
29function loadScript(url, callback = () => {})
30{
31 var body = document.body;
32
33 var script = document.createElement('script');
34 script.type = 'text/javascript';
35 script.src = url;
36
37 script.onload = callback;
38
39 body.appendChild(script);
40}
41
42var transcribeService;
43loadScript("./interfaces/atea/js/asr/TranscribeService.js", () => {
44 transcribeService = new TranscribeService();
45});
46
47/** @type {HTMLAudioElement} */
48const TRANSCRIPTION_AUDIO_ELEMENT = document.getElementById("transcriptionAudio");
49
50/** @type {HTMLSourceElement} */
51const TRANSCRIPTION_AUDIO_SOURCE_ELEMENT = document.getElementById("transcriptionAudioSource");
52
53let cachedAudioFileList = new Map();
54
55async function doAudioUpload()
56{
57 /** @type {FileList} */
58 const files = $(FILE_UPLOAD_INPUT_NAME)[0].files;
59
60 // Disable the file input while transcribing, and show the progress bar
61 $(FILE_UPLOAD_INPUT_NAME).prop("disabled", true);
62 $(FILE_UPLOAD_PROGRESS_CONTAINER_NAME).removeClass("asr-hidden");
63
64 clearTranscriptions();
65
66 // Cache the file list so that we can playback audio in the future
67 for (const file of files) {
68 cachedAudioFileList.set(file.name, file)
69 }
70
71 // Transcribe each audio file in batches.
72 try
73 {
74 for await (const batch of transcribeService.batchTranscribeFiles(files))
75 {
76 for (const t of batch)
77 {
78 if (!t.success) {
79 insertError(t.file_name, t.log);
80 }
81 else {
82 insertTranscription(t);
83 }
84 }
85 }
86 }
87 catch (e)
88 {
89 console.error("Failed to transcribe files");
90 console.error(e);
91 insertError("all", e.statusMessage);
92 }
93
94 // Hide the progress bar and re-enable the file input.
95 $(FILE_UPLOAD_PROGRESS_CONTAINER_NAME).addClass("asr-hidden");
96 $(FILE_UPLOAD_INPUT_NAME).prop("disabled", false);
97}
98
99/** @type {HTMLUListElement} */
100const TRANSCRIPTIONS_LIST = document.getElementById("transcriptionsList");
101
102/**
103 * Removes any transcriptions from the UI list.
104 */
105function clearTranscriptions()
106{
107 cachedAudioFileList = new Map();
108 while (TRANSCRIPTIONS_LIST.lastChild) {
109 TRANSCRIPTIONS_LIST.removeChild(TRANSCRIPTIONS_LIST.lastChild);
110 }
111}
112
113/** @type {HTMLTemplateElement} */
114const TRANSCRIPTION_TEMPLATE = document.getElementById("transcriptionTemplate");
115
116/**
117* Inserts a transcription object into the DOM.
118* Adapted from https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template
119*
120* @param {TranscriptionModel} transcription The transcription to insert.
121*/
122function insertTranscription(transcription)
123{
124 // Create a new transcription row
125 /** @type {HTMLLIElement} */
126 const clone = TRANSCRIPTION_TEMPLATE.content.firstElementChild.cloneNode(true);
127
128 const textElement = clone.querySelector(".transcription__text");
129 textElement.textContent = transcription.transcription;
130 const fileNameElement = clone.querySelector(".transcription__file-name");
131 fileNameElement.textContent = transcription.file_name;
132
133 // Hook up the remove button
134 /** @type {HTMLButtonElement} */
135 const removeButton = clone.querySelector(".transcription__remove-button");
136 removeButton.onclick = onDeleteTranscription;
137
138 loadAudioFile(transcription.file_name);
139
140 // Prepare the audio slider
141 /** @type {HTMLInputElement} */
142 const audioSlider = clone.querySelector(".audio-slider");
143 audioSlider.max = TRANSCRIPTION_AUDIO_ELEMENT.duration;
144 audioSlider.step = 0.01;
145
146 // Get the metadata list
147 /** @type {HTMLUListElement} */
148 const metadataList = clone.querySelector(".metadata-list");
149
150 // Insert metadata rows
151 for (const metadata of transcription.metadata) {
152 metadataList.appendChild(buildMetadataNode(metadata));
153 }
154
155 // Set the filename data property on every element in the clone's DOM node
156 recurseAddData(clone, "file-name", transcription.file_name);
157
158 TRANSCRIPTIONS_LIST.appendChild(clone);
159}
160
161/** @type {HTMLTemplateElement} */
162const metadataTemplate = document.getElementById("metadataTemplate");
163
164/**
165 * Builds a metadata node.
166 *
167 * @param {{char: string, confidence: number, start_time: number}} metadata The metadata used to create the node.
168 * @param {String} fileName The file that this metadata was generated from.
169 * @returns {Node} The constructed DOM node.
170 */
171function buildMetadataNode(metadata)
172{
173 const metadataClone = metadataTemplate.content.firstElementChild.cloneNode(true);
174
175 /** @type {HTMLParagraphElement} */
176 const p = metadataClone.querySelector("p");
177
178 /** @type {HTMLButtonElement} */
179 const button = metadataClone.querySelector("button");
180
181 button.onclick = function(e)
182 {
183 // Have to traverse the event path, because the event target is the child element of the button.
184 const requestedAudioFile = e.composedPath().find((t) => t instanceof HTMLButtonElement).dataset.fileName;
185 loadAudioFile(requestedAudioFile);
186
187 TRANSCRIPTION_AUDIO_ELEMENT.currentTime = metadata.start_time;
188 TRANSCRIPTION_AUDIO_ELEMENT.play();
189 }
190
191 if (metadata.char == ' ') {
192 p.textContent = "\u00A0 " // This helps with formatting, as elements that only contain whitespace are collapsed.
193 }
194 else {
195 p.textContent = metadata.char;
196 }
197
198 return metadataClone;
199}
200
201/**
202 * Loads an audio file.
203 * @param {String} requestedAudioFile The name of the requested audio file.
204 */
205function loadAudioFile(requestedAudioFile)
206{
207 const currentAudioFile = TRANSCRIPTION_AUDIO_SOURCE_ELEMENT.dataset.fileName;
208
209 // Load the appropiate audio if necessary.
210 if (currentAudioFile != requestedAudioFile)
211 {
212 // If an audio file is already loaded we can revoke it.
213 if (currentAudioFile) {
214 URL.revokeObjectURL(currentAudioFile);
215 }
216
217 const urlObject = URL.createObjectURL(cachedAudioFileList.get(requestedAudioFile));
218 TRANSCRIPTION_AUDIO_SOURCE_ELEMENT.src = urlObject;
219 TRANSCRIPTION_AUDIO_SOURCE_ELEMENT.dataset.fileName = requestedAudioFile;
220 TRANSCRIPTION_AUDIO_ELEMENT.load();
221 }
222}
223
224/**
225 * Removes a transcription from the DOM.
226 *
227 * @param {MouseEvent} ev The mouse click event.
228 */
229function onDeleteTranscription(ev)
230{
231 const fileName = ev.target.dataset.fileName;
232 const child = document.querySelector(`.transcription__container[data-file-name='${fileName}']`);
233
234 TRANSCRIPTIONS_LIST.removeChild(child);
235 cachedAudioFileList.delete(fileName);
236}
237
238/**
239 * Recurses through the entire tree of a parent and adds the given data attribute.
240 *
241 * @param {HTMLElement} parent The parent node.
242 * @param {String} dataElementname The name of the data element. Must use hyphen notation, rather than camel case.
243 * @param {String} value The value of the data element.
244 */
245function recurseAddData(parent, dataElementName, value)
246{
247 parent.setAttribute("data-" + dataElementName, value);
248
249 for (const child of parent.children) {
250 recurseAddData(child, dataElementName, value);
251 }
252}
253
254/** @type {HTMLTemplateElement} */
255const errorTemplate = document.querySelector("#errorTemplate");
256
257/**
258* Inserts a transcription error into the DOM.
259*
260* @param {String} fileName The file for which an error occured.
261* @param {String | null} statusMessage An informative error message to display.
262*/
263function insertError(fileName, statusMessage)
264{
265 const clone = errorTemplate.content.firstElementChild.cloneNode(true);
266
267 /** @type {HTMLSpanElement[]} */
268 const spans = clone.querySelectorAll("span");
269 spans[0].textContent = statusMessage;
270 spans[1].textContent = fileName;
271
272 TRANSCRIPTIONS_LIST.appendChild(clone);
273}
274
275// Ensure the transcribe button is disabled when there are no files selected.
276$(FILE_UPLOAD_INPUT_NAME).on("input", e =>
277{
278 if (e.target.files.length <= 0) {
279 $("#btnBeginTranscription").prop("disabled", true);
280 }
281 else {
282 $("#btnBeginTranscription").prop("disabled", false);
283 }
284});
285
286$('.tooltip-parent').on('mouseover', function() {
287 var $menuItem = $(this),
288 $submenuWrapper = $('> .tooltip-wrapper', $menuItem);
289
290 var menuItemPos = $menuItem.position();
291
292 $submenuWrapper.css({
293 top: menuItemPos.top,
294 left: menuItemPos.left + Math.round($menuItem.outerWidth() * 0.75)
295 });
296});
Note: See TracBrowser for help on using the repository browser.