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

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

Properly identify surrounding words

File size: 11.2 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 :surroundingWords="surroundingWords" :word="selectedWord" :wordIndex="selectedIndex" />
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: rgba(255, 255, 0, 0.4);
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</style>
79
80<script>
81import { mapState } from "vuex";
82import { TranscriptionViewModel } from "../main";
83import AudioPlayback from "../js/AudioPlaybackModule"
84import AudioTimeBar from "./AudioTimeBar.vue"
85import WordTimingSelector from "./WordTimingSelector.vue"
86import Util from "../js/Util";
87
88export class Word {
89 /**
90 * Initialises a new instance of the Word class.
91 * @param {String} word The word.
92 * @param {Number} startTime The time within the audio that this word starts being spoken.
93 * @param {Number} endTime The time within the audio that this word finishes being spoken.
94 */
95 constructor(word, startTime, endTime) {
96 /** @type {String} The ID of this word. */
97 this.id = Util.generateUuid();
98
99 /** @type {String} The word. */
100 this.word = word;
101
102 /** @type {Number} The time within the audio that this word starts being spoken. */
103 this.startTime = startTime;
104
105 /** @type {Number} The time within the audio that this word finishes being spoken. */
106 this.endTime = endTime;
107
108 /** @type {Boolean} A value indicating whether this word should be highlighted. */
109 this.shouldHighlight = false;
110
111 /** @type {Number} The number of times the user has tried to delete this word. */
112 this.deletionAttempts = 0;
113 }
114}
115
116export default {
117 name: "TranscriptionItemEditor",
118 components: {
119 AudioTimeBar,
120 WordTimingSelector
121 },
122 props: {
123 transcription: TranscriptionViewModel
124 },
125 data() {
126 return {
127 enableEditing: false,
128 words: [],
129 lastHighlightedWordIndex: 0,
130 selectedIndex: 0
131 }
132 },
133 computed: {
134 currentPlaybackTime: {
135 get() {
136 return this.$store.getters.transcriptionPlaybackTime(this.transcription.id);
137 },
138 set(value) {
139 this.$store.commit("playbackStateSetTime", { id: this.transcription.id, time: value });
140 }
141 },
142 audioLength() {
143 return this.$store.getters.transcriptionPlaybackLength(this.transcription.id);
144 },
145 surroundingWords() {
146 return this.getSurroundingWords(this.selectedIndex);
147 },
148 selectedWord() {
149 return this.words[this.selectedIndex];
150 },
151 ...mapState({
152 playbackState: state => state.playbackState,
153 translations: state => state.translations
154 })
155 },
156 methods: {
157 async playAudio(startTime) {
158 await AudioPlayback.play(this.transcription.id, startTime);
159 },
160 getSurroundingWords(index) {
161 const BUFFER = 2; // The number of words to take on each side
162
163 let min = index;
164 for (let i = index; i > index - BUFFER && i > 0; i--) {
165 min--;
166 }
167
168 let max = index + 1;
169 for (let i = index; i < index + BUFFER && i < this.words.length; i++) {
170 max++;
171 }
172
173 const words = this.words.slice(min, max);
174 // words.splice(index - min, 1); // Remove the index word, as it is not a 'surrounding' word
175
176 return words;
177 },
178 /**
179 * Invoked when the value of a word changes.
180 * @param event The input event.
181 * @param {Number} index The index of the word that has changed.
182 */
183 onEditorInput(event, index) {
184 /** @type {Word} */
185 const word = this.words[index];
186
187 if (event.inputType === "insertText") {
188 // Insert a new word if whitespace is entered on an existing word
189 if (event.data === " " && word.word !== " ") {
190 // TODO: Proper timing metadata
191 const newWord = new Word("", word.startTime, word.endTime);
192 this.words.splice(index + 1, 0, newWord);
193
194 // We have to give the element some time to render, even though the ref is registered immediately
195 setTimeout(() => this.$refs[newWord.id].focus(), 50);
196 }
197
198 word.word = word.word.replace(" ", "");
199 }
200 },
201 onEditorFocus(index) {
202 const wordStartTime = this.words[index].startTime + 0.01;
203 this.$store.commit("playbackStateSetTime", { id: this.transcription.id, time: wordStartTime });
204 this.selectedIndex = index;
205 },
206 onEditorFocusOut(index) {
207 // Remove empty words when they lose focus
208 if (this.words[index].word === "") {
209 this.words.splice(index, 1);
210 }
211 },
212 onEditorKeyUp(event, index) {
213 /** @type {Word} */
214 const word = this.words[index];
215
216 // Remove the word if the user is repetitively hitting backspace/delete on an empty cell
217 if (event.code === "Backspace" || event.code === "Delete") {
218 if (word.word.length < 1) {
219 if (word.deletionAttempts === 1) {
220 this.words.splice(index, 1);
221
222 setFocus(event.code === "Backspace", this);
223 }
224
225 console.log(word.deletionAttempts);
226 word.deletionAttempts++;
227 }
228 }
229 else {
230 word.deletionAttempts = 0;
231 }
232
233 // Focus on the next word if the user is moving right with their caret at the word's end
234 // if (event.code === "ArrowRight") {
235 // const inputElement = this.$refs[word.id];
236
237 // if (inputElement.selectionStart === word.word.length) {
238 // setFocus(false, this);
239 // }
240 // }
241
242 // Focus on the previous word if the user is moving left with their caret at the word's start
243 // if (event.code === "ArrowLeft") {
244 // const inputElement = this.$refs[word.id];
245
246 // if (inputElement.selectionStart === 0) {
247 // setFocus(true, this);
248 // }
249 // }
250
251 /**
252 * Gets the index of an adjacent word to focus on.
253 * @param {Boolean} focusLeft Whether to focus on the word to the left or right of the current index.
254 * @param vm The view model.
255 */
256 function getFocusIndex(focusLeft, vm) {
257 let focusIndex = 0;
258
259 if (focusLeft) {
260 focusIndex = index === 0 ? 0 : index - 1;
261 }
262 else {
263 focusIndex = index === vm.words.length - 1 ? vm.words.length - 1 : index + 1;
264 }
265
266 return focusIndex;
267 }
268
269 /**
270 * Sets the focus to an adjacent word.
271 * @param {Boolean} focusLeft Whether to focus on the word to the left or right of the current index.
272 * @param vm The view model.
273 */
274 function setFocus(focusLeft, vm) {
275 const focusIndex = getFocusIndex(focusLeft, vm);
276 const focusId = vm.words[focusIndex].id;
277 vm.$refs[focusId].focus();
278
279 if (!focusLeft) {
280 vm.$refs[focusId].setSelectionRange(-1, 0);
281 }
282 }
283 }
284 },
285 watch: {
286 currentPlaybackTime(newValue) {
287 this.words[this.lastHighlightedWordIndex].shouldHighlight = false;
288
289 if (this.playbackState.id !== this.transcription.id) {
290 return;
291 }
292
293 for (let i = 0; i < this.words.length; i++) {
294 const word = this.words[i];
295
296 if (word.startTime < newValue && word.endTime > newValue) {
297 word.shouldHighlight = true;
298
299 if (this.$refs[word.id]) {
300 this.$refs[word.id].focus();
301 }
302
303 this.lastHighlightedWordIndex = i;
304 break;
305 }
306 }
307 }
308 },
309 beforeMount() {
310 this.words = getWords(this.transcription);
311 }
312}
313
314/**
315 * Gets the words in a transcription.
316 * @param {TranscriptionViewModel} transcription The transcription.
317 * @returns {Word[]}
318 */
319export function getWords(transcription) {
320 /** @type {Word[]} */
321 const words = [];
322
323 let lastWord = "";
324 let currStartTime = 0;
325
326 for (const metadata of transcription.metadata) {
327 if (metadata.char === " ") {
328 words.push(new Word(lastWord, currStartTime, metadata.start_time));
329
330 lastWord = "";
331 currStartTime = metadata.start_time;
332 }
333 else {
334 lastWord += metadata.char;
335 }
336 }
337
338 // Push the last word, as most transcriptions will not end in a space (hence breaking the above algorithm)
339 if (lastWord.length > 0) {
340 const newWord = new Word(lastWord, currStartTime, transcription.metadata[transcription.metadata.length - 1].start_time)
341 words.push(newWord);
342 }
343
344 return words;
345}
346</script>
Note: See TracBrowser for help on using the repository browser.