source: main/trunk/model-interfaces-dev/atea/korero-maori-asr/src/components/TranscriptionItemEditor.vue@ 35426

Last change on this file since 35426 was 35426, checked in by cstephen, 3 years ago

Improve audio time bar implementation

File size: 9.3 KB
Line 
1<template>
2<div>
3 <div class="editor-controls">
4 <audio-time-bar v-model.number="currentPlaybackTime" :audio-length="2" />
5
6 <toggle-button v-model="enableEditing">
7 <span class="material-icons .mdi-m">edit</span>
8 </toggle-button>
9 </div>
10
11 <div class="text-container words-container" :hidden="enableEditing">
12 <ul class="list-view" v-if="!enableEditing">
13 <li v-for="word in words" :key="word.id" class="word-container" @click="playAudio(word.startTime)">
14 <span class="word-highlight word" :class="{ 'word-highlight-applied': word.shouldHighlight }">
15 {{ word.word }}
16 </span>
17 </li>
18 </ul>
19
20 <ul class="list-view" v-if="enableEditing">
21 <li v-for="(word, index) in words" :key="word.id" class="word-container">
22 <input :size="word.word.length <= 1 ? 1 : word.word.length - 1" :ref="word.id"
23 class="editor-word" v-model="word.word" type="text"
24 @input="onEditorInput($event, index)" @focusout="onEditorFocusOut(index)" @keyup="onEditorKeyUp($event, index)" />
25 <span>&nbsp;</span>
26 </li>
27 </ul>
28 </div>
29</div>
30</template>
31
32<style scoped lang="scss">
33.word {
34 padding: 0.25em;
35}
36
37.editor-controls {
38 display: grid;
39 align-items: center;
40 grid-template-columns: 1fr auto;
41 margin: 0.5em 0 0.3em 0;
42 gap: 1em;
43}
44
45.words-container {
46 transition-duration: var(--transition-duration);
47
48 &[hidden] {
49 background-color: inherit;
50 }
51}
52
53.word-container {
54 font: var(--monospace-font);
55 display: inline-block;
56 line-height: 2em;
57}
58
59.word-highlight-applied {
60 background-color: rgba(255, 255, 0, 0.4);
61}
62
63.word-highlight {
64 &:hover {
65 @extend .word-highlight-applied;
66 }
67}
68
69.editor-word {
70 border: 1px solid black;
71 border-radius: 2px;
72 padding: 2px;
73 font-family: inherit;
74 font-size: 1rem;
75}
76</style>
77
78<script>
79import { mapState } from "vuex";
80import { TranscriptionViewModel, PlaybackState } from "../main";
81import AudioTimeBar from "./AudioTimeBar.vue"
82import Util from "../js/Util";
83
84export class Word {
85 /**
86 * Initialises a new instance of the Word class.
87 * @param {String} word The word.
88 * @param {Number} startTime The time within the audio that this word starts being spoken.
89 * @param {Number} endTime The time within the audio that this word finishes being spoken.
90 */
91 constructor(word, startTime, endTime) {
92 /** @type {String} The ID of this word. */
93 this.id = Util.generateUuid();
94
95 /** @type {String} The word. */
96 this.word = word;
97
98 /** @type {Number} The time within the audio that this word starts being spoken. */
99 this.startTime = startTime;
100
101 /** @type {Number} The time within the audio that this word finishes being spoken. */
102 this.endTime = endTime;
103
104 /** @type {Boolean} A value indicating whether this word should be highlighted. */
105 this.shouldHighlight = false;
106
107 /** @type {Number} The number of times the user has tried to delete this word. */
108 this.deletionAttempts = 0;
109 }
110}
111
112export default {
113 name: "TranscriptionItemEditor",
114 components: {
115 AudioTimeBar
116 },
117 props: {
118 transcription: TranscriptionViewModel
119 },
120 data() {
121 return {
122 enableEditing: false,
123 words: [],
124 lastHighlightedWord: 0
125 }
126 },
127 computed: {
128 currentPlaybackTime: {
129 get() {
130 return this.$store.state.playbackState.currentTime;
131 },
132 set(value) {
133 this.$store.commit("setCurrentPlaybackTime", value);
134 }
135 },
136 ...mapState({
137 playbackState: state => state.playbackState,
138 translations: state => state.translations
139 })
140 },
141 methods: {
142 playAudio(startTime) {
143 const pState = new PlaybackState(this.transcription.id, true, startTime);
144 this.$store.commit("setPlaybackState", pState);
145 },
146 /**
147 * Invoked when the value of a word changes.
148 * @param {Number} index The index of the word that has changed.
149 */
150 onEditorInput(event, index) {
151 /** @type {Word} */
152 const word = this.words[index];
153
154 if (event.inputType === "insertText") {
155 // Insert a new word if whitespace is entered on an existing word
156 if (event.data === " " && word.word !== " ") {
157 // TODO: Proper timing metadata
158 const newWord = new Word("", word.startTime, word.endTime);
159 this.words.splice(index + 1, 0, newWord);
160
161 // We have to give the element some time to render, even though the ref is registered immediately
162 setTimeout(() => this.$refs[newWord.id].focus(), 50);
163 }
164
165 word.word = word.word.replace(" ", "");
166 }
167 },
168 onEditorFocusOut(index) {
169 // Remove empty words when they lose focus
170 if (this.words[index].word === "") {
171 this.words.splice(index, 1);
172 }
173 },
174 onEditorKeyUp(event, index) {
175 /** @type {Word} */
176 const word = this.words[index];
177
178 // Remove the word if the user is repetitively hitting backspace/delete on an empty cell
179 if (event.code === "Backspace" || event.code === "Delete") {
180 if (word.word.length < 1) {
181 if (word.deletionAttempts === 1) {
182 this.words.splice(index, 1);
183
184 setFocus(event.code === "Backspace", this);
185 }
186
187 console.log(word.deletionAttempts);
188 word.deletionAttempts++;
189 }
190 }
191 else {
192 word.deletionAttempts = 0;
193 }
194
195 // Focus on the next word if the user is moving right with their caret at the word's end
196 // if (event.code === "ArrowRight") {
197 // const inputElement = this.$refs[word.id];
198
199 // if (inputElement.selectionStart === word.word.length) {
200 // setFocus(false, this);
201 // }
202 // }
203
204 // Focus on the previous word if the user is moving left with their caret at the word's start
205 // if (event.code === "ArrowLeft") {
206 // const inputElement = this.$refs[word.id];
207
208 // if (inputElement.selectionStart === 0) {
209 // setFocus(true, this);
210 // }
211 // }
212
213 /**
214 * Gets the index of an adjacent word to focus on.
215 * @param {Boolean} focusLeft Whether to focus on the word to the left or right of the current index.
216 * @param vm The view model.
217 */
218 function getFocusIndex(focusLeft, vm) {
219 let focusIndex = 0;
220
221 if (focusLeft) {
222 focusIndex = index === 0 ? 0 : index - 1;
223 }
224 else {
225 focusIndex = index === vm.words.length - 1 ? vm.words.length - 1 : index + 1;
226 }
227
228 return focusIndex;
229 }
230
231 /**
232 * Sets the focus to an adjacent word.
233 * @param {Boolean} focusLeft Whether to focus on the word to the left or right of the current index.
234 * @param vm The view model.
235 */
236 function setFocus(focusLeft, vm) {
237 const focusIndex = getFocusIndex(focusLeft, vm);
238 const focusId = vm.words[focusIndex].id;
239 vm.$refs[focusId].focus();
240
241 if (!focusLeft) {
242 vm.$refs[focusId].setSelectionRange(-1, 0);
243 }
244 }
245 }
246 },
247 watch: {
248 currentPlaybackTime(newValue) {
249 this.words[this.lastHighlightedWord].shouldHighlight = false;
250
251 if (!this.playbackState.isPlaying || this.playbackState.id !== this.transcription.id) {
252 return;
253 }
254
255 for (const word of this.words) {
256 word.shouldHighlight = word.startTime < newValue && word.endTime > newValue;
257 }
258 }
259 },
260 beforeMount() {
261 this.words = getWords(this.transcription);
262 }
263}
264
265/**
266 * Gets the words in a transcription.
267 * @param {TranscriptionViewModel} transcription The transcription.
268 * @returns {Word[]}
269 */
270export function getWords(transcription) {
271 /** @type {Word[]} */
272 const words = [];
273
274 let lastWord = "";
275 let currStartTime = 0;
276
277 for (const metadata of transcription.metadata) {
278 if (metadata.char === " ") {
279 words.push(new Word(lastWord, currStartTime, metadata.start_time));
280
281 lastWord = "";
282 currStartTime = metadata.start_time;
283 }
284 else {
285 lastWord += metadata.char;
286 }
287 }
288
289 // Push the last word, as most transcriptions will not end in a space (hence breaking the above algorithm)
290 if (lastWord.length > 0) {
291 const newWord = new Word(lastWord, currStartTime, transcription.metadata[transcription.metadata.length - 1].start_time)
292 words.push(newWord);
293 }
294
295 return words;
296}
297</script>
Note: See TracBrowser for help on using the repository browser.