Changeset 35302


Ignore:
Timestamp:
2021-08-17T14:15:34+12:00 (3 years ago)
Author:
davidb
Message:

Fix race error to get DOM elements.
Add transcription delete animations.
Add audio current time polling.
Improve style and layout.

Location:
main/trunk/model-interfaces-dev/atea
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • main/trunk/model-interfaces-dev/atea/js/asr/asr-controller.js

    r35301 r35302  
    55
    66import { TranscribeService, TranscriptionError, TranscriptionModel, TranscriptionMetadata } from "./TranscribeModule.js";
    7 
    8 /** @type {HTMLAudioElement} */
    9 // @ts-ignore
    10 const TRANSCRIPTION_AUDIO_ELEMENT = document.getElementById("transcriptionAudio");
    11 
    12 /** @type {HTMLSourceElement} */
    13 // @ts-ignore
    14 const TRANSCRIPTION_AUDIO_SOURCE_ELEMENT = document.getElementById("transcriptionAudioSource");
    157
    168class TranscriptionViewModel {
     
    3628        /** @type {File} The file from which the transcription was generated. */
    3729        this.file = file;
    38     }
    39 }
     30
     31        /** @type {Boolean} Gets or sets a value indicating if the transcription view is expanded. */
     32        this.isExpanded = false;
     33
     34        /** @type {Boolean} Gets or sets a value indicating if this transcription view has been deleted. */
     35        this.isDeleted = false;
     36    }
     37}
     38
     39/** @type {HTMLAudioElement} */
     40var EL_AUDIO_TRANSCRIPTION_AUDIO;
     41
     42/** @type {HTMLSourceElement} */
     43var EL_SOURCE_TRANSCRIPTION_AUDIO;
     44
     45/** @type {Number} The size of each character in our monospace font. */
     46var MONOSPACE_CHAR_SIZE;
     47
     48/** @type {HTMLInputElement} */
     49var EL_INPUT_AUDIO_FILE;
    4050
    4151var transcribeService = new TranscribeService();
     52
     53window.addEventListener("load", function()
     54{
     55    // @ts-ignore | Object could be null
     56    EL_AUDIO_TRANSCRIPTION_AUDIO = document.getElementById("transcriptionAudio");
     57
     58    // @ts-ignore | Object could be null
     59    EL_INPUT_AUDIO_FILE = document.getElementById("audioFileInput");
     60
     61    // @ts-ignore | Object could be null
     62    EL_SOURCE_TRANSCRIPTION_AUDIO = document.getElementById("transcriptionAudioSource");
     63
     64    const monoCharSizeTestElement = document.querySelector(".monospace-font-sizer");
     65    if (monoCharSizeTestElement == null || monoCharSizeTestElement.textContent == null)
     66    {
     67        MONOSPACE_CHAR_SIZE = 8; // Slightly over-estimated guess for 16px font-size.
     68        return;
     69    }
     70   
     71    MONOSPACE_CHAR_SIZE = monoCharSizeTestElement.clientWidth / monoCharSizeTestElement.textContent.length;
     72
     73    pollAudioTime();
     74});
     75
     76// Adapted from https://davidwalsh.name/javascript-polling
     77function pollAudioTime()
     78{
     79    var lastTime = 0;
     80    (function p()
     81    {
     82        if (EL_AUDIO_TRANSCRIPTION_AUDIO.currentTime != lastTime)
     83        {
     84            lastTime = EL_AUDIO_TRANSCRIPTION_AUDIO.currentTime;
     85            console.log("time updated! " + lastTime);
     86        }
     87       
     88        setTimeout(p, 50);
     89    })();
     90}
    4291
    4392/**
     
    4796 * @link http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136
    4897 **/
    49  var UUID = (function() {
     98 var UUID = (function()
     99 {
    50100    var self = {};
    51     var lut = []; for (var i=0; i<256; i++) { lut[i] = (i<16?'0':'')+(i).toString(16); }
    52     self.generate = function() {
    53       var d0 = Math.random()*0xffffffff|0;
    54       var d1 = Math.random()*0xffffffff|0;
    55       var d2 = Math.random()*0xffffffff|0;
    56       var d3 = Math.random()*0xffffffff|0;
    57       return lut[d0&0xff]+lut[d0>>8&0xff]+lut[d0>>16&0xff]+lut[d0>>24&0xff]+'-'+
    58         lut[d1&0xff]+lut[d1>>8&0xff]+'-'+lut[d1>>16&0x0f|0x40]+lut[d1>>24&0xff]+'-'+
    59         lut[d2&0x3f|0x80]+lut[d2>>8&0xff]+'-'+lut[d2>>16&0xff]+lut[d2>>24&0xff]+
    60         lut[d3&0xff]+lut[d3>>8&0xff]+lut[d3>>16&0xff]+lut[d3>>24&0xff];
    61     }
     101    var lut = [];
     102
     103    for (var i=0; i<256; i++){
     104        lut[i] = (i < 16 ? '0' : '') + (i).toString(16);
     105    }
     106
     107    self.generate = function()
     108    {
     109        var d0 = Math.random() * 0xffffffff | 0;
     110        var d1 = Math.random() * 0xffffffff | 0;
     111        var d2 = Math.random() * 0xffffffff | 0;
     112        var d3 = Math.random() * 0xffffffff | 0;
     113
     114        return lut[d0 & 0xff] + lut[d0 >> 8 & 0xff] + lut[d0 >> 16 & 0xff] + lut[d0 >> 24 & 0xff] + '-' +
     115            lut[d1 & 0xff] + lut[d1 >> 8 & 0xff] + '-' + lut[d1 >> 16 & 0x0f | 0x40] + lut[d1 >> 24 & 0xff] + '-' +
     116            lut[d2 & 0x3f | 0x80] + lut[d2 >> 8 & 0xff] + '-' + lut[d2 >> 16 & 0xff] + lut[d2 >> 24 & 0xff] +
     117            lut[d3 & 0xff] + lut[d3 >> 8 & 0xff] + lut[d3 >> 16 & 0xff] + lut[d3 >> 24 & 0xff];
     118    }
     119
    62120    return self;
    63   })();
    64   // TODO: Hash file name and size instead. Good indicator that the user has uploaded a duplicate.
    65 
    66 // Get the size of each character in our monospace font
    67 var MONOSPACE_CHAR_SIZE;
    68 window.addEventListener("load", function()
    69 {
    70     const monoCharSizeTestElement = document.querySelector(".monospace-font-sizer");
    71     if (monoCharSizeTestElement == null || monoCharSizeTestElement.textContent == null)
    72     {
    73         MONOSPACE_CHAR_SIZE = 8; // Slightly over-estimated guess for size 16 font.
    74         return;
    75     }
    76    
    77     MONOSPACE_CHAR_SIZE = monoCharSizeTestElement.clientWidth / monoCharSizeTestElement.textContent.length;
    78 });
     121})();
     122// TODO: Hash file name and size instead. Good indicator that the user has uploaded a duplicate.
    79123
    80124// @ts-ignore
     
    108152        {
    109153            openFilePicker() {
    110                 document.getElementById("audioFileInput")?.click();
     154                EL_INPUT_AUDIO_FILE.click();
    111155            },
    112156            onFilesChanged()
    113157            {
    114                 /** @type {HTMLInputElement} */
    115                 // @ts-ignore | Object could be null
    116                 const audioFileInput = document.getElementById("audioFileInput");
    117 
    118                 if (audioFileInput.files?.length != undefined && audioFileInput.files?.length > 0) {
     158                this.files = [];
     159
     160                if (EL_INPUT_AUDIO_FILE.files?.length != undefined && EL_INPUT_AUDIO_FILE.files?.length > 0) {
    119161                   
    120                     for (const f of audioFileInput.files) {
    121                         this.files.push(f);
    122                     }
    123                 }
    124                 else {
    125                     this.files = [];
     162                    for (const file of EL_INPUT_AUDIO_FILE.files)
     163                        this.files.push(file);
    126164                }
    127165            },
     
    130168                // Else call getTranscriptions();
    131169                // Then, we don't have to worry about preventing the user from transcribing multiple items.
    132                 await getTranscriptions(this.files);
     170                const files = this.files;
     171                this.files = []; // Clear the file list, as there is no reason the user would want to transcribe the same file multiple times over
     172                await getTranscriptions(files);
    133173            }
    134174        }
     
    148188            failures: new Map(),
    149189            showCharDisplay: false,
    150             showWordList: false,
    151190            /** @type {{id: String, url: String} | null} Gets the ID of the transcription for which the audio is currently loaded */
    152191            currentlyLoadedAudio: null
     
    155194    methods:
    156195    {
    157         playAudioFile(transcriptionId, startTime = 0) // TODO: Convert to ID
     196        playAudioFile(transcriptionId, startTime = 0)
    158197        {
    159198            if (startTime < 0)
    160199            {
    161200                startTime = 0;
    162                 console.warn("Cannot start a audio playback at a time of less than zero.");
    163             }
    164 
    165             if (startTime >= TRANSCRIPTION_AUDIO_ELEMENT.duration)
    166             {
    167                 console.warn("Cannot start a audio playback at a time longer than the audio duration.");
     201                console.warn("Cannot start audio playback at a time of less than zero.");
     202            }
     203
     204            if (startTime >= EL_AUDIO_TRANSCRIPTION_AUDIO.duration)
     205            {
     206                console.warn("Cannot start audio playback at a time longer than the audio duration.");
    168207                return;
    169208            }
     
    171210            this.currentlyLoadedAudio = loadTranscriptionAudio(transcriptionId, this.currentlyLoadedAudio);
    172211
    173             TRANSCRIPTION_AUDIO_ELEMENT.currentTime = startTime;
    174             TRANSCRIPTION_AUDIO_ELEMENT.play();
    175         },
    176         removeTranscription(id) {
    177             this.transcriptions.delete(id);
    178             // TODO: delete cached audio file
     212            EL_AUDIO_TRANSCRIPTION_AUDIO.currentTime = startTime;
     213            EL_AUDIO_TRANSCRIPTION_AUDIO.play();
     214        },
     215        removeTranscription(id)
     216        {
     217            this.transcriptions.get(id).isDeleted = true;
     218            delay(550).then(() => this.transcriptions.delete(id));
    179219        },
    180220        removeFailure(id) {
     
    235275
    236276            return chars;
    237         },
    238         toggleWordList() {
    239             this.showWordList = !this.showWordList;
    240277        }
    241278    }
     
    273310    AudioUploadVM.isTranscribing = true;
    274311   
    275     // await delay(200); // TODO: Remove - UI testing purposes only
     312    await delay(200); // TODO: Remove - UI testing purposes only
    276313   
    277314    // Transcribe each audio file in batches.
     
    384421
    385422        const urlObject = URL.createObjectURL(TranscriptionsListVM.transcriptions.get(transcriptionId).file);
    386         TRANSCRIPTION_AUDIO_SOURCE_ELEMENT.src = urlObject;
    387         TRANSCRIPTION_AUDIO_ELEMENT.load();
     423        EL_SOURCE_TRANSCRIPTION_AUDIO.src = urlObject;
     424        EL_AUDIO_TRANSCRIPTION_AUDIO.load();
    388425
    389426        return { id: transcriptionId, url: urlObject };
     
    392429    return current;
    393430}
    394 
    395 TRANSCRIPTION_AUDIO_ELEMENT.addEventListener('durationchange', function() {
    396     TranscriptionsListVM.audioDuration = TRANSCRIPTION_AUDIO_ELEMENT.duration;
    397 });
    398 
    399 TRANSCRIPTION_AUDIO_ELEMENT.addEventListener('timeupdate', function() {
    400     console.log("time updated");
    401     TranscriptionsListVM.currentAudioTime = TRANSCRIPTION_AUDIO_ELEMENT.currentTime;
    402 });
  • main/trunk/model-interfaces-dev/atea/style/asr.scss

    r35300 r35302  
     1/*
     2 * Defines styles for asr.xsl
     3 * Author: Carl Stephens
     4 */
     5
    16@use "./material.scss";
    27@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;700&display=swap');
     
    2126
    2227/* === End theme application === */
     28
     29.transform-slideout {
     30    -webkit-transition-duration: 0.5s;
     31    -moz-transition-duration: 0.5s;
     32    transition-duration: 0.5s;
     33
     34    transform: translate(100%, 0);
     35    opacity: 0;
     36}
    2337
    2438body {
     
    4963    display: grid;
    5064    gap: 0.5em 0.5em;
    51     grid-template-columns: auto 1fr auto;
     65    grid-template-columns: 1fr auto;
    5266    align-items: stretch;
    5367
     
    7488    padding: 0;
    7589    list-style-type: none;
    76 }
    77 
    78 .transcription__container {
    79     -webkit-transition-duration: 0.15s;
    80     transition-duration: 0.15s;
    81 
    82     // &:hover {
    83     //     background-color: #EEE;
    84     // }
    8590}
    8691
     
    121126
    122127.transcription__error-container {
    123     @extend .transcription__container;
    124128    display: grid;
    125129    gap: 0.5em 0.5em;
  • main/trunk/model-interfaces-dev/atea/style/material.scss

    r35300 r35302  
     1/*
     2 * Defines themes that match the material design specification
     3 * Author: Carl Stephens
     4 */
     5
    16/* === Start theme definitions === */
    27
     
    132137    align-items: center;
    133138
    134     line-height: 1.5em;
     139    //line-height: 1.5em;
    135140    border-radius: var(--border-radius);
    136     background: #DCDCDCDC;
     141    background: #DDD;
    137142    padding: 0.5em;
     143
     144    .text-placeholder {
     145        cursor: default;
     146    }
    138147}
    139148
     
    147156
    148157.text-input {
    149     display: flex;
    150     align-items: center;
     158    @extend .text-container;
    151159
    152160    border-radius: var(--border-radius) var(--border-radius) 0 0;
    153161    border-bottom: 1px solid #AAA;
    154     background: #EEE;
    155     padding: 0.5em;
    156 
    157     @include transition-set;
    158 
    159     &:hover {
    160         background-color: #DDD;
     162    cursor: text;
     163
     164    @include transition-set;
     165
     166    &:hover {
     167        background-color: #CDCDCD;
    161168    }
    162169
     
    179186}
    180187
     188.text-input-sl {
     189    @extend .text-input;
     190
     191    overflow-x: auto;
     192    overflow-y: hidden;
     193    white-space: nowrap;
     194}
     195
    181196.text-placeholder {
    182197    color: #666;
    183198    cursor: default;
    184199    font-style: italic;
     200    cursor: inherit;
    185201}
    186202
  • main/trunk/model-interfaces-dev/atea/transform/pages/asr.xsl

    r35301 r35302  
    4949            <!-- Contains the file input, transcribe button and transcription progress indicator -->
    5050            <div id="audioUploadContainer" class="audio-file-picker">
    51                 <button type="button" v-on:click="openFilePicker"
    52                         v-bind:disabled="isTranscribing">
    53                     <span class="material-icons">&#xE2C6;</span> <!-- file_upload -->
     51                <!-- <button type="button" v-on:click="openFilePicker">
     52                    <span class="material-icons">&#xE2C6;</span> file_upload
    5453                    <span>Upload Audio Files</span>
    55                 </button>
     54                </button>-->
    5655
    57                 <div class="text-container-sl" v-on:click="openFilePicker">
     56                <div class="text-input-sl" style="cursor: pointer;" v-on:click="openFilePicker">
     57                    <span class="text-placeholder material-icons" v-if="!anyFiles">&#xe226;</span> <!-- attach_file -->
    5858                    <span class="text-placeholder" v-if="!anyFiles">Select file/s...</span>
    5959                    <span v-if="anyFiles">{{ getFileNameList }}</span>
     
    9696                    <!-- Displays each transcription -->
    9797                    <li v-for="[id, transcription] in transcriptions">
     98                        <xsl:attribute name="v-bind:class">
     99                            <xsl:text disable-output-escaping="yes">{ 'transform-slideout': transcription.isDeleted }</xsl:text>
     100                        </xsl:attribute>
     101
    98102                        <div class="transcription__container card">
    99103                            <!-- Header containing info and actions for the transcription -->
     
    111115                            </div>
    112116
    113                             <div class="transcription__word-list" v-if="showWordList">
     117                            <div class="transcription__word-list" v-if="transcription.isExpanded">
    114118                                <div class="transcription__word-list__controls">
    115119                                    <button class="btn-fab" v-on:click="playAudioFile(id)" type="button">
     
    138142                            <hr />
    139143
    140                             <button type="button" class="theme-flat" v-on:click="toggleWordList">
    141                                 <span class="material-icons" v-if="!showWordList">expand_more</span>
    142                                 <span class="material-icons" v-if="showWordList">expand_less</span>
     144                            <button type="button" class="theme-flat" v-on:click="transcription.isExpanded = !transcription.isExpanded">
     145                                <span class="material-icons" v-if="!transcription.isExpanded">expand_more</span>
     146                                <span class="material-icons" v-if="transcription.isExpanded">expand_less</span>
    143147                                <span>Playback and Edit</span>
    144148                            </button>
Note: See TracChangeset for help on using the changeset viewer.