lyrics-transcriber 0.36.1__py3-none-any.whl → 0.39.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. lyrics_transcriber/core/controller.py +22 -2
  2. lyrics_transcriber/correction/corrector.py +8 -8
  3. lyrics_transcriber/correction/handlers/base.py +4 -0
  4. lyrics_transcriber/correction/handlers/extend_anchor.py +22 -2
  5. lyrics_transcriber/correction/handlers/no_space_punct_match.py +21 -10
  6. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +21 -11
  7. lyrics_transcriber/correction/handlers/syllables_match.py +4 -4
  8. lyrics_transcriber/correction/handlers/word_count_match.py +19 -10
  9. lyrics_transcriber/correction/handlers/word_operations.py +8 -2
  10. lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js +26696 -0
  11. lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js.map +1 -0
  12. lyrics_transcriber/frontend/dist/index.html +1 -1
  13. lyrics_transcriber/frontend/package.json +3 -2
  14. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +1 -2
  15. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +76 -70
  16. lyrics_transcriber/frontend/src/components/EditModal.tsx +11 -2
  17. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +154 -128
  18. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +42 -4
  19. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +59 -15
  20. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +71 -16
  21. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +16 -19
  22. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +3 -3
  23. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +72 -57
  24. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +113 -41
  25. lyrics_transcriber/frontend/src/components/shared/types.ts +6 -3
  26. lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +202 -0
  27. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +23 -24
  28. lyrics_transcriber/frontend/src/types.ts +25 -15
  29. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  30. lyrics_transcriber/frontend/vite.config.js +4 -0
  31. lyrics_transcriber/frontend/vite.config.ts +4 -0
  32. lyrics_transcriber/lyrics/genius.py +41 -12
  33. lyrics_transcriber/output/cdg.py +33 -6
  34. lyrics_transcriber/output/cdgmaker/composer.py +839 -534
  35. lyrics_transcriber/output/video.py +17 -7
  36. lyrics_transcriber/review/server.py +22 -8
  37. {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/METADATA +3 -2
  38. {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/RECORD +41 -40
  39. {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/entry_points.txt +1 -0
  40. lyrics_transcriber/frontend/dist/assets/index-ztlAYPYT.js +0 -181
  41. lyrics_transcriber/frontend/src/components/shared/utils/newlineCalculator.ts +0 -37
  42. {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/LICENSE +0 -0
  43. {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/WHEEL +0 -0
@@ -8,11 +8,12 @@ interface TimelineEditorProps {
8
8
  endTime: number
9
9
  onWordUpdate: (index: number, updates: Partial<Word>) => void
10
10
  currentTime?: number
11
+ onPlaySegment?: (time: number) => void
11
12
  }
12
13
 
13
14
  const TimelineContainer = styled(Box)(({ theme }) => ({
14
15
  position: 'relative',
15
- height: '60px',
16
+ height: '80px',
16
17
  backgroundColor: theme.palette.grey[200],
17
18
  borderRadius: theme.shape.borderRadius,
18
19
  margin: theme.spacing(2, 0),
@@ -24,30 +25,38 @@ const TimelineRuler = styled(Box)(({ theme }) => ({
24
25
  top: 0,
25
26
  left: 0,
26
27
  right: 0,
27
- height: '16px',
28
+ height: '40px',
28
29
  borderBottom: `1px solid ${theme.palette.grey[300]}`,
30
+ cursor: 'pointer',
29
31
  }))
30
32
 
31
33
  const TimelineMark = styled(Box)(({ theme }) => ({
32
34
  position: 'absolute',
33
- top: '10px',
35
+ top: '20px',
34
36
  width: '1px',
35
- height: '6px',
36
- backgroundColor: theme.palette.grey[400],
37
+ height: '18px',
38
+ backgroundColor: theme.palette.grey[700],
39
+ '&.subsecond': {
40
+ top: '25px',
41
+ height: '13px',
42
+ backgroundColor: theme.palette.grey[500],
43
+ }
37
44
  }))
38
45
 
39
46
  const TimelineLabel = styled(Box)(({ theme }) => ({
40
47
  position: 'absolute',
41
- top: 0,
48
+ top: '5px',
42
49
  transform: 'translateX(-50%)',
43
- fontSize: '0.6rem',
44
- color: theme.palette.grey[600],
50
+ fontSize: '0.8rem',
51
+ color: theme.palette.text.primary,
52
+ fontWeight: 700,
53
+ backgroundColor: theme.palette.grey[200],
45
54
  }))
46
55
 
47
56
  const TimelineWord = styled(Box)(({ theme }) => ({
48
57
  position: 'absolute',
49
58
  height: '30px',
50
- top: '22px',
59
+ top: '40px',
51
60
  backgroundColor: theme.palette.primary.main,
52
61
  borderRadius: theme.shape.borderRadius,
53
62
  color: theme.palette.primary.contrastText,
@@ -57,6 +66,7 @@ const TimelineWord = styled(Box)(({ theme }) => ({
57
66
  display: 'flex',
58
67
  alignItems: 'center',
59
68
  fontSize: '0.875rem',
69
+ fontFamily: 'sans-serif',
60
70
  transition: 'background-color 0.1s ease',
61
71
  '&.highlighted': {
62
72
  backgroundColor: theme.palette.secondary.main,
@@ -75,7 +85,19 @@ const ResizeHandle = styled(Box)(({ theme }) => ({
75
85
  },
76
86
  }))
77
87
 
78
- export default function TimelineEditor({ words, startTime, endTime, onWordUpdate, currentTime = 0 }: TimelineEditorProps) {
88
+ // Add new styled component for the cursor
89
+ const TimelineCursor = styled(Box)(({ theme }) => ({
90
+ position: 'absolute',
91
+ top: 0,
92
+ width: '2px',
93
+ height: '100%', // Full height of container
94
+ backgroundColor: theme.palette.error.main, // Red color
95
+ pointerEvents: 'none', // Ensure it doesn't interfere with clicks
96
+ transition: 'left 0.1s linear', // Smooth movement
97
+ zIndex: 1, // Ensure it's above other elements
98
+ }))
99
+
100
+ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate, currentTime = 0, onPlaySegment }: TimelineEditorProps) {
79
101
  const containerRef = useRef<HTMLDivElement>(null)
80
102
  const [dragState, setDragState] = useState<{
81
103
  wordIndex: number
@@ -142,15 +164,23 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
142
164
  const startSecond = Math.floor(startTime)
143
165
  const endSecond = Math.ceil(endTime)
144
166
 
145
- for (let time = startSecond; time <= endSecond; time++) {
167
+ // Generate marks for each 0.1 second interval
168
+ for (let time = startSecond; time <= endSecond; time += 0.1) {
146
169
  if (time >= startTime && time <= endTime) {
147
170
  const position = timeToPosition(time)
171
+ const isFullSecond = Math.abs(time - Math.round(time)) < 0.001
172
+
148
173
  marks.push(
149
174
  <Box key={time}>
150
- <TimelineMark sx={{ left: `${position}%` }} />
151
- <TimelineLabel sx={{ left: `${position}%` }}>
152
- {time}s
153
- </TimelineLabel>
175
+ <TimelineMark
176
+ className={isFullSecond ? '' : 'subsecond'}
177
+ sx={{ left: `${position}%` }}
178
+ />
179
+ {isFullSecond && (
180
+ <TimelineLabel sx={{ left: `${position}%` }}>
181
+ {Math.round(time)}s
182
+ </TimelineLabel>
183
+ )}
154
184
  </Box>
155
185
  )
156
186
  }
@@ -266,6 +296,22 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
266
296
  return currentTime >= word.start_time && currentTime <= word.end_time
267
297
  }
268
298
 
299
+ const handleTimelineClick = (e: React.MouseEvent) => {
300
+ const rect = containerRef.current?.getBoundingClientRect()
301
+ if (!rect || !onPlaySegment) return
302
+
303
+ const x = e.clientX - rect.left
304
+ const clickedPosition = (x / rect.width) * (endTime - startTime) + startTime
305
+
306
+ console.log('Timeline clicked:', {
307
+ x,
308
+ width: rect.width,
309
+ clickedTime: clickedPosition
310
+ })
311
+
312
+ onPlaySegment(clickedPosition)
313
+ }
314
+
269
315
  return (
270
316
  <TimelineContainer
271
317
  ref={containerRef}
@@ -273,9 +319,18 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
273
319
  onMouseUp={handleMouseUp}
274
320
  onMouseLeave={handleMouseUp}
275
321
  >
276
- <TimelineRuler>
322
+ <TimelineRuler onClick={handleTimelineClick}>
277
323
  {generateTimelineMarks()}
278
324
  </TimelineRuler>
325
+
326
+ {/* Add cursor line */}
327
+ <TimelineCursor
328
+ sx={{
329
+ left: `${timeToPosition(currentTime)}%`,
330
+ display: currentTime >= startTime && currentTime <= endTime ? 'block' : 'none'
331
+ }}
332
+ />
333
+
279
334
  {words.map((word, index) => {
280
335
  const leftPosition = timeToPosition(word.start_time)
281
336
  const rightPosition = timeToPosition(word.end_time)
@@ -48,9 +48,6 @@ export default function TranscriptionView({
48
48
  }: TranscriptionViewProps) {
49
49
  const [selectedSegmentIndex, setSelectedSegmentIndex] = useState<number | null>(null)
50
50
 
51
- // Keep track of global word position
52
- let globalWordPosition = 0
53
-
54
51
  return (
55
52
  <Paper sx={{ p: 2 }}>
56
53
  <Typography variant="h6" gutterBottom>
@@ -58,36 +55,37 @@ export default function TranscriptionView({
58
55
  </Typography>
59
56
  <Box sx={{ display: 'flex', flexDirection: 'column' }}>
60
57
  {data.corrected_segments.map((segment, segmentIndex) => {
61
- // Convert segment words to TranscriptionWordPosition format
62
- const segmentWords: TranscriptionWordPosition[] = segment.words.map((word, idx) => {
63
- const position = globalWordPosition + idx
64
- const anchor = data.anchor_sequences.find(a =>
65
- position >= a.transcription_position &&
66
- position < a.transcription_position + a.length
58
+ const segmentWords: TranscriptionWordPosition[] = segment.words.map(word => {
59
+ const anchor = data.anchor_sequences?.find(a =>
60
+ a?.word_ids?.includes(word.id)
67
61
  )
68
- const gap = !anchor ? data.gap_sequences.find(g =>
69
- position >= g.transcription_position &&
70
- position < g.transcription_position + g.length
62
+
63
+ // If not in an anchor, check if it belongs to a gap sequence
64
+ const gap = !anchor ? data.gap_sequences?.find(g =>
65
+ g?.word_ids?.includes(word.id)
71
66
  ) : undefined
72
67
 
68
+ // Check if this specific word has been corrected
69
+ const isWordCorrected = gap?.corrections?.some(
70
+ correction => correction.word_id === word.id
71
+ )
72
+
73
73
  return {
74
74
  word: {
75
+ id: word.id,
75
76
  text: word.text,
76
77
  start_time: word.start_time,
77
78
  end_time: word.end_time
78
79
  },
79
- position,
80
80
  type: anchor ? 'anchor' : gap ? 'gap' : 'other',
81
81
  sequence: anchor || gap,
82
- isInRange: true
82
+ isInRange: true,
83
+ isCorrected: isWordCorrected
83
84
  }
84
85
  })
85
86
 
86
- // Update global position counter for next segment
87
- globalWordPosition += segment.words.length
88
-
89
87
  return (
90
- <Box key={segmentIndex} sx={{ display: 'flex', alignItems: 'flex-start', width: '100%' }}>
88
+ <Box key={segment.id} sx={{ display: 'flex', alignItems: 'flex-start', width: '100%' }}>
91
89
  <SegmentControls>
92
90
  <SegmentIndex
93
91
  variant="body2"
@@ -109,7 +107,6 @@ export default function TranscriptionView({
109
107
  <HighlightedText
110
108
  wordPositions={segmentWords}
111
109
  anchors={data.anchor_sequences}
112
- gaps={data.gap_sequences}
113
110
  onElementClick={onElementClick}
114
111
  onWordClick={onWordClick}
115
112
  flashingType={flashingType}
@@ -5,7 +5,7 @@ import { ModalContent } from './LyricsAnalyzer'
5
5
 
6
6
  interface WordEditControlsProps {
7
7
  content: ModalContent
8
- onUpdateCorrection?: (position: number, updatedWords: string[]) => void
8
+ onUpdateCorrection?: (wordId: string, updatedWords: string[]) => void
9
9
  onClose: () => void
10
10
  }
11
11
 
@@ -47,13 +47,13 @@ export default function WordEditControls({ content, onUpdateCorrection, onClose
47
47
 
48
48
  const handleDelete = () => {
49
49
  if (!onUpdateCorrection) return
50
- onUpdateCorrection(content.data.position, [])
50
+ onUpdateCorrection(content.data.wordId, [])
51
51
  onClose()
52
52
  }
53
53
 
54
54
  const handleSaveEdit = () => {
55
55
  if (onUpdateCorrection) {
56
- onUpdateCorrection(content.data.position, [editedWord])
56
+ onUpdateCorrection(content.data.wordId, [editedWord])
57
57
  }
58
58
  onClose()
59
59
  }
@@ -14,7 +14,6 @@ export interface HighlightedTextProps {
14
14
  wordPositions?: TranscriptionWordPosition[]
15
15
  // Common props
16
16
  anchors: AnchorSequence[]
17
- gaps: GapSequence[]
18
17
  highlightInfo: HighlightInfo | null
19
18
  mode: InteractionMode
20
19
  onElementClick: (content: ModalContent) => void
@@ -26,6 +25,8 @@ export interface HighlightedTextProps {
26
25
  preserveSegments?: boolean
27
26
  linePositions?: LinePosition[]
28
27
  currentTime?: number
28
+ referenceCorrections?: Map<string, string>
29
+ gaps?: GapSequence[]
29
30
  }
30
31
 
31
32
  export function HighlightedText({
@@ -41,54 +42,76 @@ export function HighlightedText({
41
42
  currentSource,
42
43
  preserveSegments = false,
43
44
  linePositions = [],
44
- currentTime = 0
45
+ currentTime = 0,
46
+ referenceCorrections = new Map(),
47
+ gaps = []
45
48
  }: HighlightedTextProps) {
46
49
  const { handleWordClick } = useWordClick({
47
50
  mode,
48
51
  onElementClick,
49
52
  onWordClick,
50
53
  isReference,
51
- currentSource
54
+ currentSource,
55
+ gaps
52
56
  })
53
57
 
54
- const shouldWordFlash = (wordPos: TranscriptionWordPosition | { word: string; index: number }): boolean => {
58
+ const shouldWordFlash = (wordPos: TranscriptionWordPosition | { word: string; id: string }): boolean => {
55
59
  if (!flashingType) return false
56
60
 
57
61
  if ('type' in wordPos) {
58
62
  // Handle TranscriptionWordPosition
59
- const hasCorrections = wordPos.type === 'gap' &&
60
- Boolean((wordPos.sequence as GapSequence)?.corrections?.length)
63
+ const gap = wordPos.sequence as GapSequence
64
+ const isCorrected = wordPos.type === 'gap' &&
65
+ gap?.corrections?.some(correction =>
66
+ correction.word_id === wordPos.word.id
67
+ )
61
68
 
62
69
  return Boolean(
63
70
  (flashingType === 'anchor' && wordPos.type === 'anchor') ||
64
- (flashingType === 'corrected' && hasCorrections) ||
65
- (flashingType === 'uncorrected' && wordPos.type === 'gap' && !hasCorrections) ||
66
- (flashingType === 'word' && highlightInfo?.type === 'anchor' &&
67
- wordPos.type === 'anchor' && wordPos.sequence && (
68
- (wordPos.sequence as AnchorSequence).transcription_position === highlightInfo.transcriptionIndex ||
69
- (isReference && currentSource &&
70
- (wordPos.sequence as AnchorSequence).reference_positions[currentSource as keyof typeof highlightInfo.referenceIndices] ===
71
- highlightInfo.referenceIndices?.[currentSource as keyof typeof highlightInfo.referenceIndices])
72
- ))
71
+ (flashingType === 'corrected' && isCorrected) ||
72
+ (flashingType === 'uncorrected' && wordPos.type === 'gap' && !isCorrected) ||
73
+ (flashingType === 'word' && (
74
+ // Handle anchor highlighting
75
+ (highlightInfo?.type === 'anchor' && wordPos.type === 'anchor' &&
76
+ wordPos.sequence && highlightInfo.word_ids?.includes(wordPos.word.id)) ||
77
+ // Handle gap highlighting - only highlight the specific word
78
+ (highlightInfo?.type === 'gap' && wordPos.type === 'gap' &&
79
+ highlightInfo.word_ids?.includes(wordPos.word.id))
80
+ ))
73
81
  )
74
82
  } else {
75
83
  // Handle reference word
76
- const thisWordIndex = wordPos.index
77
- const anchor = anchors.find(a => {
78
- const position = isReference
79
- ? a.reference_positions[currentSource!]
80
- : a.transcription_position
81
- if (position === undefined) return false
82
- return thisWordIndex >= position && thisWordIndex < position + a.length
83
- })
84
+ if (!currentSource) return false
85
+
86
+ const anchor = anchors?.find(a =>
87
+ a?.reference_word_ids?.[currentSource]?.includes(wordPos.id)
88
+ )
89
+
90
+ // Check if this word should flash as part of a gap
91
+ const shouldFlashGap = flashingType === 'word' &&
92
+ highlightInfo?.type === 'gap' &&
93
+ // Check if this reference word corresponds to the clicked word
94
+ gaps?.some(gap =>
95
+ gap.corrections.some(correction => {
96
+ // Only flash if this correction corresponds to the clicked word
97
+ if (!highlightInfo.word_ids?.includes(correction.word_id)) {
98
+ return false;
99
+ }
100
+
101
+ const refPosition = correction.reference_positions?.[currentSource];
102
+ const wordPosition = parseInt(wordPos.id.split('-').pop() || '', 10);
103
+ return typeof refPosition === 'number' && refPosition === wordPosition;
104
+ })
105
+ )
84
106
 
85
107
  return Boolean(
86
108
  (flashingType === 'anchor' && anchor) ||
87
- (flashingType === 'word' && highlightInfo?.type === 'anchor' && anchor && (
88
- anchor.transcription_position === highlightInfo.transcriptionIndex ||
89
- (isReference && currentSource &&
90
- anchor.reference_positions[currentSource as keyof typeof highlightInfo.referenceIndices] ===
91
- highlightInfo.referenceIndices?.[currentSource as keyof typeof highlightInfo.referenceIndices])
109
+ (flashingType === 'word' && (
110
+ // Handle anchor highlighting
111
+ (highlightInfo?.type === 'anchor' &&
112
+ highlightInfo.reference_word_ids?.[currentSource]?.includes(wordPos.id)) ||
113
+ // Handle gap highlighting
114
+ shouldFlashGap
92
115
  ))
93
116
  )
94
117
  }
@@ -106,17 +129,17 @@ export function HighlightedText({
106
129
  const renderContent = () => {
107
130
  if (wordPositions) {
108
131
  return wordPositions.map((wordPos, index) => (
109
- <React.Fragment key={`${wordPos.word.text}-${index}`}>
132
+ <React.Fragment key={wordPos.word.id}>
110
133
  <Word
111
134
  word={wordPos.word.text}
112
135
  shouldFlash={shouldWordFlash(wordPos)}
113
136
  isCurrentlyPlaying={shouldHighlightWord(wordPos)}
114
137
  isAnchor={wordPos.type === 'anchor'}
115
- isCorrectedGap={wordPos.type === 'gap' && Boolean((wordPos.sequence as GapSequence)?.corrections?.length)}
116
- isUncorrectedGap={wordPos.type === 'gap' && !(wordPos.sequence as GapSequence)?.corrections?.length}
138
+ isCorrectedGap={wordPos.type === 'gap' && wordPos.isCorrected}
139
+ isUncorrectedGap={wordPos.type === 'gap' && !wordPos.isCorrected}
117
140
  onClick={() => handleWordClick(
118
141
  wordPos.word.text,
119
- wordPos.position,
142
+ wordPos.word.id,
120
143
  wordPos.type === 'anchor' ? wordPos.sequence as AnchorSequence : undefined,
121
144
  wordPos.type === 'gap' ? wordPos.sequence as GapSequence : undefined
122
145
  )}
@@ -126,12 +149,12 @@ export function HighlightedText({
126
149
  ))
127
150
  } else if (text) {
128
151
  const lines = text.split('\n')
129
- let globalWordIndex = 0
152
+ let wordCount = 0
130
153
 
131
154
  return lines.map((line, lineIndex) => {
132
- const currentLinePosition = linePositions?.find((pos: LinePosition) => pos.position === globalWordIndex)
155
+ const currentLinePosition = linePositions?.find(pos => pos.position === wordCount)
133
156
  if (currentLinePosition?.isEmpty) {
134
- globalWordIndex++
157
+ wordCount++
135
158
  return (
136
159
  <Box key={`empty-${lineIndex}`} sx={{ display: 'flex', alignItems: 'flex-start' }}>
137
160
  <Typography
@@ -171,7 +194,7 @@ export function HighlightedText({
171
194
  paddingTop: '4px',
172
195
  }}
173
196
  >
174
- {lineIndex}
197
+ {currentLinePosition?.lineNumber ?? lineIndex}
175
198
  </Typography>
176
199
  <IconButton
177
200
  size="small"
@@ -192,32 +215,25 @@ export function HighlightedText({
192
215
  return <span key={`space-${lineIndex}-${wordIndex}`}> </span>
193
216
  }
194
217
 
195
- const position = globalWordIndex++
196
- const anchor = anchors.find(a => {
197
- const refPos = a.reference_positions[currentSource!]
198
- if (refPos === undefined) return false
199
- return position >= refPos && position < refPos + a.length
200
- })
201
-
202
- // Create a mock TranscriptionWordPosition for highlighting
203
- const wordPos: TranscriptionWordPosition = {
204
- word: { text: word },
205
- position,
206
- type: anchor ? 'anchor' : 'other',
207
- sequence: anchor,
208
- isInRange: true
209
- }
218
+ const wordId = `${currentSource}-word-${wordCount}`
219
+ wordCount++
220
+
221
+ const anchor = currentSource ? anchors?.find(a =>
222
+ a?.reference_word_ids?.[currentSource]?.includes(wordId)
223
+ ) : undefined
224
+
225
+ // Check if this word has a correction
226
+ const hasCorrection = referenceCorrections.has(wordId)
210
227
 
211
228
  return (
212
229
  <Word
213
- key={`${word}-${lineIndex}-${wordIndex}`}
230
+ key={wordId}
214
231
  word={word}
215
- shouldFlash={shouldWordFlash({ word, index: position })}
216
- isCurrentlyPlaying={shouldHighlightWord(wordPos)}
232
+ shouldFlash={shouldWordFlash({ word, id: wordId })}
217
233
  isAnchor={Boolean(anchor)}
218
- isCorrectedGap={false}
234
+ isCorrectedGap={hasCorrection}
219
235
  isUncorrectedGap={false}
220
- onClick={() => handleWordClick(word, position, anchor, undefined)}
236
+ onClick={() => handleWordClick(word, wordId, anchor, undefined)}
221
237
  />
222
238
  )
223
239
  })}
@@ -226,7 +242,6 @@ export function HighlightedText({
226
242
  )
227
243
  })
228
244
  }
229
-
230
245
  return null
231
246
  }
232
247