Changeset 35294
- Timestamp:
- 2021-08-16T09:51:56+12:00 (3 years ago)
- Location:
- main/trunk/model-interfaces-dev/atea
- Files:
-
- 3 edited
Legend:
- Unmodified
- Added
- Removed
-
main/trunk/model-interfaces-dev/atea/js/asr/asr-controller.js
r35285 r35294 14 14 const TRANSCRIPTION_AUDIO_SOURCE_ELEMENT = document.getElementById("transcriptionAudioSource"); 15 15 16 /** @type {HTMLUListElement} */17 // @ts-ignore18 const TRANSCRIPTIONS_LIST = document.getElementById("transcriptionsList");19 20 /** @type {HTMLTemplateElement} */21 // @ts-ignore22 const TRANSCRIPTION_TEMPLATE = document.getElementById("transcriptionTemplate");23 24 16 let cachedAudioFileList = new Map(); 17 var transcribeService = new TranscribeService(); 18 19 /** 20 * Fast UUID generator, RFC4122 version 4 compliant. 21 * @author Jeff Ward (jcward.com). 22 * @license MIT 23 * @link http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136 24 **/ 25 var UUID = (function() { 26 var self = {}; 27 var lut = []; for (var i=0; i<256; i++) { lut[i] = (i<16?'0':'')+(i).toString(16); } 28 self.generate = function() { 29 var d0 = Math.random()*0xffffffff|0; 30 var d1 = Math.random()*0xffffffff|0; 31 var d2 = Math.random()*0xffffffff|0; 32 var d3 = Math.random()*0xffffffff|0; 33 return lut[d0&0xff]+lut[d0>>8&0xff]+lut[d0>>16&0xff]+lut[d0>>24&0xff]+'-'+ 34 lut[d1&0xff]+lut[d1>>8&0xff]+'-'+lut[d1>>16&0x0f|0x40]+lut[d1>>24&0xff]+'-'+ 35 lut[d2&0x3f|0x80]+lut[d2>>8&0xff]+'-'+lut[d2>>16&0xff]+lut[d2>>24&0xff]+ 36 lut[d3&0xff]+lut[d3>>8&0xff]+lut[d3>>16&0xff]+lut[d3>>24&0xff]; 37 } 38 return self; 39 })(); 40 // TODO: Hash file name and size instead. Good indicator that the user has uploaded a duplicate. 25 41 26 42 // Get the size of each character in our monospace font … … 37 53 MONOSPACE_CHAR_SIZE = monoCharSizeTestElement.clientWidth / monoCharSizeTestElement.textContent.length; 38 54 }); 39 40 var transcribeService = new TranscribeService();41 55 42 56 // @ts-ignore … … 82 96 const AudioUploadVM = AudioUploadComponent.mount("#audioUploadContainer"); 83 97 98 // @ts-ignore 84 99 const TranscriptionsListComponent = Vue.createApp( 85 { 86 data() 87 { 88 return { 89 /** @type {TranscriptionModel[]} */ 90 transcriptions: [], 91 /** @type {TranscriptionError[]} */ 92 failures: [] 93 } 100 { 101 data() 102 { 103 return { 104 /** @type {Map<String, TranscriptionModel>} */ 105 transcriptions: new Map(), 106 /** @type {TranscriptionError[]} */ 107 failures: [], // TODO: Ability to remove failures 108 showCharDisplay: false 109 } 110 }, 111 computed: 112 { 113 getTranscriptions() 114 { 115 let converted = []; 116 117 for (const [key, value] of this.transcriptions) 118 { 119 converted.push( 120 { 121 id: key, 122 transcription: value.transcription, 123 fileName: value.file_name, 124 metadata: value.metadata 125 }); 126 } 127 128 return converted; 129 } 130 }, 131 methods: 132 { 133 playAudioFile(fileName, startTime = 0) // TODO: Convert to ID 134 { 135 if (startTime < 0) 136 { 137 startTime = 0; 138 console.warn("Cannot start a audio playback at a time of less than zero."); 139 } 140 141 if (startTime >= TRANSCRIPTION_AUDIO_ELEMENT.duration) 142 { 143 console.warn("Cannot start a audio playback at a time longer than the audio duration."); 144 return; 145 } 146 147 console.log("Starting at " + startTime + " seconds"); 148 loadAudioFile(fileName); 149 TRANSCRIPTION_AUDIO_ELEMENT.currentTime = startTime; 150 TRANSCRIPTION_AUDIO_ELEMENT.play(); 94 151 }, 95 methods: 96 { 97 playAudioFile(fileName) { 98 loadAudioFile(fileName); 99 // TODO: play audio element 100 }, 101 // TODO: give transcriptions a unique ID 102 removeTranscription(id) { 103 104 } 105 } 106 } 107 ) 152 removeTranscription(id) { 153 this.transcriptions.delete(id); 154 // TODO: delete cached audio file 155 }, 156 getWords(transcriptionId) 157 { 158 /** @type {TranscriptionModel} */ 159 const transcription = this.transcriptions.get(transcriptionId); 160 const words = []; 161 162 let lastWord = ""; 163 let currStartTime = 0; 164 165 for (const metadata of transcription.metadata) 166 { 167 if (metadata.char == ' ') 168 { 169 lastWord += "\u00A0"; 170 words.push({ word: lastWord, startTime: currStartTime }); 171 172 lastWord = ""; 173 currStartTime = metadata.start_time; 174 } 175 else 176 { 177 lastWord += metadata.char; 178 } 179 } 180 181 return words; 182 // console.log(this); 183 // console.log(this.$refs); 184 // let charsPerLine = Math.floor(this.$refs.wordListContainer.clientWidth / MONOSPACE_CHAR_SIZE); 185 186 // return getWidthNormalisedLines(transcription.transcription, charsPerLine); 187 }, 188 getChars(transcriptionId) 189 { 190 /** @type {TranscriptionModel} */ 191 const transcription = this.transcriptions.get(transcriptionId); 192 const chars = []; 193 194 for (const metadata of transcription.metadata) 195 { 196 if (metadata.char == ' ') { 197 chars.push({ char: "\u00A0", startTime: metadata.start_time }); 198 } 199 else { 200 chars.push({ char: metadata.char, startTime: metadata.start_time }); 201 } 202 } 203 204 return chars; 205 } 206 } 207 }); 108 208 const TranscriptionsListVM = TranscriptionsListComponent.mount("#transcriptionsDisplayContainer"); 109 209 … … 111 211 * When awaited, creates a delay for the given number of milliseconds. 112 212 * 113 * @param {Number} delay Inms The number of milliseconds to delay for.213 * @param {Number} delayMilliseconds The number of milliseconds to delay for. 114 214 * @returns A promise that will resolve when the delay has finished. 115 215 */ 116 function delay(delay Inms)216 function delay(delayMilliseconds) 117 217 { 118 218 return new Promise( … … 123 223 resolve(2); 124 224 }, 125 delay Inms225 delayMilliseconds 126 226 ); 127 227 } … … 139 239 const files = AudioUploadVM.files; 140 240 141 await delay(500); // TODO: Remove - UI testing purposes only 241 await delay(200); // TODO: Remove - UI testing purposes only 242 142 243 // Cache the file list so that we can playback audio in the future 143 244 for (const file of files) { … … 156 257 } 157 258 else { 158 TranscriptionsListVM.transcriptions. push(t);259 TranscriptionsListVM.transcriptions.set(UUID.generate(), t); 159 260 } 160 261 } … … 172 273 173 274 /** 174 * Inserts a transcription object into the DOM. 175 * Adapted from https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template 176 * 177 * @param {TranscriptionModel} transcription The transcription to insert. 178 */ 179 function insertTranscription(transcription) 180 { 181 // Create a new transcription row 182 /** @type {HTMLLIElement} */ 183 // @ts-ignore 184 const clone = TRANSCRIPTION_TEMPLATE.content.firstElementChild.cloneNode(true); 185 186 // @ts-ignore 187 clone.querySelector(".transcription__text").textContent = transcription.transcription; 188 // @ts-ignore 189 clone.querySelector(".transcription__file-name").textContent = transcription.file_name; 190 191 // Hook up the remove button 192 /** @type {HTMLButtonElement} */ 193 // @ts-ignore 194 const removeButton = clone.querySelector(".transcription__remove-button"); 195 removeButton.addEventListener("click", onDeleteTranscription); 196 197 loadAudioFile(transcription.file_name); 198 199 // Prepare the audio slider 200 /** @type {HTMLInputElement} */ 201 // @ts-ignore 202 const audioSlider = clone.querySelector(".audio-slider"); 203 audioSlider.max = TRANSCRIPTION_AUDIO_ELEMENT.duration.toString(); 204 audioSlider.step = "0.01"; 205 206 // Set the filename data property on every element in the clone's DOM node 207 recurseAddData(clone, "file-name", transcription.file_name); 208 209 // Insert the entry. This could occur first (if you need to make calculations based off of the rendered element) 210 // But it is preferable to do it last in order to avoid multiple render passes. 211 TRANSCRIPTIONS_LIST.appendChild(clone); 212 213 const TranscriptionWordListComponent = Vue.createApp( 214 { 215 data() 216 { 217 return { 218 lines: [] 219 } 220 } 221 }); 222 const TranscriptionWordListVM = TranscriptionWordListComponent.mount("#transcriptionWordList"); 223 224 transcription.transcription.replace('', "\u00A0"); // This helps with formatting, as whitespace is trimmed. 225 226 // Get the amount of space that we have to align words within 227 /** @type {HTMLDivElement} */ 228 // @ts-ignore 229 const transcriptionTextList = clone.querySelector(".transcription__word-list"); 230 const charsPerLine = Math.floor(transcriptionTextList.clientWidth / MONOSPACE_CHAR_SIZE); 231 232 // Insert words with correct line alignment 233 const tText = transcription.transcription; 234 235 for (let i = 0; i < tText.length; i += 0) 275 * Splits a transcription into line objects that fit as many words per line as possible. 276 * 277 * @param {String} transcription The transcription to generate lines from. 278 * @returns {{words: String[], chars: String[]}[]} 279 */ 280 function getWidthNormalisedLines(transcription, charsPerLine) 281 { 282 /** @type {{words: String[], chars: String[]}[]} */ 283 let lines = []; 284 285 if (charsPerLine < 1) 286 { 287 console.error("Attempted to normalise lines to zero or less characters per line."); 288 return lines; 289 } 290 291 for (let i = 0; i < transcription.length; i += 0) 236 292 { 237 293 /** @type {String} */ 238 294 let slice; 239 295 240 if (i + charsPerLine < tText.length) 241 { 242 slice = tText.slice(i, charsPerLine); 243 244 if (tText.charAt(i) != ' ' && tText.charAt(i + 1) != ' ') { 296 if (i + charsPerLine < transcription.length) 297 { 298 slice = transcription.slice(i, charsPerLine); 299 300 if (transcription.charAt(i) != ' ' && transcription.charAt(i + 1) != ' ') 301 { 245 302 let lastSpacePos = slice.lastIndexOf(' '); 246 303 let decrement = slice.length - lastSpacePos; … … 251 308 else 252 309 { 253 slice = tText.slice(i); 254 } 255 256 TranscriptionWordListVM.lines.push({ words: slice.split(' ') }); 310 slice = transcription.slice(i); 311 } 312 313 /** @type {{words: String[], chars: String[]}} */ 314 const convertedLine = 315 { 316 words: [], 317 chars: [] 318 } 319 320 for (const word of slice.split(' ')) { 321 convertedLine.words.push(word + " "); 322 } 323 324 for (const char of slice) { 325 convertedLine.chars.push(char); 326 } 327 328 lines.push(convertedLine); 257 329 i += charsPerLine; 258 330 } 331 332 return lines; 259 333 } 260 334 … … 280 354 TRANSCRIPTION_AUDIO_ELEMENT.load(); 281 355 } 282 }283 284 /**285 * Removes a transcription from the DOM.286 *287 * @param {MouseEvent} ev The mouse click event.288 */289 function onDeleteTranscription(ev)290 {291 if (ev == null || ev.target == null) {292 return;293 }294 295 const fileName = ev.target.dataset.fileName;296 const child = document.querySelector(`.transcription__container[data-file-name='${fileName}']`);297 298 TRANSCRIPTIONS_LIST.removeChild(child);299 cachedAudioFileList.delete(fileName);300 356 } 301 357 -
main/trunk/model-interfaces-dev/atea/style/asr.scss
r35285 r35294 219 219 } 220 220 221 .transcription__word { 222 display: inline-block; 223 margin-top: 1em; 224 225 &:hover { 226 background-color: rgba(255, 255, 0, 0.315) 227 } 228 } 229 221 230 .transcription__error-container { 222 231 @extend .transcription__container; -
main/trunk/model-interfaces-dev/atea/transform/pages/asr.xsl
r35285 r35294 75 75 <div id="transcriptionsDisplayContainer"> 76 76 77 <audio id="transcriptionAudio" >77 <audio id="transcriptionAudio" v-on:timeupdate=""> 78 78 <source id="transcriptionAudioSource" /> 79 79 </audio> … … 86 86 </li> 87 87 88 <li v-for="transcription in transcriptions" class="transcription__container">88 <li v-for="transcription in getTranscriptions" class="transcription__container"> 89 89 <div class="transcription__header"> 90 <button class="btn-fab theme-flat" v-on:click="playAudioFile " type="button">90 <button class="btn-fab theme-flat" v-on:click="playAudioFile(transcription.fileName)" type="button"> 91 91 <span class="material-icons"></span> <!-- play_arrow --> 92 92 </button> 93 93 94 94 <p class="transcription__text">{{ transcription.transcription }}</p> 95 <p class="body2 transcription__file-name">File: {{ transcription.file _name }}</p>95 <p class="body2 transcription__file-name">File: {{ transcription.fileName }}</p> 96 96 97 <button class="btn-fab theme-error" v-on:click="removeTranscription " type="button">97 <button class="btn-fab theme-error" v-on:click="removeTranscription(transcription.id)" type="button"> 98 98 <span class="material-icons"></span> <!-- delete --> 99 99 </button> 100 100 </div> 101 101 102 <gsf:div class="transcription__word-list" id="transcriptionWordList"> 102 <div class="transcription__word-list"> 103 <input type="checkbox" v-model="showCharDisplay" /> 103 104 <ul class="transcription__list"> 104 <li v-for="line in lines" style="display: flex"> 105 <li v-if="!showCharDisplay"> 106 <span v-for="word in getWords(transcription.id)" 107 class="transcription__word" 108 v-on:click="playAudioFile(transcription.fileName, word.startTime)"> 109 {{ word.word }} 110 </span> 111 </li> 112 <li v-if="showCharDisplay"> 113 <span v-for="char in getChars(transcription.id)" 114 class="transcription__word" 115 v-on:click="playAudioFile(transcription.fileName, char.startTime)"> 116 {{ char.char }} 117 </span> 118 </li> 119 <!-- <li v-for="line in getLines(transcription.id)" style="display: flex"> 105 120 <span v-for="word in line.words" style="border: 1px solid blue">{{ word }}</span> 106 </li> 121 <input type="range" min="0" max="100" /> 122 </li> --> 107 123 </ul> 108 </gsf:div>109 110 <div class="audio-slider-container">111 <input type="range" min="0" value="0" class="audio-slider" />112 124 </div> 113 125 … … 115 127 </li> 116 128 </ul> 117 118 <ul id="transcriptionsList" class="transcription__list"></ul>119 120 <template id="transcriptionTemplate">121 <li class="transcription__container">122 123 <div class="transcription__header">124 <button class="btn-fab transcription__play-button theme-flat" type="button">125 <span class="material-icons"></span> <!-- play_arrow -->126 </button>127 <button class="btn-fab transcription__remove-button theme-error" type="button">128 <span class="material-icons"></span> <!-- delete -->129 </button>130 <p class="transcription__text"></p>131 <p class="body2 transcription__file-name">test</p>132 </div>133 134 <gsf:div class="transcription__word-list" id="transcriptionWordList">135 <ul class="transcription__list">136 <li v-for="line in lines" style="display: flex">137 <span v-for="word in line.words" style="border: 1px solid blue">{{ word }}</span>138 </li>139 </ul>140 </gsf:div>141 142 <hr class="divider" />143 144 </li>145 </template>146 129 </div> 147 130 … … 157 140 <!-- TODO: Switch to production version for release builds --> 158 141 <!-- <gsf:script src="https://unpkg.com/vue@next"></gsf:script> --> 159 <gsf:script src=" https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.js"></gsf:script>142 <gsf:script src="interfaces/{$interface_name}/js/asr/Vue.js"></gsf:script> 160 143 <gsf:script src="interfaces/{$interface_name}/js/asr/asr-controller.js" type="module"></gsf:script> 161 <!-- <script type="module" src="interfaces/{$interface_name}/js/asr/asr-controller.js">162 <xsl:comment>Filler</xsl:comment>>163 </script> -->164 144 </xsl:template> 165 145
Note:
See TracChangeset
for help on using the changeset viewer.