- Timestamp:
- 2021-11-08T15:51:51+13:00 (2 years ago)
- Location:
- main/trunk/model-interfaces-dev/atea/macron-restoration/src
- Files:
-
- 2 added
- 1 deleted
- 6 edited
Legend:
- Unmodified
- Added
- Removed
-
main/trunk/model-interfaces-dev/atea/macron-restoration/src/App.vue
r35715 r35723 12 12 13 13 <div class="paper content"> 14 <DirectInput /> 14 <tab-bar :tabs="tabs" v-model="selectedTab" /> 15 <hr class="divider" /> 16 17 <div class="inner-content" v-if="selectedTab === 'direct'"> 18 <direct-input /> 19 </div> 20 <div class="inner-content" v-if="selectedTab === 'file'"> 21 <file-input /> 22 </div> 15 23 </div> 16 17 <!-- <ul id="transcription-list" class="list-view content">18 <transition-group name="transcription-list">19 <li class="list-item transcription-list-item" v-for="[id, transcription] in transcriptions" :key="id">20 <TranscriptionItem :transcription="transcription" />21 </li>22 </transition-group>23 </ul> -->24 24 </template> 25 25 … … 55 55 56 56 .content { 57 padding: 0; 57 58 margin: 1em; 59 } 60 61 .inner-content { 62 padding: 1em; 58 63 } 59 64 … … 79 84 import { mapState } from "vuex"; 80 85 import DirectInput from "./components/DirectInput.vue" 86 import FileInput from "./components/FileInput.vue" 87 import TabBar from "./components/TabBar.vue" 81 88 82 89 export default { 83 90 name: "App", 84 91 components: { 85 DirectInput 92 DirectInput, 93 FileInput, 94 TabBar 86 95 }, 87 computed: mapState({ 88 translations: state => state.translations 89 }) 96 data() { 97 return { 98 selectedTab: "direct" 99 } 100 }, 101 computed: { 102 tabs() { 103 return [ 104 { label: this.translations.get("DirectInput"), value: "direct" }, 105 { label: this.translations.get("FileInput"), value: "file" } 106 ] 107 }, 108 ...mapState({ 109 translations: state => state.translations 110 }) 111 } 90 112 } 91 113 </script> -
main/trunk/model-interfaces-dev/atea/macron-restoration/src/components/DirectInput.vue
r35718 r35723 3 3 <textarea class="text-input input-area" @input="onTextInput" 4 4 v-model="input" :placeholder="translations.get('DirectInput_InputPlaceholder')" /> 5 5 6 <div class="text-container"> 6 {{ output }} 7 <span v-if="errorState" class="material-icons mdi-m error-text">error</span> 8 <span v-if="errorState" class="error-text">Something went wrong! Please try modifying your input text.</span> 9 10 <!-- eslint-disable-next-line vue/require-v-for-key --> 11 <span v-for="word in validRestored" :class="{ 'highlight': word.macronised && this.showMacronisedWords }">{{ word.w }} </span> 7 12 </div> 8 13 9 <button class="btn-primary right-column" :disabled="!canDownload" @click="downloadAsText">Download</button> 14 <div class="flex"> 15 <input type="checkbox" id="i-preserve-existing-macrons" v-model="preserveExistingMacrons" /> 16 <label for="i-preserve-existing-macrons">{{ translations.get('DirectInput_PreserveExistingMacrons') }}</label> 17 </div> 18 19 <div class="flex"> 20 <input type="checkbox" id="i-show-macronised-words" v-model="showMacronisedWords" /> 21 <label for="i-show-macronised-words">{{ translations.get('DirectInput_ShowMacronisedWords') }}</label> 22 </div> 23 24 <button class="btn-primary right-column" :disabled="!canDownload" @click="downloadAsText">{{ translations.get('DirectInput_Download') }}</button> 10 25 </div> 11 26 </template> … … 15 30 display: grid; 16 31 grid-template-columns: 1fr 1fr; 17 grid-template-rows: auto auto;32 grid-template-rows: 1fr auto auto; 18 33 gap: 1em; 19 34 } … … 25 40 } 26 41 42 .flex { 43 display: flex; 44 align-items: center; 45 gap: 0.5em; 46 } 47 27 48 .right-column { 28 49 grid-column: 2; 50 } 51 52 .highlight { 53 background-color: var(--highlighted-word-bg); 54 } 55 56 .error-text { 57 color: rgb(185, 3, 3); 29 58 } 30 59 </style> … … 33 62 import { mapState } from "vuex"; 34 63 import { saveAs } from "file-saver" 64 import { log } from "../js/Util" 35 65 import MacronRestorationModule from "../js/MacronRestorationModule" 36 66 … … 41 71 data() { 42 72 return { 43 input: "",44 output: "",73 preserveExistingMacrons: true, 74 showMacronisedWords: false, 45 75 transcribeTimeout: null, 46 waitingOnTranscribe: false 76 waitingOnTranscribe: false, 77 errorState: false 47 78 } 48 79 }, 49 80 computed: { 50 81 canDownload() { 51 return this.output.length > 0 && !this.waitingOnTranscribe; 82 return this.restored !== null && !this.waitingOnTranscribe; 83 }, 84 input: { 85 get() { 86 return this.$store.state.directInput; 87 }, 88 set(newValue) { 89 this.$store.commit("setDirectInput", newValue); 90 } 91 }, 92 restored: { 93 get() { 94 return this.$store.state.directOutput; 95 }, 96 set(newValue) { 97 this.$store.commit("setDirectOutput", newValue); 98 } 99 }, 100 validRestored() { 101 return this.restored.filter(w => w.w !== ""); 52 102 }, 53 103 ...mapState({ … … 75 125 }, 76 126 async updateMacronisedText() { 77 // TODO: Handle error 78 const response = await macroniser.directMacronisation(this.input); 79 this.output = response.fragment2; 80 this.waitingOnTranscribe = false; 127 this.errorState = false; 128 129 try { 130 this.restored = await macroniser.directMacronisation(this.input, this.preserveExistingMacrons); 131 this.waitingOnTranscribe = false; 132 } 133 catch (ex) { 134 this.errorState = true; 135 log(ex, "warn"); 136 } 81 137 }, 82 138 downloadAsText() { 83 const blob = new Blob([ this. output], { type: "text/plain;charset=utf-8" });84 saveAs(blob, " output.txt");139 const blob = new Blob([ this.restored ], { type: "text/plain;charset=utf-8" }); 140 saveAs(blob, "restored.txt"); 85 141 } 86 142 } -
main/trunk/model-interfaces-dev/atea/macron-restoration/src/components/FileInput.vue
r35716 r35723 1 1 <template> 2 <div class="card"> 3 <!-- Header containing info and actions for the transcription --> 4 <div class="transcription__header"> 5 <button class="btn-fab" v-on:click="toggleAudio" type="button" :title="translations.get('TranscriptionItem_PlayButtonTooltip')"> 6 <span class="material-icons mdi-l play-button" v-if="!isPlaying">play_arrow</span> 7 <span class="material-icons mdi-l play-button" v-if="isPlaying">pause</span> 8 </button> 2 <file-upload /> 9 3 10 <span>{{ translations.get("TranscriptionItem_FileName") }}: {{ transcription.fileName }}</span> 4 <div class="file-info-list"> 5 <div v-for="(fileInfo, index) in fileInfos" :key="fileInfo.id"> 6 <div class="info-container"> 7 <span class="material-icons mdi-override" v-if="fileInfo.fileType === '.txt'">description</span> 8 <img class="icon-l" src="resources/word_icon.ico" v-if="fileInfo.fileType === '.docx'" /> 11 9 12 <div style="position: relative;"> 13 <button class="btn-primary" @mouseover="showDownloadOptions = true" @mouseout="showDownloadOptions = false" type="button"> 14 <span class="material-icons">download</span> 15 <span>{{ translations.get("TranscriptionItem_Download") }}</span> 16 </button> 10 {{ fileInfo.fileName }} 17 11 18 <div class="download-popup card" :class="{ 'download-popup-show': showDownloadOptions }" 19 @mouseover="showDownloadOptions = true" @mouseout="showDownloadOptions = false"> 20 <button @click="downloadAsText" type="button" class="btn-primary theme-flat"> 21 <span class="material-icons">text_snippet</span> 22 <span>{{ translations.get("TranscriptionItem_DownloadAsText") }}</span> 23 </button> 24 25 <button @click="downloadAsJson" type="button" class="btn-primary theme-flat"> 26 <span class="material-icons">integration_instructions</span> 27 <span>{{ translations.get("TranscriptionItem_DownloadAsJson") }}</span> 28 </button> 29 30 <button @click="downloadAsWebvtt" type="button" class="btn-primary theme-flat"> 31 <span class="material-icons">subtitles</span> 32 <span>{{ translations.get("TranscriptionItem_DownloadAsWebvtt") }}</span> 33 </button> 34 </div> 35 </div> 36 37 <button class="btn-primary theme-error" @click="remove" type="button"> 38 <span class="material-icons">delete</span> 39 <span>{{ translations.get("TranscriptionItem_Remove") }}</span> 12 <button @click="download(fileInfo)" type="button" class="btn-primary theme-flat"> 13 <span class="material-icons">download</span> 14 <span>{{ translations.get("FileInput_Download") }}</span> 40 15 </button> 41 16 </div> 42 17 43 < div class="editor-controls">44 <audio-time-bar v-model.number="currentPlaybackTime" :audio-length="audioLength" :isDisabled="playbackState.id != transcription.id" />45 18 <hr class="divider-s" v-if="index !== fileInfos.length - 1" /> 19 </div> 20 </div> 46 21 47 <hr /> 22 <form ref="formDownload" method="post" target="_blank"> 23 <input ref="inputFilePath" type="hidden" name="filepath" /> 24 <input ref="inputFileName" type="hidden" name="filename" /> 25 </form> 48 26 49 <div class="editor-controls">50 <TranscriptionItemEditor ref="editor" :transcription="transcription" style="margin-bottom: 1em" :enableEditing="enableEditing" />51 <toggle-button v-model="enableEditing" :title="translations.get('TranscriptionItemEditor_ToggleEditTooltip')">52 <span class="material-icons">edit</span>53 </toggle-button>54 </div>55 </div>56 27 </template> 57 28 58 29 <style scoped lang="scss"> 59 . transcription__header {30 .info-container { 60 31 display: grid; 61 gap: 0.5em 0.5em; 62 grid-template-columns: auto 1fr auto auto; 32 grid-template-columns: auto 1fr auto; 63 33 align-items: center; 34 35 gap: 0.5em; 36 padding: 0.5em; 37 transition-duration: var(--transition-duration); 38 39 &:hover { 40 background-color: #DDD; 41 } 64 42 } 65 43 66 .download-popup { 67 display: flex; 68 flex-direction: column; 69 align-items: stretch; 70 71 position: absolute; 72 top: 97%; 73 z-index: 2; 74 75 padding: 2px; 76 margin: 0; 77 width: 14em; 78 79 transition-duration: var(--transition-duration); 80 visibility: hidden; 81 opacity: 0; 44 .file-info-list { 45 border: 1px solid #BBB; 46 margin-top: 1em; 82 47 } 83 48 84 .download-popup-show { 85 visibility: visible; 86 opacity: 1; 49 .mdi-override { 50 @extend .mdi-l; 51 52 color: var(--bg-color); 87 53 } 88 54 89 .editor-controls { 90 display: grid; 91 align-items: flex-start; 92 grid-template-columns: 1fr auto; 93 margin: 0.5em 0 0.3em 0; 94 gap: 1em; 95 } 96 97 .rotate-180 { 98 transform: rotate(180deg); 55 .icon-l { 56 height: 36px; 99 57 } 100 58 </style> … … 102 60 <script> 103 61 import { mapState } from "vuex"; 104 import { saveAs } from "file-saver" 105 import { TranscriptionViewModel } from "../main"; 106 import AudioPlayback from "../js/AudioPlaybackModule" 107 import Util from "../js/Util" 108 import AudioTimeBar from "./AudioTimeBar.vue" 109 import TranscriptionItemEditor from "./TranscriptionItemEditor.vue" 62 import MacronRestorationService from "../js/MacronRestorationModule" 63 import FileUpload from "./FileUpload.vue" 64 65 const macronRestorer = new MacronRestorationService(); 110 66 111 67 export default { 112 name: " TranscriptionItem",68 name: "FileInput", 113 69 components: { 114 AudioTimeBar, 115 TranscriptionItemEditor 116 }, 117 props: { 118 transcription: TranscriptionViewModel 119 }, 120 data() { 121 return { 122 enableEditing: false, 123 showDownloadOptions: false 124 } 70 FileUpload 125 71 }, 126 72 computed: { 127 currentPlaybackTime: {128 get() {129 return this.$store.getters.transcriptionPlaybackTime(this.transcription.id);130 },131 set(value) {132 this.$store.commit("playbackStateSetTime", { id: this.transcription.id, time: value });133 }134 },135 audioLength() {136 return this.$store.getters.transcriptionPlaybackLength(this.transcription.id);137 },138 isPlaying() {139 return this.playbackState.isPlaying && this.playbackState.id === this.transcription.id;140 },141 73 ...mapState({ 142 74 translations: state => state.translations, 143 playbackState: state => state.playbackState75 fileInfos: state => state.macronisedFileInfos 144 76 }) 145 77 }, 146 78 methods: { 147 async toggleAudio() { 148 this.isPlaying ? AudioPlayback.pause() : await AudioPlayback.play(this.transcription.id, -1); 149 }, 150 remove() { 151 this.$store.commit("rawTranscriptionRemove", this.transcription.id); 152 }, 153 downloadAsText() { 154 const fileName = buildDownloadableFileName(this.transcription.fileName, "txt"); 155 156 const blob = new Blob([ this.$refs.editor.words.map(w => w.word).join(" ") ], { type: "text/plain;charset=utf-8" }); 157 saveAs(blob, fileName); 158 }, 159 downloadAsJson() { 160 const fileName = buildDownloadableFileName(this.transcription.fileName, "json"); 161 const toDownload = (({ fileName, transcription }) => ({ fileName, transcription }))(this.transcription); 162 toDownload.words = this.$refs.editor.words.map(w => (({ word, startTime, endTime }) => ({ word, startTime, endTime }))(w)); 163 164 const blob = new Blob([ JSON.stringify(toDownload, null, 4) ], { type: "application/json;charset=utf-8" }); 165 saveAs(blob, fileName); 166 }, 167 downloadAsWebvtt() { 168 const fileName = buildDownloadableFileName(this.transcription.fileName, "vtt"); 169 const toDownload = buildWebvttFileContents(this.transcription, this.$refs.editor); 170 171 const blob = new Blob([ toDownload ], { type: "text/vtt;charset=utf-8" }); 172 saveAs(blob, fileName); 79 download(fileInfo) { 80 this.$refs.inputFilePath.value = fileInfo.filePath; 81 this.$refs.inputFileName.value = fileInfo.fileName; 82 this.$refs.formDownload.submit(); 173 83 } 84 }, 85 mounted() { 86 this.$refs.formDownload.setAttribute("action", macronRestorer.queryUrl + "Download"); 174 87 } 175 88 } 176 177 /**178 * Builds a file name for a download.179 * @param {String} transcriptionFileName The name of the transcription that will be downloaded.180 * @param {String} extension The file extension of the download. Do not include a period.181 * @returns {String} The file name.182 */183 function buildDownloadableFileName(transcriptionFileName, extension) {184 const extensionIndex = transcriptionFileName.lastIndexOf(".");185 let fileName = transcriptionFileName.slice(0, extensionIndex);186 fileName += "_transcription." + extension;187 188 return fileName;189 }190 191 /**192 * Builds a WebVTT file of the given transcription193 * @param {TranscriptionViewModel} transcription The transcription.194 * @returns {String} The WebVTT content.195 */196 function buildWebvttFileContents(transcription, editor) {197 let contents = "WEBVTT Transcription of " + transcription.fileName + "\n\n";198 199 for (const word of editor.words) {200 const startTime = Util.formatSecondsTimeString(word.startTime, true);201 const endTime = Util.formatSecondsTimeString(word.endTime, true);202 203 contents += startTime + " --> " + endTime + "\n";204 contents += "- " + word.word + "\n\n";205 }206 207 return contents;208 }209 89 </script> -
main/trunk/model-interfaces-dev/atea/macron-restoration/src/js/MacronRestorationModule.js
r35716 r35723 33 33 /** @type {String} The URL to which query POST requests should be made. */ 34 34 if (process.env.NODE_ENV !== "production") { 35 this.queryUrl = "//localhost:8383/macron-restoration/jsp/servlet/ DirectInput";35 this.queryUrl = "//localhost:8383/macron-restoration/jsp/servlet/"; 36 36 } 37 37 else { 38 this.queryUrl = "/macron-restoration/jsp/servlet/ DirectInput";38 this.queryUrl = "/macron-restoration/jsp/servlet/"; 39 39 } 40 40 } … … 43 43 * Performs a request to the macron restoration API to restore the given input text. 44 44 * @param {String} input The input text to macronise 45 * @returns The JSON response from the macron restoration API.45 * @returns {Promise<{ w: String, macronised?: Boolean }>} The JSON response from the macron restoration API. 46 46 */ 47 async directMacronisation(input ) {48 const that = this;47 async directMacronisation(input, preserveExistingMacrons) { 48 const queryUrl = this.queryUrl + "DirectInput"; 49 49 50 50 try { 51 51 const response = await fetch( 52 that.queryUrl,52 queryUrl, 53 53 { 54 54 method: "POST", … … 56 56 "Content-Type": "application/x-www-form-urlencoded" 57 57 }, 58 body: `o=json&fragment=${input} `58 body: `o=json&fragment=${input}&preserveExistingMacrons=${preserveExistingMacrons}` 59 59 } 60 60 ); … … 74 74 75 75 /** 76 * Performs chunked queries to transcribe the given audio files, returning the data in iterations. 77 * Data is chunked according to which ever occurs first: 78 * A maximum of three files per request, or; 79 * A maximum of 5 MiB before chunking. 76 * Performs a query to macronise the given file. 80 77 * 81 * @param {FileList | File[]} files The files to upload 82 * @returns {AsyncGenerator<TranscriptionModel[]>} The transcribed audio files. 83 * @throws {TranscriptionError} When the transcription request fails to complete. 78 * @param {File} file The file to macronise. 79 * @param {Boolean} preserveExistingMacrons Indicates if existing macrons on the file should be preserved. 80 * @returns {Promise<{ fileName: String, filePath: String, fileType: String }>} Information about the macronised file. 81 * @throws {MacronRestorationError} When the macronisation request fails to complete. 84 82 */ 85 async* batchFileMacronisation(files) { 86 let filesToSubmit = []; 87 let fileCounter = 0; 88 let byteCounter = 0; 89 90 for (const file of files) { 91 if (fileCounter === this.MAX_BATCH_COUNT || byteCounter > this.BATCH_BYTE_LIMIT) { // 5 MiB 92 yield await this.fileMacronisation(filesToSubmit); 93 filesToSubmit = []; 94 byteCounter = 0; 95 fileCounter = 0; 96 } 97 98 filesToSubmit[fileCounter++] = file; 99 byteCounter += file.size; 100 } 101 102 if (filesToSubmit.length > 0) { 103 yield await this.fileMacronisation(filesToSubmit); 104 } 105 } 106 107 /** 108 * Performs a query to transcribe the given audio files. 109 * 110 * @param {File[]} files The files to upload. 111 * @returns {Promise<TranscriptionModel[]>} The transcribed audio file. 112 * @throws {TranscriptionError} When the transcription request fails to complete. 113 */ 114 async fileMacronisation(files) { 115 if (process.env.NODE_ENV !== "production" || files.some(file => file.name === "akl_mi_pk_0002_offline.wav")) { 116 return await this.fileMacronisationTest(files); 117 } 118 else { 119 return await this.fileMacronisationActual(files); 120 } 121 } 122 123 async fileMacronisationTest(files) { 124 const objects = []; 125 126 for (const file of files) { 127 const object = JSON.parse(this.TEST_JSON); 128 object.file_name = file.name; 129 objects.push(object); 130 } 131 132 return objects; 133 } 134 135 /** 136 * Performs a query to transcribe the given audio files. 137 * 138 * @param {FileList | File[]} files The files to upload. 139 * @returns {Promise<TranscriptionModel[]>} The transcribed audio file. 140 * @throws {TranscriptionError} When the transcription request fails to complete. 141 */ 142 async fileMacronisationActual(files) { 143 const that = this; 83 async fileMacronisation(file, preserveExistingMacrons) { 84 const queryUrl = this.queryUrl + "FileUpload"; 144 85 const formData = new FormData(); 145 86 146 let audioFileKeys = ""; 147 for (let i = 0; i < files.length; i++) { 148 const f = files[i]; 149 const key = "audioFile" + i; 150 151 formData.append(key, f, f.name); 152 audioFileKeys += key + "|"; 153 } 154 formData.append("audioFileKeys", audioFileKeys); 87 formData.append("o", "json"); 88 formData.append("preserveExistingMacrons", preserveExistingMacrons); 89 formData.append(file.name, file, file.name); 155 90 156 91 try { 157 92 const response = await fetch( 158 that.queryUrl,93 queryUrl, 159 94 { 160 95 method: "POST", … … 164 99 165 100 if (!response.ok) { 166 log(` Transcription API failed with status ${response.status} and message ${response.statusText}`, "error")167 throw new MacronRestorationError(response.statusText, undefined, response.status);101 log(`Macronisation API failed with status ${response.status} and message ${response.statusText}`, "error") 102 throw new MacronRestorationError(response.statusText, file.Name, response.status); 168 103 } 169 104 … … 171 106 } 172 107 catch (e) { 173 log(` Transcription failed with reason ${e}`, "error");174 throw new MacronRestorationError( undefined, "Unknown");108 log(`Macronisation failed with reason ${e}`, "error"); 109 throw new MacronRestorationError("Unknown", file.Name, "Unknown"); 175 110 } 176 111 } -
main/trunk/model-interfaces-dev/atea/macron-restoration/src/main.js
r35714 r35723 1 1 import { createApp } from "vue"; 2 2 import { createStore } from "vuex" 3 // import AudioPlayback from "./js/AudioPlaybackModule"; 3 import Util from "./js/Util" 4 4 import App from "./App.vue"; 5 5 import ToggleButton from "./components/ToggleButton.vue" 6 7 // export class TranscriptionExistsError extends Error {8 // constructor(message = "", ...args) {9 // super(message, ...args);10 // }11 // }12 6 13 7 const store = createStore({ … … 15 9 return { 16 10 /** @type {Map<String, String>} */ 17 translations: new Map() 11 translations: new Map(), 12 macronisedFileInfos: [], 13 directInput: null, 14 directOutput: [] 18 15 } 19 16 }, … … 21 18 setTranslations(state, translations) { 22 19 state.translations = translations; 20 }, 21 pushFileInfo(state, fileInfo) { 22 state.macronisedFileInfos.push({ 23 id: Util.generateUuid(), 24 ...fileInfo 25 }); 26 }, 27 setDirectInput(state, input) { 28 state.directInput = input; 29 }, 30 setDirectOutput(state, output) { 31 state.directOutput = output; 23 32 } 24 33 } 25 // getters: {26 // hasTranscriptionOfFile: (state) => (file) => {27 // const id = TranscriptionViewModel.getId(file);28 // return state.rawTranscriptions.has(id);29 // },30 // }31 34 }); 32 35 -
main/trunk/model-interfaces-dev/atea/macron-restoration/src/styles/_material.scss
r35718 r35723 64 64 } 65 65 66 .divider-s { 67 @extend .divider; 68 margin: 0 2rem; 69 } 70 66 71 /* Buttons */ 67 72 … … 108 113 .text-container { 109 114 display: flex; 110 align-items: flex-start;111 115 flex-wrap: wrap; 112 116 … … 129 133 border-radius: var(--border-radius) var(--border-radius) 0 0; 130 134 border: none; 131 border-bottom: 1px solid #AAA;135 border-bottom: 2px solid #999; 132 136 outline: none; 133 137 cursor: text; … … 163 167 @extend .text-input; 164 168 169 align-items: center; 165 170 overflow-x: auto; 166 171 overflow-y: hidden;
Note:
See TracChangeset
for help on using the changeset viewer.