lyrics-transcriber 0.41.0__py3-none-any.whl → 0.43.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 (78) hide show
  1. lyrics_transcriber/core/controller.py +30 -52
  2. lyrics_transcriber/correction/anchor_sequence.py +325 -150
  3. lyrics_transcriber/correction/corrector.py +224 -107
  4. lyrics_transcriber/correction/handlers/base.py +28 -10
  5. lyrics_transcriber/correction/handlers/extend_anchor.py +47 -24
  6. lyrics_transcriber/correction/handlers/levenshtein.py +75 -33
  7. lyrics_transcriber/correction/handlers/llm.py +290 -0
  8. lyrics_transcriber/correction/handlers/no_space_punct_match.py +81 -36
  9. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +46 -26
  10. lyrics_transcriber/correction/handlers/repeat.py +28 -11
  11. lyrics_transcriber/correction/handlers/sound_alike.py +68 -32
  12. lyrics_transcriber/correction/handlers/syllables_match.py +80 -30
  13. lyrics_transcriber/correction/handlers/word_count_match.py +36 -19
  14. lyrics_transcriber/correction/handlers/word_operations.py +68 -22
  15. lyrics_transcriber/correction/text_utils.py +3 -7
  16. lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
  17. lyrics_transcriber/frontend/.yarn/releases/yarn-4.6.0.cjs +934 -0
  18. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  19. lyrics_transcriber/frontend/dist/assets/{index-DKnNJHRK.js → index-D0Gr3Ep7.js} +16509 -9038
  20. lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +1 -0
  21. lyrics_transcriber/frontend/dist/index.html +1 -1
  22. lyrics_transcriber/frontend/package.json +6 -2
  23. lyrics_transcriber/frontend/src/App.tsx +18 -2
  24. lyrics_transcriber/frontend/src/api.ts +103 -6
  25. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +14 -6
  26. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +86 -59
  27. lyrics_transcriber/frontend/src/components/EditModal.tsx +281 -63
  28. lyrics_transcriber/frontend/src/components/FileUpload.tsx +2 -2
  29. lyrics_transcriber/frontend/src/components/Header.tsx +249 -0
  30. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +320 -266
  31. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +120 -0
  32. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +174 -52
  33. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +158 -114
  34. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +59 -78
  35. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +39 -16
  36. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +4 -10
  37. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +134 -68
  38. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +1 -1
  39. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +85 -115
  40. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  41. lyrics_transcriber/frontend/src/components/shared/types.ts +15 -7
  42. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +67 -0
  43. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  44. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +7 -7
  45. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +121 -0
  46. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  47. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  48. lyrics_transcriber/frontend/src/types.js +2 -0
  49. lyrics_transcriber/frontend/src/types.ts +70 -49
  50. lyrics_transcriber/frontend/src/validation.ts +132 -0
  51. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  52. lyrics_transcriber/frontend/yarn.lock +3752 -0
  53. lyrics_transcriber/lyrics/base_lyrics_provider.py +75 -12
  54. lyrics_transcriber/lyrics/file_provider.py +6 -5
  55. lyrics_transcriber/lyrics/genius.py +5 -2
  56. lyrics_transcriber/lyrics/spotify.py +58 -21
  57. lyrics_transcriber/output/ass/config.py +16 -5
  58. lyrics_transcriber/output/cdg.py +1 -1
  59. lyrics_transcriber/output/generator.py +22 -8
  60. lyrics_transcriber/output/plain_text.py +15 -10
  61. lyrics_transcriber/output/segment_resizer.py +16 -3
  62. lyrics_transcriber/output/subtitles.py +27 -1
  63. lyrics_transcriber/output/video.py +107 -1
  64. lyrics_transcriber/review/__init__.py +0 -1
  65. lyrics_transcriber/review/server.py +337 -164
  66. lyrics_transcriber/transcribers/audioshake.py +3 -0
  67. lyrics_transcriber/transcribers/base_transcriber.py +11 -3
  68. lyrics_transcriber/transcribers/whisper.py +11 -1
  69. lyrics_transcriber/types.py +151 -105
  70. lyrics_transcriber/utils/word_utils.py +27 -0
  71. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/METADATA +3 -1
  72. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/RECORD +75 -61
  73. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/WHEEL +1 -1
  74. lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js.map +0 -1
  75. lyrics_transcriber/frontend/package-lock.json +0 -4260
  76. lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +0 -202
  77. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/LICENSE +0 -0
  78. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/entry_points.txt +0 -0
