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.
- lyrics_transcriber/core/controller.py +22 -2
- lyrics_transcriber/correction/corrector.py +8 -8
- lyrics_transcriber/correction/handlers/base.py +4 -0
- lyrics_transcriber/correction/handlers/extend_anchor.py +22 -2
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +21 -10
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +21 -11
- lyrics_transcriber/correction/handlers/syllables_match.py +4 -4
- lyrics_transcriber/correction/handlers/word_count_match.py +19 -10
- lyrics_transcriber/correction/handlers/word_operations.py +8 -2
- lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js +26696 -0
- lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/package.json +3 -2
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +1 -2
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +76 -70
- lyrics_transcriber/frontend/src/components/EditModal.tsx +11 -2
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +154 -128
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +42 -4
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +59 -15
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +71 -16
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +16 -19
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +3 -3
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +72 -57
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +113 -41
- lyrics_transcriber/frontend/src/components/shared/types.ts +6 -3
- lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +202 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +23 -24
- lyrics_transcriber/frontend/src/types.ts +25 -15
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/vite.config.js +4 -0
- lyrics_transcriber/frontend/vite.config.ts +4 -0
- lyrics_transcriber/lyrics/genius.py +41 -12
- lyrics_transcriber/output/cdg.py +33 -6
- lyrics_transcriber/output/cdgmaker/composer.py +839 -534
- lyrics_transcriber/output/video.py +17 -7
- lyrics_transcriber/review/server.py +22 -8
- {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/METADATA +3 -2
- {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/RECORD +41 -40
- {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/entry_points.txt +1 -0
- lyrics_transcriber/frontend/dist/assets/index-ztlAYPYT.js +0 -181
- lyrics_transcriber/frontend/src/components/shared/utils/newlineCalculator.ts +0 -37
- {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/LICENSE +0 -0
- {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: '
|
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: '
|
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: '
|
35
|
+
top: '20px',
|
34
36
|
width: '1px',
|
35
|
-
height: '
|
36
|
-
backgroundColor: theme.palette.grey[
|
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:
|
48
|
+
top: '5px',
|
42
49
|
transform: 'translateX(-50%)',
|
43
|
-
fontSize: '0.
|
44
|
-
color: theme.palette.
|
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: '
|
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
|
-
|
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
|
-
|
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
|
151
|
-
|
152
|
-
{
|
153
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
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={
|
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?: (
|
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.
|
50
|
+
onUpdateCorrection(content.data.wordId, [])
|
51
51
|
onClose()
|
52
52
|
}
|
53
53
|
|
54
54
|
const handleSaveEdit = () => {
|
55
55
|
if (onUpdateCorrection) {
|
56
|
-
onUpdateCorrection(content.data.
|
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;
|
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
|
60
|
-
|
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' &&
|
65
|
-
(flashingType === 'uncorrected' && wordPos.type === 'gap' && !
|
66
|
-
(flashingType === 'word' &&
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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' &&
|
88
|
-
|
89
|
-
(
|
90
|
-
|
91
|
-
|
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={
|
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' &&
|
116
|
-
isUncorrectedGap={wordPos.type === 'gap' && !
|
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.
|
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
|
152
|
+
let wordCount = 0
|
130
153
|
|
131
154
|
return lines.map((line, lineIndex) => {
|
132
|
-
const currentLinePosition = linePositions?.find(
|
155
|
+
const currentLinePosition = linePositions?.find(pos => pos.position === wordCount)
|
133
156
|
if (currentLinePosition?.isEmpty) {
|
134
|
-
|
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
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
//
|
203
|
-
const
|
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={
|
230
|
+
key={wordId}
|
214
231
|
word={word}
|
215
|
-
shouldFlash={shouldWordFlash({ word,
|
216
|
-
isCurrentlyPlaying={shouldHighlightWord(wordPos)}
|
232
|
+
shouldFlash={shouldWordFlash({ word, id: wordId })}
|
217
233
|
isAnchor={Boolean(anchor)}
|
218
|
-
isCorrectedGap={
|
234
|
+
isCorrectedGap={hasCorrection}
|
219
235
|
isUncorrectedGap={false}
|
220
|
-
onClick={() => handleWordClick(word,
|
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
|
|