lyrics-transcriber 0.43.0__py3-none-any.whl → 0.44.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 +58 -24
- lyrics_transcriber/correction/anchor_sequence.py +22 -8
- lyrics_transcriber/correction/corrector.py +47 -3
- lyrics_transcriber/correction/handlers/llm.py +15 -12
- lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
- lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
- lyrics_transcriber/frontend/dist/assets/{index-D0Gr3Ep7.js → index-DVoI6Z16.js} +10799 -7490
- lyrics_transcriber/frontend/dist/assets/index-DVoI6Z16.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/App.tsx +4 -4
- lyrics_transcriber/frontend/src/api.ts +37 -0
- lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +14 -10
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +62 -56
- lyrics_transcriber/frontend/src/components/EditModal.tsx +232 -237
- lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
- lyrics_transcriber/frontend/src/components/GlobalSyncEditor.tsx +675 -0
- lyrics_transcriber/frontend/src/components/Header.tsx +141 -101
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +146 -80
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +22 -13
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +1 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +29 -12
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +21 -4
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +29 -15
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +34 -16
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +186 -0
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +89 -41
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +9 -2
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +28 -3
- lyrics_transcriber/frontend/src/components/shared/types.ts +17 -2
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +63 -14
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +192 -0
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +267 -0
- lyrics_transcriber/frontend/src/main.tsx +7 -1
- lyrics_transcriber/frontend/src/theme.ts +177 -0
- lyrics_transcriber/frontend/src/types.ts +1 -1
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/lyrics/base_lyrics_provider.py +2 -2
- lyrics_transcriber/lyrics/user_input_provider.py +44 -0
- lyrics_transcriber/output/generator.py +40 -12
- lyrics_transcriber/output/video.py +18 -8
- lyrics_transcriber/review/server.py +238 -8
- {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/METADATA +3 -2
- {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/RECORD +47 -41
- lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +0 -1
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +0 -252
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +0 -110
- {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/WHEEL +0 -0
- {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/entry_points.txt +0 -0
@@ -9,14 +9,15 @@ interface TimelineEditorProps {
|
|
9
9
|
onWordUpdate: (index: number, updates: Partial<Word>) => void
|
10
10
|
currentTime?: number
|
11
11
|
onPlaySegment?: (time: number) => void
|
12
|
+
showPlaybackIndicator?: boolean
|
12
13
|
}
|
13
14
|
|
14
15
|
const TimelineContainer = styled(Box)(({ theme }) => ({
|
15
16
|
position: 'relative',
|
16
|
-
height: '
|
17
|
+
height: '75px',
|
17
18
|
backgroundColor: theme.palette.grey[200],
|
18
19
|
borderRadius: theme.shape.borderRadius,
|
19
|
-
margin: theme.spacing(
|
20
|
+
margin: theme.spacing(1, 0),
|
20
21
|
padding: theme.spacing(0, 1),
|
21
22
|
}))
|
22
23
|
|
@@ -68,6 +69,7 @@ const TimelineWord = styled(Box)(({ theme }) => ({
|
|
68
69
|
fontSize: '0.875rem',
|
69
70
|
fontFamily: 'sans-serif',
|
70
71
|
transition: 'background-color 0.1s ease',
|
72
|
+
boxSizing: 'border-box',
|
71
73
|
'&.highlighted': {
|
72
74
|
backgroundColor: theme.palette.secondary.main,
|
73
75
|
}
|
@@ -76,17 +78,27 @@ const TimelineWord = styled(Box)(({ theme }) => ({
|
|
76
78
|
const ResizeHandle = styled(Box)(({ theme }) => ({
|
77
79
|
position: 'absolute',
|
78
80
|
top: 0,
|
79
|
-
width:
|
81
|
+
width: 10,
|
80
82
|
height: '100%',
|
81
83
|
cursor: 'col-resize',
|
82
84
|
'&:hover': {
|
83
85
|
backgroundColor: theme.palette.primary.light,
|
86
|
+
opacity: 0.8,
|
87
|
+
boxShadow: `0 0 0 1px ${theme.palette.primary.dark}`,
|
84
88
|
},
|
85
89
|
'&.left': {
|
86
|
-
left:
|
90
|
+
left: 0,
|
91
|
+
right: 'auto',
|
92
|
+
paddingRight: 0,
|
93
|
+
borderTopLeftRadius: theme.shape.borderRadius,
|
94
|
+
borderBottomLeftRadius: theme.shape.borderRadius,
|
87
95
|
},
|
88
96
|
'&.right': {
|
89
|
-
right:
|
97
|
+
right: 0,
|
98
|
+
left: 'auto',
|
99
|
+
paddingLeft: 0,
|
100
|
+
borderTopRightRadius: theme.shape.borderRadius,
|
101
|
+
borderBottomRightRadius: theme.shape.borderRadius,
|
90
102
|
}
|
91
103
|
}))
|
92
104
|
|
@@ -102,7 +114,7 @@ const TimelineCursor = styled(Box)(({ theme }) => ({
|
|
102
114
|
zIndex: 1, // Ensure it's above other elements
|
103
115
|
}))
|
104
116
|
|
105
|
-
export default function TimelineEditor({ words, startTime, endTime, onWordUpdate, currentTime = 0, onPlaySegment }: TimelineEditorProps) {
|
117
|
+
export default function TimelineEditor({ words, startTime, endTime, onWordUpdate, currentTime = 0, onPlaySegment, showPlaybackIndicator = true }: TimelineEditorProps) {
|
106
118
|
const containerRef = useRef<HTMLDivElement>(null)
|
107
119
|
const [dragState, setDragState] = useState<{
|
108
120
|
wordIndex: number
|
@@ -297,12 +309,14 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
|
|
297
309
|
</TimelineRuler>
|
298
310
|
|
299
311
|
{/* Add cursor line */}
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
312
|
+
{showPlaybackIndicator && (
|
313
|
+
<TimelineCursor
|
314
|
+
sx={{
|
315
|
+
left: `${timeToPosition(currentTime)}%`,
|
316
|
+
display: currentTime >= startTime && currentTime <= endTime ? 'block' : 'none'
|
317
|
+
}}
|
318
|
+
/>
|
319
|
+
)}
|
306
320
|
|
307
321
|
{words.map((word, index) => {
|
308
322
|
// Skip words with null timestamps
|
@@ -311,8 +325,8 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
|
|
311
325
|
const leftPosition = timeToPosition(word.start_time)
|
312
326
|
const rightPosition = timeToPosition(word.end_time)
|
313
327
|
const width = rightPosition - leftPosition
|
314
|
-
|
315
|
-
const adjustedWidth =
|
328
|
+
// Remove the visual padding that creates gaps
|
329
|
+
const adjustedWidth = width
|
316
330
|
|
317
331
|
return (
|
318
332
|
<TimelineWord
|
@@ -321,7 +335,7 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
|
|
321
335
|
sx={{
|
322
336
|
left: `${leftPosition}%`,
|
323
337
|
width: `${adjustedWidth}%`,
|
324
|
-
maxWidth: `calc(${100 - leftPosition}%
|
338
|
+
maxWidth: `calc(${100 - leftPosition}%)`,
|
325
339
|
}}
|
326
340
|
onMouseDown={(e) => {
|
327
341
|
e.stopPropagation()
|
@@ -9,14 +9,16 @@ import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
|
|
9
9
|
|
10
10
|
const SegmentIndex = styled(Typography)(({ theme }) => ({
|
11
11
|
color: theme.palette.text.secondary,
|
12
|
-
width: '
|
13
|
-
minWidth: '
|
12
|
+
width: '1.8em',
|
13
|
+
minWidth: '1.8em',
|
14
14
|
textAlign: 'right',
|
15
|
-
marginRight: theme.spacing(
|
15
|
+
marginRight: theme.spacing(0.8),
|
16
16
|
userSelect: 'none',
|
17
17
|
fontFamily: 'monospace',
|
18
18
|
cursor: 'pointer',
|
19
|
-
paddingTop: '
|
19
|
+
paddingTop: '1px',
|
20
|
+
fontSize: '0.8rem',
|
21
|
+
lineHeight: 1.2,
|
20
22
|
'&:hover': {
|
21
23
|
textDecoration: 'underline',
|
22
24
|
},
|
@@ -30,10 +32,10 @@ const TextContainer = styled(Box)({
|
|
30
32
|
const SegmentControls = styled(Box)({
|
31
33
|
display: 'flex',
|
32
34
|
alignItems: 'center',
|
33
|
-
gap: '
|
34
|
-
minWidth: '
|
35
|
-
paddingTop: '
|
36
|
-
paddingRight: '
|
35
|
+
gap: '2px',
|
36
|
+
minWidth: '2.5em',
|
37
|
+
paddingTop: '1px',
|
38
|
+
paddingRight: '4px'
|
37
39
|
})
|
38
40
|
|
39
41
|
export default function TranscriptionView({
|
@@ -51,11 +53,13 @@ export default function TranscriptionView({
|
|
51
53
|
const [selectedSegmentIndex, setSelectedSegmentIndex] = useState<number | null>(null)
|
52
54
|
|
53
55
|
return (
|
54
|
-
<Paper sx={{ p:
|
55
|
-
<
|
56
|
-
|
57
|
-
|
58
|
-
|
56
|
+
<Paper sx={{ p: 0.8 }}>
|
57
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.5 }}>
|
58
|
+
<Typography variant="h6" sx={{ fontSize: '0.9rem', mb: 0 }}>
|
59
|
+
Corrected Transcription
|
60
|
+
</Typography>
|
61
|
+
</Box>
|
62
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.2 }}>
|
59
63
|
{data.corrected_segments.map((segment, segmentIndex) => {
|
60
64
|
const segmentWords: TranscriptionWordPosition[] = segment.words.map(word => {
|
61
65
|
// Find if this word is part of a correction
|
@@ -105,7 +109,15 @@ export default function TranscriptionView({
|
|
105
109
|
})
|
106
110
|
|
107
111
|
return (
|
108
|
-
<Box key={segment.id} sx={{
|
112
|
+
<Box key={segment.id} sx={{
|
113
|
+
display: 'flex',
|
114
|
+
alignItems: 'flex-start',
|
115
|
+
width: '100%',
|
116
|
+
mb: 0,
|
117
|
+
'&:hover': {
|
118
|
+
backgroundColor: 'rgba(0, 0, 0, 0.03)'
|
119
|
+
}
|
120
|
+
}}>
|
109
121
|
<SegmentControls>
|
110
122
|
<SegmentIndex
|
111
123
|
variant="body2"
|
@@ -117,9 +129,15 @@ export default function TranscriptionView({
|
|
117
129
|
<IconButton
|
118
130
|
size="small"
|
119
131
|
onClick={() => onPlaySegment?.(segment.start_time!)}
|
120
|
-
sx={{
|
132
|
+
sx={{
|
133
|
+
padding: '1px',
|
134
|
+
height: '18px',
|
135
|
+
width: '18px',
|
136
|
+
minHeight: '18px',
|
137
|
+
minWidth: '18px'
|
138
|
+
}}
|
121
139
|
>
|
122
|
-
<PlayCircleOutlineIcon
|
140
|
+
<PlayCircleOutlineIcon sx={{ fontSize: '0.9rem' }} />
|
123
141
|
</IconButton>
|
124
142
|
)}
|
125
143
|
</SegmentControls>
|
@@ -0,0 +1,186 @@
|
|
1
|
+
import { Box, Button, Typography } from '@mui/material'
|
2
|
+
import AddIcon from '@mui/icons-material/Add'
|
3
|
+
import MergeIcon from '@mui/icons-material/CallMerge'
|
4
|
+
import CallSplitIcon from '@mui/icons-material/CallSplit'
|
5
|
+
|
6
|
+
interface WordDividerProps {
|
7
|
+
onAddWord: () => void
|
8
|
+
onMergeWords?: () => void
|
9
|
+
onAddSegmentBefore?: () => void
|
10
|
+
onAddSegmentAfter?: () => void
|
11
|
+
onSplitSegment?: () => void
|
12
|
+
onMergeSegment?: () => void
|
13
|
+
canMerge?: boolean
|
14
|
+
isFirst?: boolean
|
15
|
+
isLast?: boolean
|
16
|
+
sx?: any
|
17
|
+
}
|
18
|
+
|
19
|
+
const buttonTextStyle = {
|
20
|
+
color: 'rgba(0, 0, 0, 0.6)',
|
21
|
+
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
22
|
+
fontWeight: 400,
|
23
|
+
fontSize: '0.7rem',
|
24
|
+
lineHeight: '1.4375em',
|
25
|
+
textTransform: 'none'
|
26
|
+
}
|
27
|
+
|
28
|
+
const buttonBaseStyle = {
|
29
|
+
minHeight: 0,
|
30
|
+
padding: '2px 8px',
|
31
|
+
'& .MuiButton-startIcon': {
|
32
|
+
marginRight: 0.5
|
33
|
+
},
|
34
|
+
'& .MuiSvgIcon-root': {
|
35
|
+
fontSize: '1.2rem'
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
export default function WordDivider({
|
40
|
+
onAddWord,
|
41
|
+
onMergeWords,
|
42
|
+
onAddSegmentBefore,
|
43
|
+
onAddSegmentAfter,
|
44
|
+
onSplitSegment,
|
45
|
+
onMergeSegment,
|
46
|
+
canMerge = false,
|
47
|
+
isFirst = false,
|
48
|
+
isLast = false,
|
49
|
+
sx = {}
|
50
|
+
}: WordDividerProps) {
|
51
|
+
return (
|
52
|
+
<Box
|
53
|
+
sx={{
|
54
|
+
display: 'flex',
|
55
|
+
alignItems: 'center',
|
56
|
+
justifyContent: 'center',
|
57
|
+
height: '20px',
|
58
|
+
my: -0.5,
|
59
|
+
width: '50%',
|
60
|
+
backgroundColor: '#fff',
|
61
|
+
...sx
|
62
|
+
}}
|
63
|
+
>
|
64
|
+
<Box sx={{
|
65
|
+
display: 'flex',
|
66
|
+
alignItems: 'center',
|
67
|
+
gap: 1,
|
68
|
+
backgroundColor: '#fff',
|
69
|
+
padding: '0 8px',
|
70
|
+
zIndex: 1
|
71
|
+
}}>
|
72
|
+
<Button
|
73
|
+
onClick={onAddWord}
|
74
|
+
title="Add Word"
|
75
|
+
size="small"
|
76
|
+
startIcon={<AddIcon />}
|
77
|
+
sx={{
|
78
|
+
...buttonBaseStyle,
|
79
|
+
color: 'primary.main',
|
80
|
+
}}
|
81
|
+
>
|
82
|
+
<Typography sx={buttonTextStyle}>
|
83
|
+
Add Word
|
84
|
+
</Typography>
|
85
|
+
</Button>
|
86
|
+
{isFirst && (
|
87
|
+
<>
|
88
|
+
<Button
|
89
|
+
onClick={onAddSegmentBefore}
|
90
|
+
title="Add Segment"
|
91
|
+
size="small"
|
92
|
+
startIcon={<AddIcon sx={{ transform: 'rotate(90deg)' }} />}
|
93
|
+
sx={{
|
94
|
+
...buttonBaseStyle,
|
95
|
+
color: 'success.main',
|
96
|
+
}}
|
97
|
+
>
|
98
|
+
<Typography sx={buttonTextStyle}>
|
99
|
+
Add Segment
|
100
|
+
</Typography>
|
101
|
+
</Button>
|
102
|
+
<Button
|
103
|
+
onClick={onMergeSegment}
|
104
|
+
title="Merge with Previous Segment"
|
105
|
+
size="small"
|
106
|
+
startIcon={<MergeIcon sx={{ transform: 'rotate(90deg)' }} />}
|
107
|
+
sx={{
|
108
|
+
...buttonBaseStyle,
|
109
|
+
color: 'warning.main',
|
110
|
+
}}
|
111
|
+
>
|
112
|
+
<Typography sx={buttonTextStyle}>
|
113
|
+
Merge Segment
|
114
|
+
</Typography>
|
115
|
+
</Button>
|
116
|
+
</>
|
117
|
+
)}
|
118
|
+
{onMergeWords && !isLast && (
|
119
|
+
<Button
|
120
|
+
onClick={onMergeWords}
|
121
|
+
title="Merge Words"
|
122
|
+
size="small"
|
123
|
+
startIcon={<MergeIcon sx={{ transform: 'rotate(90deg)' }} />}
|
124
|
+
disabled={!canMerge}
|
125
|
+
sx={{
|
126
|
+
...buttonBaseStyle,
|
127
|
+
color: 'primary.main',
|
128
|
+
}}
|
129
|
+
>
|
130
|
+
<Typography sx={buttonTextStyle}>
|
131
|
+
Merge Words
|
132
|
+
</Typography>
|
133
|
+
</Button>
|
134
|
+
)}
|
135
|
+
{onSplitSegment && !isLast && (
|
136
|
+
<Button
|
137
|
+
onClick={onSplitSegment}
|
138
|
+
title="Split Segment"
|
139
|
+
size="small"
|
140
|
+
startIcon={<CallSplitIcon sx={{ transform: 'rotate(90deg)' }} />}
|
141
|
+
sx={{
|
142
|
+
...buttonBaseStyle,
|
143
|
+
color: 'warning.main',
|
144
|
+
}}
|
145
|
+
>
|
146
|
+
<Typography sx={buttonTextStyle}>
|
147
|
+
Split Segment
|
148
|
+
</Typography>
|
149
|
+
</Button>
|
150
|
+
)}
|
151
|
+
{isLast && (
|
152
|
+
<>
|
153
|
+
<Button
|
154
|
+
onClick={onAddSegmentAfter}
|
155
|
+
title="Add Segment"
|
156
|
+
size="small"
|
157
|
+
startIcon={<AddIcon sx={{ transform: 'rotate(90deg)' }} />}
|
158
|
+
sx={{
|
159
|
+
...buttonBaseStyle,
|
160
|
+
color: 'success.main',
|
161
|
+
}}
|
162
|
+
>
|
163
|
+
<Typography sx={buttonTextStyle}>
|
164
|
+
Add Segment
|
165
|
+
</Typography>
|
166
|
+
</Button>
|
167
|
+
<Button
|
168
|
+
onClick={onMergeSegment}
|
169
|
+
title="Merge with Next Segment"
|
170
|
+
size="small"
|
171
|
+
startIcon={<MergeIcon sx={{ transform: 'rotate(90deg)' }} />}
|
172
|
+
sx={{
|
173
|
+
...buttonBaseStyle,
|
174
|
+
color: 'warning.main',
|
175
|
+
}}
|
176
|
+
>
|
177
|
+
<Typography sx={buttonTextStyle}>
|
178
|
+
Merge Segment
|
179
|
+
</Typography>
|
180
|
+
</Button>
|
181
|
+
</>
|
182
|
+
)}
|
183
|
+
</Box>
|
184
|
+
</Box>
|
185
|
+
)
|
186
|
+
}
|
@@ -173,13 +173,28 @@ export function HighlightedText({
|
|
173
173
|
wordPos.type === 'anchor' ? wordPos.sequence as AnchorSequence : undefined,
|
174
174
|
wordPos.type === 'gap' ? wordPos.sequence as GapSequence : undefined
|
175
175
|
)}
|
176
|
+
correction={(() => {
|
177
|
+
const correction = corrections?.find(c =>
|
178
|
+
c.corrected_word_id === wordPos.word.id ||
|
179
|
+
c.word_id === wordPos.word.id
|
180
|
+
);
|
181
|
+
return correction ? {
|
182
|
+
originalWord: correction.original_word,
|
183
|
+
handler: correction.handler,
|
184
|
+
confidence: correction.confidence
|
185
|
+
} : null;
|
186
|
+
})()}
|
176
187
|
/>
|
177
188
|
{index < wordPositions.length - 1 && ' '}
|
178
189
|
</React.Fragment>
|
179
190
|
))
|
180
191
|
} else if (segments) {
|
181
192
|
return segments.map((segment) => (
|
182
|
-
<Box key={segment.id} sx={{
|
193
|
+
<Box key={segment.id} sx={{
|
194
|
+
display: 'flex',
|
195
|
+
alignItems: 'flex-start',
|
196
|
+
mb: 0
|
197
|
+
}}>
|
183
198
|
<Box sx={{ flex: 1 }}>
|
184
199
|
{segment.words.map((word, wordIndex) => {
|
185
200
|
const wordPos = wordPositions.find((pos: TranscriptionWordPosition) =>
|
@@ -195,6 +210,18 @@ export function HighlightedText({
|
|
195
210
|
|
196
211
|
const sequence = wordPos?.type === 'gap' ? wordPos.sequence as GapSequence : undefined;
|
197
212
|
|
213
|
+
// Find correction information for the tooltip
|
214
|
+
const correction = corrections?.find(c =>
|
215
|
+
c.corrected_word_id === word.id ||
|
216
|
+
c.word_id === word.id
|
217
|
+
);
|
218
|
+
|
219
|
+
const correctionInfo = correction ? {
|
220
|
+
originalWord: correction.original_word,
|
221
|
+
handler: correction.handler,
|
222
|
+
confidence: correction.confidence
|
223
|
+
} : null;
|
224
|
+
|
198
225
|
return (
|
199
226
|
<React.Fragment key={word.id}>
|
200
227
|
<WordComponent
|
@@ -205,6 +232,7 @@ export function HighlightedText({
|
|
205
232
|
isUncorrectedGap={isUncorrectedGap}
|
206
233
|
isCurrentlyPlaying={shouldHighlightWord(wordPos || { word: word.text, id: word.id })}
|
207
234
|
onClick={() => handleWordClick(word.text, word.id, anchor, sequence)}
|
235
|
+
correction={correctionInfo}
|
208
236
|
/>
|
209
237
|
{wordIndex < segment.words.length - 1 && ' '}
|
210
238
|
</React.Fragment>
|
@@ -222,7 +250,12 @@ export function HighlightedText({
|
|
222
250
|
if (currentLinePosition?.isEmpty) {
|
223
251
|
wordCount++
|
224
252
|
return (
|
225
|
-
<Box key={`empty-${lineIndex}`} sx={{
|
253
|
+
<Box key={`empty-${lineIndex}`} sx={{
|
254
|
+
display: 'flex',
|
255
|
+
alignItems: 'flex-start',
|
256
|
+
mb: 0,
|
257
|
+
lineHeight: 1
|
258
|
+
}}>
|
226
259
|
<Typography
|
227
260
|
component="span"
|
228
261
|
sx={{
|
@@ -233,20 +266,58 @@ export function HighlightedText({
|
|
233
266
|
marginRight: 1,
|
234
267
|
userSelect: 'none',
|
235
268
|
fontFamily: 'monospace',
|
236
|
-
paddingTop: '
|
269
|
+
paddingTop: '1px',
|
270
|
+
fontSize: '0.8rem',
|
271
|
+
lineHeight: 1
|
237
272
|
}}
|
238
273
|
>
|
239
274
|
{currentLinePosition.lineNumber}
|
240
275
|
</Typography>
|
241
|
-
<Box sx={{ width: '
|
242
|
-
<Box sx={{ flex: 1, height: '
|
276
|
+
<Box sx={{ width: '18px' }} />
|
277
|
+
<Box sx={{ flex: 1, height: '1em' }} />
|
243
278
|
</Box>
|
244
279
|
)
|
245
280
|
}
|
246
281
|
|
247
|
-
const
|
282
|
+
const words = line.split(' ')
|
283
|
+
const lineWords: React.ReactNode[] = []
|
284
|
+
|
285
|
+
words.forEach((word, wordIndex) => {
|
286
|
+
if (word === '') return null
|
287
|
+
if (/^\s+$/.test(word)) {
|
288
|
+
return lineWords.push(<span key={`space-${lineIndex}-${wordIndex}`}> </span>)
|
289
|
+
}
|
290
|
+
|
291
|
+
const wordId = `${currentSource}-word-${wordCount}`
|
292
|
+
wordCount++
|
293
|
+
|
294
|
+
const anchor = currentSource ? anchors?.find(a =>
|
295
|
+
a.reference_word_ids[currentSource]?.includes(wordId)
|
296
|
+
) : undefined
|
297
|
+
|
298
|
+
const hasCorrection = referenceCorrections.has(wordId)
|
299
|
+
|
300
|
+
lineWords.push(
|
301
|
+
<WordComponent
|
302
|
+
key={wordId}
|
303
|
+
word={word}
|
304
|
+
shouldFlash={shouldWordFlash({ word, id: wordId })}
|
305
|
+
isAnchor={Boolean(anchor)}
|
306
|
+
isCorrectedGap={hasCorrection}
|
307
|
+
isUncorrectedGap={false}
|
308
|
+
isCurrentlyPlaying={shouldHighlightWord({ word, id: wordId })}
|
309
|
+
onClick={() => handleWordClick(word, wordId, anchor, undefined)}
|
310
|
+
/>
|
311
|
+
)
|
312
|
+
})
|
313
|
+
|
248
314
|
return (
|
249
|
-
<Box key={`line-${lineIndex}`} sx={{
|
315
|
+
<Box key={`line-${lineIndex}`} sx={{
|
316
|
+
display: 'flex',
|
317
|
+
alignItems: 'flex-start',
|
318
|
+
mb: 0,
|
319
|
+
lineHeight: 1
|
320
|
+
}}>
|
250
321
|
<Typography
|
251
322
|
component="span"
|
252
323
|
sx={{
|
@@ -257,7 +328,9 @@ export function HighlightedText({
|
|
257
328
|
marginRight: 1,
|
258
329
|
userSelect: 'none',
|
259
330
|
fontFamily: 'monospace',
|
260
|
-
paddingTop: '
|
331
|
+
paddingTop: '1px',
|
332
|
+
fontSize: '0.8rem',
|
333
|
+
lineHeight: 1
|
261
334
|
}}
|
262
335
|
>
|
263
336
|
{currentLinePosition?.lineNumber ?? lineIndex}
|
@@ -266,43 +339,18 @@ export function HighlightedText({
|
|
266
339
|
size="small"
|
267
340
|
onClick={() => handleCopyLine(line)}
|
268
341
|
sx={{
|
269
|
-
padding: '
|
270
|
-
marginRight:
|
271
|
-
height: '
|
272
|
-
width: '
|
342
|
+
padding: '1px',
|
343
|
+
marginRight: 0.5,
|
344
|
+
height: '18px',
|
345
|
+
width: '18px',
|
346
|
+
minHeight: '18px',
|
347
|
+
minWidth: '18px'
|
273
348
|
}}
|
274
349
|
>
|
275
|
-
<ContentCopyIcon sx={{ fontSize: '
|
350
|
+
<ContentCopyIcon sx={{ fontSize: '0.9rem' }} />
|
276
351
|
</IconButton>
|
277
352
|
<Box sx={{ flex: 1 }}>
|
278
|
-
{
|
279
|
-
if (word === '') return null
|
280
|
-
if (/^\s+$/.test(word)) {
|
281
|
-
return <span key={`space-${lineIndex}-${wordIndex}`}> </span>
|
282
|
-
}
|
283
|
-
|
284
|
-
const wordId = `${currentSource}-word-${wordCount}`
|
285
|
-
wordCount++
|
286
|
-
|
287
|
-
const anchor = currentSource ? anchors?.find(a =>
|
288
|
-
a.reference_word_ids[currentSource]?.includes(wordId)
|
289
|
-
) : undefined
|
290
|
-
|
291
|
-
const hasCorrection = referenceCorrections.has(wordId)
|
292
|
-
|
293
|
-
return (
|
294
|
-
<WordComponent
|
295
|
-
key={wordId}
|
296
|
-
word={word}
|
297
|
-
shouldFlash={shouldWordFlash({ word, id: wordId })}
|
298
|
-
isAnchor={Boolean(anchor)}
|
299
|
-
isCorrectedGap={hasCorrection}
|
300
|
-
isUncorrectedGap={false}
|
301
|
-
isCurrentlyPlaying={shouldHighlightWord({ word, id: wordId })}
|
302
|
-
onClick={() => handleWordClick(word, wordId, anchor, undefined)}
|
303
|
-
/>
|
304
|
-
)
|
305
|
-
})}
|
353
|
+
{lineWords}
|
306
354
|
</Box>
|
307
355
|
</Box>
|
308
356
|
)
|
@@ -8,14 +8,21 @@ export interface SourceSelectorProps {
|
|
8
8
|
|
9
9
|
export function SourceSelector({ currentSource, onSourceChange, availableSources }: SourceSelectorProps) {
|
10
10
|
return (
|
11
|
-
<Box>
|
11
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.3 }}>
|
12
12
|
{availableSources.map((source) => (
|
13
13
|
<Button
|
14
14
|
key={source}
|
15
15
|
size="small"
|
16
16
|
variant={currentSource === source ? 'contained' : 'outlined'}
|
17
17
|
onClick={() => onSourceChange(source)}
|
18
|
-
sx={{
|
18
|
+
sx={{
|
19
|
+
mr: 0,
|
20
|
+
py: 0.2,
|
21
|
+
px: 0.8,
|
22
|
+
minWidth: 'auto',
|
23
|
+
fontSize: '0.7rem',
|
24
|
+
lineHeight: 1.2
|
25
|
+
}}
|
19
26
|
>
|
20
27
|
{/* Capitalize first letter of source */}
|
21
28
|
{source.charAt(0).toUpperCase() + source.slice(1)}
|
@@ -2,6 +2,7 @@ import React from 'react'
|
|
2
2
|
import { COLORS } from '../constants'
|
3
3
|
import { HighlightedWord } from '../styles'
|
4
4
|
import { WordProps } from '../types'
|
5
|
+
import { Tooltip } from '@mui/material'
|
5
6
|
|
6
7
|
export const WordComponent = React.memo(function Word({
|
7
8
|
word,
|
@@ -10,8 +11,9 @@ export const WordComponent = React.memo(function Word({
|
|
10
11
|
isCorrectedGap,
|
11
12
|
isUncorrectedGap,
|
12
13
|
isCurrentlyPlaying,
|
13
|
-
padding = '
|
14
|
+
padding = '1px 3px',
|
14
15
|
onClick,
|
16
|
+
correction
|
15
17
|
}: WordProps) {
|
16
18
|
if (/^\s+$/.test(word)) {
|
17
19
|
return word
|
@@ -29,15 +31,20 @@ export const WordComponent = React.memo(function Word({
|
|
29
31
|
? COLORS.uncorrectedGap
|
30
32
|
: 'transparent'
|
31
33
|
|
32
|
-
|
34
|
+
const wordElement = (
|
33
35
|
<HighlightedWord
|
34
36
|
shouldFlash={shouldFlash}
|
35
37
|
style={{
|
36
38
|
backgroundColor,
|
37
39
|
padding,
|
38
40
|
cursor: 'pointer',
|
39
|
-
borderRadius: '
|
41
|
+
borderRadius: '2px',
|
40
42
|
color: isCurrentlyPlaying ? '#ffffff' : 'inherit',
|
43
|
+
textDecoration: correction ? 'underline dotted' : 'none',
|
44
|
+
textDecorationColor: correction ? '#666' : 'inherit',
|
45
|
+
textUnderlineOffset: '2px',
|
46
|
+
fontSize: '0.85rem',
|
47
|
+
lineHeight: 1.2
|
41
48
|
}}
|
42
49
|
sx={{
|
43
50
|
'&:hover': {
|
@@ -49,4 +56,22 @@ export const WordComponent = React.memo(function Word({
|
|
49
56
|
{word}
|
50
57
|
</HighlightedWord>
|
51
58
|
)
|
59
|
+
|
60
|
+
if (correction) {
|
61
|
+
const tooltipContent = (
|
62
|
+
<>
|
63
|
+
<strong>Original:</strong> "{correction.originalWord}"<br />
|
64
|
+
<strong>Corrected by:</strong> {correction.handler}<br />
|
65
|
+
<strong>Confidence:</strong> {(correction.confidence * 100).toFixed(0)}%
|
66
|
+
</>
|
67
|
+
)
|
68
|
+
|
69
|
+
return (
|
70
|
+
<Tooltip title={tooltipContent} arrow placement="top">
|
71
|
+
{wordElement}
|
72
|
+
</Tooltip>
|
73
|
+
)
|
74
|
+
}
|
75
|
+
|
76
|
+
return wordElement
|
52
77
|
})
|