@@ -41,10 +41,12 @@ export default function TranscriptionView({
41
41
  onElementClick,
42
42
  onWordClick,
43
43
  flashingType,
44
+ flashingHandler,
44
45
  highlightInfo,
45
46
  mode,
46
47
  onPlaySegment,
47
- currentTime = 0
48
+ currentTime = 0,
49
+ anchors = []
48
50
  }: TranscriptionViewProps) {
49
51
  const [selectedSegmentIndex, setSelectedSegmentIndex] = useState<number | null>(null)
50
52
 
@@ -56,31 +58,49 @@ export default function TranscriptionView({
56
58
  <Box sx={{ display: 'flex', flexDirection: 'column' }}>
57
59
  {data.corrected_segments.map((segment, segmentIndex) => {
58
60
  const segmentWords: TranscriptionWordPosition[] = segment.words.map(word => {
61
+ // Find if this word is part of a correction
62
+ const correction = data.corrections?.find(c =>
63
+ c.corrected_word_id === word.id ||
64
+ c.word_id === word.id
65
+ )
66
+
67
+ // Find if this word is part of an anchor sequence
59
68
  const anchor = data.anchor_sequences?.find(a =>
60
- a?.word_ids?.includes(word.id)
69
+ a.transcribed_word_ids.includes(word.id)
61
70
  )
62
71
 
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)
66
- ) : undefined
72
+ // If not in anchor, check if it belongs to a gap sequence
73
+ const gap = data.gap_sequences?.find(g => {
74
+ // Check transcribed words
75
+ const inTranscribed = g.transcribed_word_ids.includes(word.id)
67
76
 
68
- // Check if this specific word has been corrected
69
- const isWordCorrected = gap?.corrections?.some(
70
- correction => correction.word_id === word.id
71
- )
77
+ // Check reference words
78
+ const inReference = Object.values(g.reference_word_ids).some(ids =>
79
+ ids.includes(word.id)
80
+ )
81
+
82
+ // Check if this word is a corrected version
83
+ const isCorrection = data.corrections.some(c =>
84
+ (c.corrected_word_id === word.id || c.word_id === word.id) &&
85
+ g.transcribed_word_ids.includes(c.word_id)
86
+ )
87
+
88
+ return inTranscribed || inReference || isCorrection
89
+ })
72
90
 
73
91
  return {
74
92
  word: {
75
93
  id: word.id,
76
94
  text: word.text,
77
- start_time: word.start_time,
78
- end_time: word.end_time
95
+ start_time: word.start_time ?? undefined,
96
+ end_time: word.end_time ?? undefined
79
97
  },
80
98
  type: anchor ? 'anchor' : gap ? 'gap' : 'other',
81
99
  sequence: anchor || gap,
100
+ sequencePosition: anchor?.transcription_position ?? gap?.transcription_position ?? undefined,
82
101
  isInRange: true,
83
- isCorrected: isWordCorrected
102
+ isCorrected: Boolean(correction),
103
+ gap: gap
84
104
  }
85
105
  })
86
106
 
@@ -93,10 +113,10 @@ export default function TranscriptionView({
93
113
  >
94
114
  {segmentIndex}
95
115
  </SegmentIndex>
96
- {segment.start_time !== undefined && (
116
+ {segment.start_time !== null && (
97
117
  <IconButton
98
118
  size="small"
99
- onClick={() => onPlaySegment?.(segment.start_time)}
119
+ onClick={() => onPlaySegment?.(segment.start_time!)}
100
120
  sx={{ padding: '2px' }}
101
121
  >
102
122
  <PlayCircleOutlineIcon fontSize="small" />
@@ -106,14 +126,17 @@ export default function TranscriptionView({
106
126
  <TextContainer>
107
127
  <HighlightedText
108
128
  wordPositions={segmentWords}
109
- anchors={data.anchor_sequences}
129
+ anchors={anchors}
110
130
  onElementClick={onElementClick}
111
131
  onWordClick={onWordClick}
112
132
  flashingType={flashingType}
133
+ flashingHandler={flashingHandler}
113
134
  highlightInfo={highlightInfo}
114
135
  mode={mode}
115
136
  preserveSegments={true}
116
137
  currentTime={currentTime}
138
+ gaps={data.gap_sequences}
139
+ corrections={data.corrections}
117
140
  />
118
141
  </TextContainer>
119
142
  </Box>
@@ -15,7 +15,7 @@ export function useWordEdit(content: ModalContent | null) {
15
15
 
16
16
  useEffect(() => {
17
17
  if (content) {
18
- setEditedWord(content.type === 'gap' ? content.data.word : content.type === 'anchor' ? content.data.words[0] : '')
18
+ setEditedWord(content.data.word || '')
19
19
  setIsEditing(false)
20
20
  }
21
21
  }, [content])
@@ -37,11 +37,7 @@ export default function WordEditControls({ content, onUpdateCorrection, onClose
37
37
  } = useWordEdit(content)
38
38
 
39
39
  const handleStartEdit = () => {
40
- if (content.type === 'gap') {
41
- setEditedWord(content.data.word)
42
- } else if (content.type === 'anchor') {
43
- setEditedWord(content.data.words[0])
44
- }
40
+ setEditedWord(content.data.word || '')
45
41
  setIsEditing(true)
46
42
  }
47
43
 
@@ -59,10 +55,8 @@ export default function WordEditControls({ content, onUpdateCorrection, onClose
59
55
  }
60
56
 
61
57
  const handleCancelEdit = () => {
62
- if (content.type === 'gap') {
63
- setEditedWord(content.data.word)
64
- setIsEditing(false)
65
- }
58
+ setEditedWord(content.data.word || '')
59
+ setIsEditing(false)
66
60
  }
67
61
 
68
62
  const handleWordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -1,25 +1,32 @@
1
1
  import { Typography, Box } from '@mui/material'
2
- import { Word } from './Word'
2
+ import { WordComponent } from './Word'
3
3
  import { useWordClick } from '../hooks/useWordClick'
4
- import { AnchorSequence, GapSequence, HighlightInfo, InteractionMode } from '../../../types'
4
+ import {
5
+ AnchorSequence,
6
+ GapSequence,
7
+ HighlightInfo,
8
+ InteractionMode,
9
+ LyricsSegment,
10
+ Word,
11
+ WordCorrection
12
+ } from '../../../types'
5
13
  import { ModalContent } from '../../LyricsAnalyzer'
6
- import { WordClickInfo, TranscriptionWordPosition, FlashType, LinePosition } from '../types'
14
+ import type { FlashType, LinePosition, TranscriptionWordPosition, WordClickInfo } from '../types'
7
15
  import React from 'react'
8
- import ContentCopyIcon from '@mui/icons-material/ContentCopy';
9
- import IconButton from '@mui/material/IconButton';
16
+ import ContentCopyIcon from '@mui/icons-material/ContentCopy'
17
+ import IconButton from '@mui/material/IconButton'
18
+ import { getWordsFromIds } from '../utils/wordUtils'
10
19
 
11
20
  export interface HighlightedTextProps {
12
- // Input can be either raw text or pre-processed word positions
13
21
  text?: string
14
- wordPositions?: TranscriptionWordPosition[]
15
- // Common props
22
+ segments?: LyricsSegment[]
23
+ wordPositions: TranscriptionWordPosition[]
16
24
  anchors: AnchorSequence[]
17
25
  highlightInfo: HighlightInfo | null
18
26
  mode: InteractionMode
19
27
  onElementClick: (content: ModalContent) => void
20
28
  onWordClick?: (info: WordClickInfo) => void
21
29
  flashingType: FlashType
22
- // Reference-specific props
23
30
  isReference?: boolean
24
31
  currentSource?: string
25
32
  preserveSegments?: boolean
@@ -27,11 +34,14 @@ export interface HighlightedTextProps {
27
34
  currentTime?: number
28
35
  referenceCorrections?: Map<string, string>
29
36
  gaps?: GapSequence[]
37
+ flashingHandler?: string | null
38
+ corrections?: WordCorrection[]
30
39
  }
31
40
 
32
41
  export function HighlightedText({
33
42
  text,
34
- wordPositions,
43
+ segments,
44
+ wordPositions = [] as TranscriptionWordPosition[],
35
45
  anchors,
36
46
  highlightInfo,
37
47
  mode,
@@ -39,12 +49,14 @@ export function HighlightedText({
39
49
  onWordClick,
40
50
  flashingType,
41
51
  isReference,
42
- currentSource,
52
+ currentSource = '',
43
53
  preserveSegments = false,
44
54
  linePositions = [],
45
55
  currentTime = 0,
46
56
  referenceCorrections = new Map(),
47
- gaps = []
57
+ gaps = [],
58
+ flashingHandler,
59
+ corrections = [],
48
60
  }: HighlightedTextProps) {
49
61
  const { handleWordClick } = useWordClick({
50
62
  mode,
@@ -52,74 +64,91 @@ export function HighlightedText({
52
64
  onWordClick,
53
65
  isReference,
54
66
  currentSource,
55
- gaps
67
+ gaps,
68
+ anchors,
69
+ corrections
56
70
  })
57
71
 
58
72
  const shouldWordFlash = (wordPos: TranscriptionWordPosition | { word: string; id: string }): boolean => {
59
- if (!flashingType) return false
73
+ if (!flashingType) {
74
+ return false;
75
+ }
60
76
 
61
77
  if ('type' in wordPos) {
62
- // Handle TranscriptionWordPosition
78
+ // Add handler-specific flashing
79
+ if (flashingType === 'handler' && flashingHandler) {
80
+ console.log('Checking handler flash for word:', wordPos.word.text);
81
+ console.log('Current flashingHandler:', flashingHandler);
82
+ console.log('Word ID:', wordPos.word.id);
83
+
84
+ const shouldFlash = corrections.some(correction =>
85
+ correction.handler === flashingHandler &&
86
+ (correction.corrected_word_id === wordPos.word.id ||
87
+ correction.word_id === wordPos.word.id)
88
+ );
89
+
90
+ console.log('Should flash:', shouldFlash);
91
+ return shouldFlash;
92
+ }
93
+
63
94
  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
- )
95
+ const isCorrected = (
96
+ // Check corrections array for this word
97
+ corrections.some(correction =>
98
+ (correction.word_id === wordPos.word.id ||
99
+ correction.corrected_word_id === wordPos.word.id) &&
100
+ gap.transcribed_word_ids.includes(correction.word_id)
101
+ ) ||
102
+ // Also check if marked as corrected in wordPos
103
+ wordPos.isCorrected
104
+ )
68
105
 
69
106
  return Boolean(
70
107
  (flashingType === 'anchor' && wordPos.type === 'anchor') ||
71
108
  (flashingType === 'corrected' && isCorrected) ||
72
109
  (flashingType === 'uncorrected' && wordPos.type === 'gap' && !isCorrected) ||
73
110
  (flashingType === 'word' && (
74
- // Handle anchor highlighting
111
+ // For anchors
75
112
  (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
113
+ (isReference && currentSource && highlightInfo.sequence
114
+ ? getWordsFromIds(segments || [],
115
+ (highlightInfo.sequence as AnchorSequence).reference_word_ids[currentSource] || []
116
+ ).some(w => w.id === wordPos.word.id)
117
+ : getWordsFromIds(segments || [],
118
+ (highlightInfo.sequence as AnchorSequence).transcribed_word_ids
119
+ ).some(w => w.id === wordPos.word.id)
120
+ )) ||
121
+ // For gaps
78
122
  (highlightInfo?.type === 'gap' && wordPos.type === 'gap' &&
79
- highlightInfo.word_ids?.includes(wordPos.word.id))
80
- ))
81
- )
82
- } else {
83
- // Handle reference word
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
- )
106
-
107
- return Boolean(
108
- (flashingType === 'anchor' && anchor) ||
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
123
+ (isReference && currentSource && highlightInfo.sequence
124
+ ? getWordsFromIds(segments || [],
125
+ (highlightInfo.sequence as GapSequence).reference_word_ids[currentSource] || []
126
+ ).some(w => w.id === wordPos.word.id)
127
+ : getWordsFromIds(segments || [],
128
+ (highlightInfo.sequence as GapSequence).transcribed_word_ids
129
+ ).some(w => w.id === wordPos.word.id))
130
+ ) ||
131
+ // For corrections
132
+ (highlightInfo?.type === 'correction' && isReference && currentSource &&
133
+ highlightInfo.correction?.reference_positions?.[currentSource]?.toString() === wordPos.word.id)
115
134
  ))
116
135
  )
117
136
  }
137
+ return false
118
138
  }
119
139
 
120
- const shouldHighlightWord = (wordPos: TranscriptionWordPosition): boolean => {
121
- if (!currentTime || !wordPos.word.start_time || !wordPos.word.end_time) return false
122
- return currentTime >= wordPos.word.start_time && currentTime <= wordPos.word.end_time
140
+ const shouldHighlightWord = (wordPos: TranscriptionWordPosition | { word: string; id: string }): boolean => {
141
+ // Don't highlight words in reference view
142
+ if (isReference) return false
143
+
144
+ if ('type' in wordPos && currentTime !== undefined && 'start_time' in wordPos.word) {
145
+ const word = wordPos.word as Word
146
+ return word.start_time !== null &&
147
+ word.end_time !== null &&
148
+ currentTime >= word.start_time &&
149
+ currentTime <= word.end_time
150
+ }
151
+ return false
123
152
  }
124
153
 
125
154
  const handleCopyLine = (text: string) => {
@@ -127,16 +156,17 @@ export function HighlightedText({
127
156
  };
128
157
 
129
158
  const renderContent = () => {
130
- if (wordPositions) {
159
+ if (wordPositions && !segments) {
131
160
  return wordPositions.map((wordPos, index) => (
132
161
  <React.Fragment key={wordPos.word.id}>
133
- <Word
162
+ <WordComponent
163
+ key={`${wordPos.word.id}-${index}`}
134
164
  word={wordPos.word.text}
135
165
  shouldFlash={shouldWordFlash(wordPos)}
136
- isCurrentlyPlaying={shouldHighlightWord(wordPos)}
137
166
  isAnchor={wordPos.type === 'anchor'}
138
- isCorrectedGap={wordPos.type === 'gap' && wordPos.isCorrected}
167
+ isCorrectedGap={wordPos.isCorrected}
139
168
  isUncorrectedGap={wordPos.type === 'gap' && !wordPos.isCorrected}
169
+ isCurrentlyPlaying={shouldHighlightWord(wordPos)}
140
170
  onClick={() => handleWordClick(
141
171
  wordPos.word.text,
142
172
  wordPos.word.id,
@@ -147,6 +177,42 @@ export function HighlightedText({
147
177
  {index < wordPositions.length - 1 && ' '}
148
178
  </React.Fragment>
149
179
  ))
180
+ } else if (segments) {
181
+ return segments.map((segment) => (
182
+ <Box key={segment.id} sx={{ display: 'flex', alignItems: 'flex-start' }}>
183
+ <Box sx={{ flex: 1 }}>
184
+ {segment.words.map((word, wordIndex) => {
185
+ const wordPos = wordPositions.find((pos: TranscriptionWordPosition) =>
186
+ pos.word.id === word.id
187
+ );
188
+
189
+ const anchor = wordPos?.type === 'anchor' ? anchors?.find(a =>
190
+ (a.reference_word_ids[currentSource] || []).includes(word.id)
191
+ ) : undefined;
192
+
193
+ const hasCorrection = referenceCorrections.has(word.id);
194
+ const isUncorrectedGap = wordPos?.type === 'gap' && !hasCorrection;
195
+
196
+ const sequence = wordPos?.type === 'gap' ? wordPos.sequence as GapSequence : undefined;
197
+
198
+ return (
199
+ <React.Fragment key={word.id}>
200
+ <WordComponent
201
+ word={word.text}
202
+ shouldFlash={shouldWordFlash(wordPos || { word: word.text, id: word.id })}
203
+ isAnchor={Boolean(anchor)}
204
+ isCorrectedGap={hasCorrection}
205
+ isUncorrectedGap={isUncorrectedGap}
206
+ isCurrentlyPlaying={shouldHighlightWord(wordPos || { word: word.text, id: word.id })}
207
+ onClick={() => handleWordClick(word.text, word.id, anchor, sequence)}
208
+ />
209
+ {wordIndex < segment.words.length - 1 && ' '}
210
+ </React.Fragment>
211
+ );
212
+ })}
213
+ </Box>
214
+ </Box>
215
+ ));
150
216
  } else if (text) {
151
217
  const lines = text.split('\n')
152
218
  let wordCount = 0
@@ -219,20 +285,20 @@ export function HighlightedText({
219
285
  wordCount++
220
286
 
221
287
  const anchor = currentSource ? anchors?.find(a =>
222
- a?.reference_word_ids?.[currentSource]?.includes(wordId)
288
+ a.reference_word_ids[currentSource]?.includes(wordId)
223
289
  ) : undefined
224
290
 
225
- // Check if this word has a correction
226
291
  const hasCorrection = referenceCorrections.has(wordId)
227
292
 
228
293
  return (
229
- <Word
294
+ <WordComponent
230
295
  key={wordId}
231
296
  word={word}
232
297
  shouldFlash={shouldWordFlash({ word, id: wordId })}
233
298
  isAnchor={Boolean(anchor)}
234
299
  isCorrectedGap={hasCorrection}
235
300
  isUncorrectedGap={false}
301
+ isCurrentlyPlaying={shouldHighlightWord({ word, id: wordId })}
236
302
  onClick={() => handleWordClick(word, wordId, anchor, undefined)}
237
303
  />
238
304
  )
@@ -3,7 +3,7 @@ import { COLORS } from '../constants'
3
3
  import { HighlightedWord } from '../styles'
4
4
  import { WordProps } from '../types'
5
5
 
6
- export const Word = React.memo(function Word({
6
+ export const WordComponent = React.memo(function Word({
7
7
  word,
8
8
  shouldFlash,
9
9
  isAnchor,