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 | */
|
---|
10 | const FILE_UPLOAD_INPUT_NAME = "#audioFileUpload";
|
---|
11 |
|
---|
12 | /**
|
---|
13 | * The name of the container that holds the progress display for the audio upload.
|
---|
14 | */
|
---|
15 | const 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 | */
|
---|
29 | function 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 |
|
---|
42 | var transcribeService;
|
---|
43 | loadScript("./interfaces/atea/js/asr/TranscribeService.js", () => {
|
---|
44 | transcribeService = new TranscribeService();
|
---|
45 | });
|
---|
46 |
|
---|
47 | /** @type {HTMLAudioElement} */
|
---|
48 | const TRANSCRIPTION_AUDIO_ELEMENT = document.getElementById("transcriptionAudio");
|
---|
49 |
|
---|
50 | /** @type {HTMLSourceElement} */
|
---|
51 | const TRANSCRIPTION_AUDIO_SOURCE_ELEMENT = document.getElementById("transcriptionAudioSource");
|
---|
52 |
|
---|
53 | let cachedAudioFileList = new Map();
|
---|
54 |
|
---|
55 | async 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} */
|
---|
100 | const TRANSCRIPTIONS_LIST = document.getElementById("transcriptionsList");
|
---|
101 |
|
---|
102 | /**
|
---|
103 | * Removes any transcriptions from the UI list.
|
---|
104 | */
|
---|
105 | function clearTranscriptions()
|
---|
106 | {
|
---|
107 | cachedAudioFileList = new Map();
|
---|
108 | while (TRANSCRIPTIONS_LIST.lastChild) {
|
---|
109 | TRANSCRIPTIONS_LIST.removeChild(TRANSCRIPTIONS_LIST.lastChild);
|
---|
110 | }
|
---|
111 | }
|
---|
112 |
|
---|
113 | /** @type {HTMLTemplateElement} */
|
---|
114 | const 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 | */
|
---|
122 | function 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} */
|
---|
162 | const 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 | */
|
---|
171 | function 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 | */
|
---|
205 | function 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 | */
|
---|
229 | function 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 | */
|
---|
245 | function 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} */
|
---|
255 | const 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 | */
|
---|
263 | function 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 | });
|
---|