Changeset 35294


Ignore:
Timestamp:
2021-08-16T09:51:56+12:00 (3 years ago)
Author:
davidb
Message:

Refactor transcription display list to use Vue

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  
    1414const TRANSCRIPTION_AUDIO_SOURCE_ELEMENT = document.getElementById("transcriptionAudioSource");
    1515
    16 /** @type {HTMLUListElement} */
    17 // @ts-ignore
    18 const TRANSCRIPTIONS_LIST = document.getElementById("transcriptionsList");
    19 
    20 /** @type {HTMLTemplateElement} */
    21 // @ts-ignore
    22 const TRANSCRIPTION_TEMPLATE = document.getElementById("transcriptionTemplate");
    23 
    2416let cachedAudioFileList = new Map();
     17var 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.
    2541
    2642// Get the size of each character in our monospace font
     
    3753    MONOSPACE_CHAR_SIZE = monoCharSizeTestElement.clientWidth / monoCharSizeTestElement.textContent.length;
    3854});
    39 
    40 var transcribeService = new TranscribeService();
    4155
    4256// @ts-ignore
     
    8296const AudioUploadVM = AudioUploadComponent.mount("#audioUploadContainer");
    8397
     98// @ts-ignore
    8499const 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();
    94151        },
    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});
    108208const TranscriptionsListVM = TranscriptionsListComponent.mount("#transcriptionsDisplayContainer");
    109209
     
    111211 * When awaited, creates a delay for the given number of milliseconds.
    112212 *
    113  * @param {Number} delayInms The number of milliseconds to delay for.
     213 * @param {Number} delayMilliseconds The number of milliseconds to delay for.
    114214 * @returns A promise that will resolve when the delay has finished.
    115215 */
    116 function delay(delayInms)
     216function delay(delayMilliseconds)
    117217{
    118218    return new Promise(
     
    123223                    resolve(2);
    124224                },
    125                 delayInms
     225                delayMilliseconds
    126226            );
    127227        }
     
    139239    const files = AudioUploadVM.files;
    140240   
    141     await delay(500); // TODO: Remove - UI testing purposes only
     241    await delay(200); // TODO: Remove - UI testing purposes only
     242
    142243    // Cache the file list so that we can playback audio in the future
    143244    for (const file of files) {
     
    156257                }
    157258                else {
    158                     TranscriptionsListVM.transcriptions.push(t);
     259                    TranscriptionsListVM.transcriptions.set(UUID.generate(), t);
    159260                }
    160261            }
     
    172273
    173274/**
    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 */
     280function 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)
    236292    {
    237293        /** @type {String} */
    238294        let slice;
    239295
    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            {
    245302                let lastSpacePos = slice.lastIndexOf(' ');
    246303                let decrement = slice.length - lastSpacePos;
     
    251308        else
    252309        {
    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);
    257329        i += charsPerLine;
    258330    }
     331
     332    return lines;
    259333}
    260334
     
    280354        TRANSCRIPTION_AUDIO_ELEMENT.load();
    281355    }
    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);
    300356}
    301357
  • main/trunk/model-interfaces-dev/atea/style/asr.scss

    r35285 r35294  
    219219}
    220220
     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
    221230.transcription__error-container {
    222231    @extend .transcription__container;
  • main/trunk/model-interfaces-dev/atea/transform/pages/asr.xsl

    r35285 r35294  
    7575            <div id="transcriptionsDisplayContainer">
    7676
    77                 <audio id="transcriptionAudio">
     77                <audio id="transcriptionAudio" v-on:timeupdate="">
    7878                    <source id="transcriptionAudioSource" />
    7979                </audio>
     
    8686                    </li>
    8787
    88                     <li v-for="transcription in transcriptions" class="transcription__container">
     88                    <li v-for="transcription in getTranscriptions" class="transcription__container">
    8989                        <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">
    9191                                <span class="material-icons">&#xE037;</span> <!-- play_arrow -->
    9292                            </button>
    9393
    9494                            <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>
    9696
    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">
    9898                                <span class="material-icons">&#xE872;</span> <!-- delete -->
    9999                            </button>
    100100                        </div>
    101101
    102                         <gsf:div class="transcription__word-list" id="transcriptionWordList">
     102                        <div class="transcription__word-list">
     103                            <input type="checkbox" v-model="showCharDisplay" />
    103104                            <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">
    105120                                    <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> -->
    107123                            </ul>
    108                         </gsf:div>
    109                        
    110                         <div class="audio-slider-container">
    111                             <input type="range" min="0" value="0" class="audio-slider" />
    112124                        </div>
    113125
     
    115127                    </li>
    116128                </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">&#xE037;</span> <!-- play_arrow -->
    126                             </button>
    127                             <button class="btn-fab transcription__remove-button theme-error" type="button">
    128                                 <span class="material-icons">&#xE872;</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>
    146129            </div>
    147130
     
    157140        <!-- TODO: Switch to production version for release builds -->
    158141        <!-- <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>
    160143        <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> -->
    164144    </xsl:template>
    165145
Note: See TracChangeset for help on using the changeset viewer.