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

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

Fix word selection in time editor
Improve initial timing metadata for new words

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