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 = "#fileUpload";
|
---|
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 transcriptionAudioElement = document.getElementById("transcriptionAudio");
|
---|
49 |
|
---|
50 | /** @type {HTMLSourceElement} */
|
---|
51 | const transcriptionAudioSourceElement = 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: " + e);
|
---|
90 | insertError("all", e.statusMessage);
|
---|
91 | }
|
---|
92 |
|
---|
93 | // Hide the progress bar and re-enable the file input.
|
---|
94 | $(FILE_UPLOAD_PROGRESS_CONTAINER_NAME).addClass("asr-hidden");
|
---|
95 | $(FILE_UPLOAD_INPUT_NAME).prop("disabled", false);
|
---|
96 | }
|
---|
97 |
|
---|
98 | /** @type {HTMLUListElement} */
|
---|
99 | const transcriptionsList = document.getElementById("transcriptionsList");
|
---|
100 |
|
---|
101 | /**
|
---|
102 | * Removes any transcriptions from the UI list.
|
---|
103 | */
|
---|
104 | function clearTranscriptions()
|
---|
105 | {
|
---|
106 | cachedAudioFileList = new Map();
|
---|
107 | while (transcriptionsList.lastChild) {
|
---|
108 | transcriptionsList.removeChild(transcriptionsList.lastChild);
|
---|
109 | }
|
---|
110 | }
|
---|
111 |
|
---|
112 | /** @type {HTMLTemplateElement} */
|
---|
113 | const transcriptionTemplate = document.getElementById("transcriptionTemplate");
|
---|
114 |
|
---|
115 | /**
|
---|
116 | * Inserts a transcription object into the DOM.
|
---|
117 | * Adapted from https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template
|
---|
118 | *
|
---|
119 | * @param {TranscriptionModel} transcription The transcription to insert.
|
---|
120 | */
|
---|
121 | function insertTranscription(transcription)
|
---|
122 | {
|
---|
123 | // Insert a new transcription row
|
---|
124 | const clone = transcriptionTemplate.content.firstElementChild.cloneNode(true);
|
---|
125 |
|
---|
126 | /** @type {HTMLSpanElement[]} */
|
---|
127 | const spans = clone.querySelectorAll("span");
|
---|
128 | spans[0].textContent = transcription.file_name;
|
---|
129 | spans[1].textContent = transcription.transcription;
|
---|
130 |
|
---|
131 | // Get the metadata list and template
|
---|
132 | /** @type {HTMLUListElement} */
|
---|
133 | const metadataList = clone.querySelector("#metadataList");
|
---|
134 |
|
---|
135 | // Insert metadata rows
|
---|
136 | for (const metadata of transcription.metadata) {
|
---|
137 | metadataList.appendChild(buildMetadataNode(metadata, transcription.file_name));
|
---|
138 | }
|
---|
139 |
|
---|
140 | transcriptionsList.appendChild(clone);
|
---|
141 | }
|
---|
142 |
|
---|
143 | /** @type {HTMLTemplateElement} */
|
---|
144 | const metadataTemplate = document.getElementById("metadataTemplate");
|
---|
145 |
|
---|
146 | /**
|
---|
147 | * Builds a metadata node.
|
---|
148 | *
|
---|
149 | * @param {{char: string, confidence: number, start_time: number}} metadata The metadata used to create the node.
|
---|
150 | * @param {String} fileName The file that this metadata was generated from.
|
---|
151 | * @returns {Node} The constructed DOM node.
|
---|
152 | */
|
---|
153 | function buildMetadataNode(metadata, fileName)
|
---|
154 | {
|
---|
155 | const metadataClone = metadataTemplate.content.firstElementChild.cloneNode(true);
|
---|
156 |
|
---|
157 | /** @type {HTMLParagraphElement} */
|
---|
158 | const p = metadataClone.querySelector("p");
|
---|
159 |
|
---|
160 | /** @type {HTMLButtonElement} */
|
---|
161 | const button = metadataClone.querySelector("button");
|
---|
162 |
|
---|
163 | button.dataset.fileName = fileName;
|
---|
164 | button.onclick = function(e)
|
---|
165 | {
|
---|
166 | // Have to traverse the event path, because the event target is the child element of the button.
|
---|
167 | const requestedAudioFile = e.composedPath().find((t) => t instanceof HTMLButtonElement).dataset.fileName;
|
---|
168 | const currentAudioFile = transcriptionAudioSourceElement.dataset.fileName;
|
---|
169 |
|
---|
170 | // Load the appropiate audio if necessary.
|
---|
171 | if (currentAudioFile != requestedAudioFile)
|
---|
172 | {
|
---|
173 | if (currentAudioFile) {
|
---|
174 | URL.revokeObjectURL(currentAudioFile);
|
---|
175 | }
|
---|
176 |
|
---|
177 | const urlObject = URL.createObjectURL(cachedAudioFileList.get(requestedAudioFile));
|
---|
178 | transcriptionAudioSourceElement.src = urlObject;
|
---|
179 | transcriptionAudioSourceElement.dataset.fileName = requestedAudioFile;
|
---|
180 | transcriptionAudioElement.load();
|
---|
181 | }
|
---|
182 |
|
---|
183 | transcriptionAudioElement.currentTime = metadata.start_time;
|
---|
184 | transcriptionAudioElement.play();
|
---|
185 | }
|
---|
186 |
|
---|
187 | if (metadata.char == ' ') {
|
---|
188 | p.textContent = "\u00A0 " // This helps with formatting, as elements that only contain whitespace are collapsed.
|
---|
189 | }
|
---|
190 | else {
|
---|
191 | p.textContent = metadata.char;
|
---|
192 | }
|
---|
193 |
|
---|
194 | return metadataClone;
|
---|
195 | }
|
---|
196 |
|
---|
197 | /** @type {HTMLTemplateElement} */
|
---|
198 | const errorTemplate = document.querySelector("#errorTemplate");
|
---|
199 |
|
---|
200 | /**
|
---|
201 | * Inserts a transcription error into the DOM.
|
---|
202 | *
|
---|
203 | * @param {String} fileName The file for which an error occured.
|
---|
204 | * @param {String | null} statusMessage An informative error message to display.
|
---|
205 | */
|
---|
206 | function insertError(fileName, statusMessage)
|
---|
207 | {
|
---|
208 | const clone = errorTemplate.content.firstElementChild.cloneNode(true);
|
---|
209 |
|
---|
210 | /** @type {HTMLSpanElement[]} */
|
---|
211 | const spans = clone.querySelectorAll("span");
|
---|
212 | spans[0].textContent = statusMessage;
|
---|
213 | spans[1].textContent = fileName;
|
---|
214 |
|
---|
215 | transcriptionsList.appendChild(clone);
|
---|
216 | }
|
---|
217 |
|
---|
218 | // Ensure the transcribe button is disabled when there are no files selected.
|
---|
219 | $(FILE_UPLOAD_INPUT_NAME).on("input", e =>
|
---|
220 | {
|
---|
221 | if (e.target.files.length <= 0) {
|
---|
222 | $("#btnFileUpload").prop("disabled", true);
|
---|
223 | }
|
---|
224 | else {
|
---|
225 | $("#btnFileUpload").prop("disabled", false);
|
---|
226 | }
|
---|
227 | });
|
---|
228 |
|
---|
229 | // whenever we hover over a menu item that has a submenu
|
---|
230 | $('.tooltip-parent').on('mouseover', function() {
|
---|
231 | var $menuItem = $(this),
|
---|
232 | $submenuWrapper = $('> .tooltip-wrapper', $menuItem);
|
---|
233 |
|
---|
234 | var menuItemPos = $menuItem.position();
|
---|
235 |
|
---|
236 | $submenuWrapper.css({
|
---|
237 | top: menuItemPos.top,
|
---|
238 | left: menuItemPos.left + Math.round($menuItem.outerWidth() * 0.75)
|
---|
239 | });
|
---|
240 | });
|
---|
241 |
|
---|
242 | // $(function() {
|
---|
243 |
|
---|
244 | // });
|
---|