1import { Controller } from '@hotwired/stimulus'; 2 3// Highlights the transcript sentence that matches the current audio position. 4// The sentence whose `start_time` is the latest one at or before the playhead 5// stays lit until the next sentence's `start_time` is reached. 6export default class extends Controller { 7 static targets = ['sentence']; 8 static classes = ['active']; 9 10 connect() { 11 this.audio = this.element.querySelector('audio'); 12 if (!this.audio) return; 13 14 // Sentence start times, ascending (sentences are rendered in time order). 15 this.starts = this.sentenceTargets.map((el) => parseFloat(el.dataset.startTime) || 0); 16 this.activeIndex = -1; 17 18 this.refresh = this.refresh.bind(this); 19 this.audio.addEventListener('timeupdate', this.refresh); 20 this.audio.addEventListener('seeked', this.refresh); 21 } 22 23 disconnect() { 24 if (!this.audio) return; 25 26 this.audio.removeEventListener('timeupdate', this.refresh); 27 this.audio.removeEventListener('seeked', this.refresh); 28 } 29 30 refresh() { 31 const index = this.indexForTime(this.audio.currentTime); 32 if (index === this.activeIndex) return; 33 34 this.setActive(index); 35 } 36 37 // Index of the last sentence whose start time is <= t (binary search). 38 indexForTime(t) { 39 let lo = 0; 40 let hi = this.starts.length - 1; 41 let result = -1; 42 43 while (lo <= hi) { 44 const mid = (lo + hi) >> 1; 45 if (this.starts[mid] <= t) { 46 result = mid; 47 lo = mid + 1; 48 } else { 49 hi = mid - 1; 50 } 51 } 52 53 return result; 54 } 55 56 setActive(index) { 57 if (this.activeIndex >= 0) { 58 this.sentenceTargets[this.activeIndex].classList.remove(...this.activeClasses); 59 } 60 if (index >= 0) { 61 this.sentenceTargets[index].classList.add(...this.activeClasses); 62 } 63 this.activeIndex = index; 64 } 65};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.