lyrics-transcriber 0.40.0__py3-none-any.whl → 0.42.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/cli/cli_main.py +7 -0
- lyrics_transcriber/core/config.py +1 -0
- lyrics_transcriber/core/controller.py +30 -52
- lyrics_transcriber/correction/anchor_sequence.py +325 -150
- lyrics_transcriber/correction/corrector.py +224 -107
- lyrics_transcriber/correction/handlers/base.py +28 -10
- lyrics_transcriber/correction/handlers/extend_anchor.py +47 -24
- lyrics_transcriber/correction/handlers/levenshtein.py +75 -33
- lyrics_transcriber/correction/handlers/llm.py +290 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +81 -36
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +46 -26
- lyrics_transcriber/correction/handlers/repeat.py +28 -11
- lyrics_transcriber/correction/handlers/sound_alike.py +68 -32
- lyrics_transcriber/correction/handlers/syllables_match.py +80 -30
- lyrics_transcriber/correction/handlers/word_count_match.py +36 -19
- lyrics_transcriber/correction/handlers/word_operations.py +68 -22
- lyrics_transcriber/correction/text_utils.py +3 -7
- lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
- lyrics_transcriber/frontend/.yarn/releases/yarn-4.6.0.cjs +934 -0
- lyrics_transcriber/frontend/.yarnrc.yml +3 -0
- lyrics_transcriber/frontend/dist/assets/{index-DKnNJHRK.js → index-coH8y7gV.js} +16284 -9032
- lyrics_transcriber/frontend/dist/assets/index-coH8y7gV.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/package.json +6 -2
- lyrics_transcriber/frontend/src/App.tsx +18 -2
- lyrics_transcriber/frontend/src/api.ts +103 -6
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +7 -6
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +86 -59
- lyrics_transcriber/frontend/src/components/EditModal.tsx +93 -43
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +2 -2
- lyrics_transcriber/frontend/src/components/Header.tsx +251 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +303 -265
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +117 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +125 -40
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +129 -115
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +59 -78
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +40 -16
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +4 -10
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +137 -68
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +1 -1
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +85 -115
- lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
- lyrics_transcriber/frontend/src/components/shared/types.ts +15 -7
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +35 -0
- lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +7 -7
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +121 -0
- lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
- lyrics_transcriber/frontend/src/types.js +2 -0
- lyrics_transcriber/frontend/src/types.ts +70 -49
- lyrics_transcriber/frontend/src/validation.ts +132 -0
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/yarn.lock +3752 -0
- lyrics_transcriber/lyrics/base_lyrics_provider.py +75 -12
- lyrics_transcriber/lyrics/file_provider.py +6 -5
- lyrics_transcriber/lyrics/genius.py +5 -2
- lyrics_transcriber/lyrics/spotify.py +58 -21
- lyrics_transcriber/output/ass/config.py +16 -5
- lyrics_transcriber/output/cdg.py +8 -8
- lyrics_transcriber/output/generator.py +29 -14
- lyrics_transcriber/output/plain_text.py +15 -10
- lyrics_transcriber/output/segment_resizer.py +16 -3
- lyrics_transcriber/output/subtitles.py +56 -2
- lyrics_transcriber/output/video.py +107 -1
- lyrics_transcriber/review/__init__.py +0 -1
- lyrics_transcriber/review/server.py +337 -164
- lyrics_transcriber/transcribers/audioshake.py +3 -0
- lyrics_transcriber/transcribers/base_transcriber.py +11 -3
- lyrics_transcriber/transcribers/whisper.py +11 -1
- lyrics_transcriber/types.py +151 -105
- lyrics_transcriber/utils/word_utils.py +27 -0
- {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/METADATA +3 -1
- {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/RECORD +76 -63
- {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/WHEEL +1 -1
- lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js.map +0 -1
- lyrics_transcriber/frontend/package-lock.json +0 -4260
- lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +0 -202
- {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/entry_points.txt +0 -0
@@ -35,6 +35,8 @@ interface EditModalProps {
|
|
35
35
|
onPlaySegment?: (startTime: number) => void
|
36
36
|
currentTime?: number
|
37
37
|
onDelete?: (segmentIndex: number) => void
|
38
|
+
onAddSegment?: (segmentIndex: number) => void
|
39
|
+
onSplitSegment?: (segmentIndex: number, afterWordIndex: number) => void
|
38
40
|
}
|
39
41
|
|
40
42
|
export default function EditModal({
|
@@ -47,6 +49,8 @@ export default function EditModal({
|
|
47
49
|
onPlaySegment,
|
48
50
|
currentTime = 0,
|
49
51
|
onDelete,
|
52
|
+
onAddSegment,
|
53
|
+
onSplitSegment,
|
50
54
|
}: EditModalProps) {
|
51
55
|
const [editedSegment, setEditedSegment] = useState<LyricsSegment | null>(segment)
|
52
56
|
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null)
|
@@ -58,22 +62,36 @@ export default function EditModal({
|
|
58
62
|
setEditedSegment(segment)
|
59
63
|
}, [segment])
|
60
64
|
|
65
|
+
// Add a function to get safe time values
|
66
|
+
const getSafeTimeRange = (segment: LyricsSegment | null) => {
|
67
|
+
if (!segment) return { start: 0, end: 1 }; // Default 1-second range
|
68
|
+
|
69
|
+
const start = segment.start_time ?? 0;
|
70
|
+
const end = segment.end_time ?? (start + 1);
|
71
|
+
return { start, end };
|
72
|
+
}
|
73
|
+
|
61
74
|
if (!segment || segmentIndex === null || !editedSegment || !originalSegment) return null
|
62
75
|
|
63
|
-
|
76
|
+
// Get safe time values for TimelineEditor
|
77
|
+
const timeRange = getSafeTimeRange(editedSegment)
|
78
|
+
|
79
|
+
const handleWordChange = (index: number, updates: Partial<Word>) => {
|
64
80
|
const newWords = [...editedSegment.words]
|
65
81
|
newWords[index] = {
|
66
82
|
...newWords[index],
|
67
|
-
|
68
|
-
? parseFloat(Number(value).toFixed(4))
|
69
|
-
: value
|
83
|
+
...updates
|
70
84
|
}
|
71
85
|
updateSegment(newWords)
|
72
86
|
}
|
73
87
|
|
74
88
|
const updateSegment = (newWords: Word[]) => {
|
75
|
-
|
76
|
-
const
|
89
|
+
// Filter out null values before finding min/max
|
90
|
+
const validStartTimes = newWords.map(w => w.start_time).filter((t): t is number => t !== null)
|
91
|
+
const validEndTimes = newWords.map(w => w.end_time).filter((t): t is number => t !== null)
|
92
|
+
|
93
|
+
const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null
|
94
|
+
const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null
|
77
95
|
|
78
96
|
setEditedSegment({
|
79
97
|
...editedSegment,
|
@@ -91,11 +109,12 @@ export default function EditModal({
|
|
91
109
|
if (index === undefined) {
|
92
110
|
// Add at end
|
93
111
|
const lastWord = newWords[newWords.length - 1]
|
112
|
+
const lastEndTime = lastWord.end_time ?? 0
|
94
113
|
newWord = {
|
95
114
|
id: nanoid(),
|
96
115
|
text: '',
|
97
|
-
start_time:
|
98
|
-
end_time:
|
116
|
+
start_time: lastEndTime,
|
117
|
+
end_time: lastEndTime + 0.5,
|
99
118
|
confidence: 1.0
|
100
119
|
}
|
101
120
|
newWords.push(newWord)
|
@@ -104,8 +123,11 @@ export default function EditModal({
|
|
104
123
|
const prevWord = newWords[index]
|
105
124
|
const nextWord = newWords[index + 1]
|
106
125
|
const midTime = prevWord ?
|
107
|
-
(nextWord ?
|
108
|
-
|
126
|
+
(nextWord ?
|
127
|
+
((prevWord.end_time ?? 0) + (nextWord.start_time ?? 0)) / 2 :
|
128
|
+
(prevWord.end_time ?? 0) + 0.5
|
129
|
+
) :
|
130
|
+
(nextWord ? (nextWord.start_time ?? 0) - 0.5 : 0)
|
109
131
|
|
110
132
|
newWord = {
|
111
133
|
id: nanoid(),
|
@@ -122,7 +144,9 @@ export default function EditModal({
|
|
122
144
|
|
123
145
|
const handleSplitWord = (index: number) => {
|
124
146
|
const word = editedSegment.words[index]
|
125
|
-
const
|
147
|
+
const startTime = word.start_time ?? 0
|
148
|
+
const endTime = word.end_time ?? startTime + 0.5
|
149
|
+
const midTime = (startTime + endTime) / 2
|
126
150
|
const words = word.text.split(/\s+/)
|
127
151
|
|
128
152
|
if (words.length <= 1) {
|
@@ -138,7 +162,7 @@ export default function EditModal({
|
|
138
162
|
{
|
139
163
|
id: nanoid(),
|
140
164
|
text: words[0],
|
141
|
-
start_time:
|
165
|
+
start_time: startTime,
|
142
166
|
end_time: midTime,
|
143
167
|
confidence: 1.0
|
144
168
|
},
|
@@ -146,7 +170,7 @@ export default function EditModal({
|
|
146
170
|
id: nanoid(),
|
147
171
|
text: words[1],
|
148
172
|
start_time: midTime,
|
149
|
-
end_time:
|
173
|
+
end_time: endTime,
|
150
174
|
confidence: 1.0
|
151
175
|
}
|
152
176
|
)
|
@@ -164,8 +188,8 @@ export default function EditModal({
|
|
164
188
|
newWords.splice(index, 2, {
|
165
189
|
id: nanoid(),
|
166
190
|
text: `${word1.text} ${word2.text}`.trim(),
|
167
|
-
start_time: word1.start_time,
|
168
|
-
end_time: word2.end_time,
|
191
|
+
start_time: word1.start_time ?? null,
|
192
|
+
end_time: word2.end_time ?? null,
|
169
193
|
confidence: 1.0
|
170
194
|
})
|
171
195
|
|
@@ -198,7 +222,7 @@ export default function EditModal({
|
|
198
222
|
originalText: segment?.text,
|
199
223
|
editedText: editedSegment.text,
|
200
224
|
wordCount: editedSegment.words.length,
|
201
|
-
timeRange: `${editedSegment.start_time
|
225
|
+
timeRange: `${editedSegment.start_time?.toFixed(4) ?? 'N/A'} - ${editedSegment.end_time?.toFixed(4) ?? 'N/A'}`
|
202
226
|
})
|
203
227
|
onSave(editedSegment)
|
204
228
|
onClose()
|
@@ -209,7 +233,9 @@ export default function EditModal({
|
|
209
233
|
if (!editedSegment) return
|
210
234
|
|
211
235
|
const newWords = replacementText.trim().split(/\s+/)
|
212
|
-
const
|
236
|
+
const startTime = editedSegment.start_time ?? 0
|
237
|
+
const endTime = editedSegment.end_time ?? (startTime + newWords.length) // Default to 1 second per word
|
238
|
+
const segmentDuration = endTime - startTime
|
213
239
|
|
214
240
|
let updatedWords: Word[]
|
215
241
|
|
@@ -228,8 +254,8 @@ export default function EditModal({
|
|
228
254
|
updatedWords = newWords.map((text, index) => ({
|
229
255
|
id: nanoid(), // Generate new ID
|
230
256
|
text,
|
231
|
-
start_time:
|
232
|
-
end_time:
|
257
|
+
start_time: startTime + (index * avgWordDuration),
|
258
|
+
end_time: startTime + ((index + 1) * avgWordDuration),
|
233
259
|
confidence: 1.0
|
234
260
|
}))
|
235
261
|
}
|
@@ -252,6 +278,13 @@ export default function EditModal({
|
|
252
278
|
}
|
253
279
|
}
|
254
280
|
|
281
|
+
const handleSplitSegment = (wordIndex: number) => {
|
282
|
+
if (segmentIndex !== null && editedSegment) {
|
283
|
+
handleSave() // Save current changes first
|
284
|
+
onSplitSegment?.(segmentIndex, wordIndex)
|
285
|
+
}
|
286
|
+
}
|
287
|
+
|
255
288
|
return (
|
256
289
|
<Dialog
|
257
290
|
open={open}
|
@@ -263,10 +296,10 @@ export default function EditModal({
|
|
263
296
|
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
264
297
|
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
|
265
298
|
Edit Segment {segmentIndex}
|
266
|
-
{segment?.start_time !==
|
299
|
+
{segment?.start_time !== null && onPlaySegment && (
|
267
300
|
<IconButton
|
268
301
|
size="small"
|
269
|
-
onClick={() => onPlaySegment(segment.start_time)}
|
302
|
+
onClick={() => onPlaySegment(segment.start_time!)}
|
270
303
|
sx={{ padding: '4px' }}
|
271
304
|
>
|
272
305
|
<PlayCircleOutlineIcon />
|
@@ -281,22 +314,18 @@ export default function EditModal({
|
|
281
314
|
<Box sx={{ mb: 2 }}>
|
282
315
|
<TimelineEditor
|
283
316
|
words={editedSegment.words}
|
284
|
-
startTime={
|
285
|
-
endTime={
|
286
|
-
onWordUpdate={
|
287
|
-
const newWords = [...editedSegment.words]
|
288
|
-
newWords[index] = { ...newWords[index], ...updates }
|
289
|
-
updateSegment(newWords)
|
290
|
-
}}
|
317
|
+
startTime={timeRange.start}
|
318
|
+
endTime={timeRange.end}
|
319
|
+
onWordUpdate={handleWordChange}
|
291
320
|
currentTime={currentTime}
|
292
321
|
onPlaySegment={onPlaySegment}
|
293
322
|
/>
|
294
323
|
</Box>
|
295
324
|
|
296
325
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
297
|
-
Original Time Range: {originalSegment.start_time
|
326
|
+
Original Time Range: {originalSegment.start_time?.toFixed(2) ?? 'N/A'} - {originalSegment.end_time?.toFixed(2) ?? 'N/A'}
|
298
327
|
<br />
|
299
|
-
Current Time Range: {editedSegment.start_time
|
328
|
+
Current Time Range: {editedSegment.start_time?.toFixed(2) ?? 'N/A'} - {editedSegment.end_time?.toFixed(2) ?? 'N/A'}
|
300
329
|
</Typography>
|
301
330
|
|
302
331
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 3 }}>
|
@@ -305,14 +334,14 @@ export default function EditModal({
|
|
305
334
|
<TextField
|
306
335
|
label={`Word ${index}`}
|
307
336
|
value={word.text}
|
308
|
-
onChange={(e) => handleWordChange(index,
|
337
|
+
onChange={(e) => handleWordChange(index, { text: e.target.value })}
|
309
338
|
fullWidth
|
310
339
|
size="small"
|
311
340
|
/>
|
312
341
|
<TextField
|
313
342
|
label="Start Time"
|
314
|
-
value={word.start_time
|
315
|
-
onChange={(e) => handleWordChange(index,
|
343
|
+
value={word.start_time?.toFixed(2) ?? ''}
|
344
|
+
onChange={(e) => handleWordChange(index, { start_time: parseFloat(e.target.value) })}
|
316
345
|
type="number"
|
317
346
|
inputProps={{ step: 0.01 }}
|
318
347
|
sx={{ width: '150px' }}
|
@@ -320,13 +349,20 @@ export default function EditModal({
|
|
320
349
|
/>
|
321
350
|
<TextField
|
322
351
|
label="End Time"
|
323
|
-
value={word.end_time
|
324
|
-
onChange={(e) => handleWordChange(index,
|
352
|
+
value={word.end_time?.toFixed(2) ?? ''}
|
353
|
+
onChange={(e) => handleWordChange(index, { end_time: parseFloat(e.target.value) })}
|
325
354
|
type="number"
|
326
355
|
inputProps={{ step: 0.01 }}
|
327
356
|
sx={{ width: '150px' }}
|
328
357
|
size="small"
|
329
358
|
/>
|
359
|
+
<IconButton
|
360
|
+
onClick={() => handleRemoveWord(index)}
|
361
|
+
disabled={editedSegment.words.length <= 1}
|
362
|
+
sx={{ color: 'error.main' }}
|
363
|
+
>
|
364
|
+
<DeleteIcon fontSize="small" />
|
365
|
+
</IconButton>
|
330
366
|
<IconButton onClick={(e) => handleWordMenu(e, index)}>
|
331
367
|
<MoreVertIcon />
|
332
368
|
</IconButton>
|
@@ -361,14 +397,22 @@ export default function EditModal({
|
|
361
397
|
>
|
362
398
|
Reset
|
363
399
|
</Button>
|
364
|
-
<
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
400
|
+
<Box sx={{ mr: 'auto', display: 'flex', gap: 1 }}>
|
401
|
+
<Button
|
402
|
+
startIcon={<AddIcon />}
|
403
|
+
onClick={() => segmentIndex !== null && onAddSegment?.(segmentIndex)}
|
404
|
+
color="primary"
|
405
|
+
>
|
406
|
+
Add Segment Before
|
407
|
+
</Button>
|
408
|
+
<Button
|
409
|
+
startIcon={<DeleteIcon />}
|
410
|
+
onClick={handleDelete}
|
411
|
+
color="error"
|
412
|
+
>
|
413
|
+
Delete Segment
|
414
|
+
</Button>
|
415
|
+
</Box>
|
372
416
|
<Button onClick={onClose}>Cancel</Button>
|
373
417
|
<Button onClick={handleSave} variant="contained">
|
374
418
|
Save Changes
|
@@ -392,6 +436,12 @@ export default function EditModal({
|
|
392
436
|
}}>
|
393
437
|
<SplitIcon sx={{ mr: 1 }} /> Split Word
|
394
438
|
</MenuItem>
|
439
|
+
<MenuItem onClick={() => {
|
440
|
+
handleSplitSegment(selectedWordIndex!)
|
441
|
+
handleMenuClose()
|
442
|
+
}}>
|
443
|
+
<SplitIcon sx={{ mr: 1 }} /> Split Segment After Word
|
444
|
+
</MenuItem>
|
395
445
|
<MenuItem
|
396
446
|
onClick={() => {
|
397
447
|
handleMergeWords(selectedWordIndex!)
|
@@ -1,10 +1,10 @@
|
|
1
1
|
import { ChangeEvent, DragEvent, useState } from 'react'
|
2
2
|
import { Paper, Typography } from '@mui/material'
|
3
3
|
import CloudUploadIcon from '@mui/icons-material/CloudUpload'
|
4
|
-
import {
|
4
|
+
import { CorrectionData } from '../types'
|
5
5
|
|
6
6
|
interface FileUploadProps {
|
7
|
-
onUpload: (data:
|
7
|
+
onUpload: (data: CorrectionData) => void
|
8
8
|
}
|
9
9
|
|
10
10
|
export default function FileUpload({ onUpload }: FileUploadProps) {
|
@@ -0,0 +1,251 @@
|
|
1
|
+
import { Box, Button, Typography, useMediaQuery, useTheme, Switch, FormControlLabel, Tooltip, CircularProgress } from '@mui/material'
|
2
|
+
import LockIcon from '@mui/icons-material/Lock'
|
3
|
+
import UploadFileIcon from '@mui/icons-material/UploadFile'
|
4
|
+
import { CorrectionData } from '../types'
|
5
|
+
import CorrectionMetrics from './CorrectionMetrics'
|
6
|
+
import ModeSelector from './ModeSelector'
|
7
|
+
import AudioPlayer from './AudioPlayer'
|
8
|
+
import { InteractionMode } from '../types'
|
9
|
+
import { ApiClient } from '../api'
|
10
|
+
import { findWordById } from './shared/utils/wordUtils'
|
11
|
+
|
12
|
+
interface HeaderProps {
|
13
|
+
isReadOnly: boolean
|
14
|
+
onFileLoad: () => void
|
15
|
+
data: CorrectionData
|
16
|
+
onMetricClick: {
|
17
|
+
anchor: () => void
|
18
|
+
corrected: () => void
|
19
|
+
uncorrected: () => void
|
20
|
+
}
|
21
|
+
effectiveMode: InteractionMode
|
22
|
+
onModeChange: (mode: InteractionMode) => void
|
23
|
+
apiClient: ApiClient | null
|
24
|
+
audioHash: string
|
25
|
+
onTimeUpdate: (time: number) => void
|
26
|
+
onHandlerToggle: (handler: string, enabled: boolean) => void
|
27
|
+
isUpdatingHandlers: boolean
|
28
|
+
onHandlerClick?: (handler: string) => void
|
29
|
+
}
|
30
|
+
|
31
|
+
export default function Header({
|
32
|
+
isReadOnly,
|
33
|
+
onFileLoad,
|
34
|
+
data,
|
35
|
+
onMetricClick,
|
36
|
+
effectiveMode,
|
37
|
+
onModeChange,
|
38
|
+
apiClient,
|
39
|
+
audioHash,
|
40
|
+
onTimeUpdate,
|
41
|
+
onHandlerToggle,
|
42
|
+
isUpdatingHandlers,
|
43
|
+
onHandlerClick
|
44
|
+
}: HeaderProps) {
|
45
|
+
const theme = useTheme()
|
46
|
+
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
47
|
+
|
48
|
+
// Get handlers with their correction counts
|
49
|
+
const handlerCounts = data.corrections?.reduce((counts: Record<string, number>, correction) => {
|
50
|
+
counts[correction.handler] = (counts[correction.handler] || 0) + 1
|
51
|
+
return counts
|
52
|
+
}, {}) || {}
|
53
|
+
|
54
|
+
// Get available handlers from metadata
|
55
|
+
const availableHandlers = data.metadata.available_handlers || []
|
56
|
+
const enabledHandlers = new Set(data.metadata.enabled_handlers || [])
|
57
|
+
|
58
|
+
// Create a map of gap IDs to their corrections
|
59
|
+
const gapCorrections = data.corrections.reduce((map: Record<string, number>, correction) => {
|
60
|
+
// Find the gap that contains this correction's word_id
|
61
|
+
const gap = data.gap_sequences.find(g =>
|
62
|
+
g.transcribed_word_ids.includes(correction.word_id)
|
63
|
+
)
|
64
|
+
if (gap) {
|
65
|
+
map[gap.id] = (map[gap.id] || 0) + 1
|
66
|
+
}
|
67
|
+
return map
|
68
|
+
}, {})
|
69
|
+
|
70
|
+
// Calculate metrics
|
71
|
+
const correctedGapCount = Object.keys(gapCorrections).length
|
72
|
+
const uncorrectedGapCount = data.gap_sequences.length - correctedGapCount
|
73
|
+
|
74
|
+
const uncorrectedGaps = data.gap_sequences
|
75
|
+
.filter(gap => !gapCorrections[gap.id] && gap.transcribed_word_ids.length > 0)
|
76
|
+
.map(gap => {
|
77
|
+
const firstWord = findWordById(data.corrected_segments, gap.transcribed_word_ids[0])
|
78
|
+
return {
|
79
|
+
position: firstWord?.id ?? '',
|
80
|
+
length: gap.transcribed_word_ids.length
|
81
|
+
}
|
82
|
+
})
|
83
|
+
|
84
|
+
// Calculate correction type counts
|
85
|
+
const replacedCount = data.corrections.filter(c => !c.is_deletion && !c.split_total).length
|
86
|
+
const addedCount = data.corrections.filter(c => c.split_total).length
|
87
|
+
const deletedCount = data.corrections.filter(c => c.is_deletion).length
|
88
|
+
|
89
|
+
console.log('Header: Render with isUpdatingHandlers =', isUpdatingHandlers)
|
90
|
+
|
91
|
+
return (
|
92
|
+
<>
|
93
|
+
{isReadOnly && (
|
94
|
+
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2, color: 'text.secondary' }}>
|
95
|
+
<LockIcon sx={{ mr: 1 }} />
|
96
|
+
<Typography variant="body2">
|
97
|
+
View Only Mode
|
98
|
+
</Typography>
|
99
|
+
</Box>
|
100
|
+
)}
|
101
|
+
|
102
|
+
<Box sx={{
|
103
|
+
display: 'flex',
|
104
|
+
flexDirection: isMobile ? 'column' : 'row',
|
105
|
+
gap: 2,
|
106
|
+
justifyContent: 'space-between',
|
107
|
+
alignItems: isMobile ? 'stretch' : 'center',
|
108
|
+
mb: 3
|
109
|
+
}}>
|
110
|
+
<Typography variant="h4" sx={{ fontSize: isMobile ? '1.75rem' : '2.125rem' }}>
|
111
|
+
Lyrics Correction Review
|
112
|
+
</Typography>
|
113
|
+
{isReadOnly && (
|
114
|
+
<Button
|
115
|
+
variant="outlined"
|
116
|
+
startIcon={<UploadFileIcon />}
|
117
|
+
onClick={onFileLoad}
|
118
|
+
fullWidth={isMobile}
|
119
|
+
>
|
120
|
+
Load File
|
121
|
+
</Button>
|
122
|
+
)}
|
123
|
+
</Box>
|
124
|
+
|
125
|
+
<Box sx={{
|
126
|
+
display: 'flex',
|
127
|
+
gap: 2,
|
128
|
+
mb: 3,
|
129
|
+
flexDirection: isMobile ? 'column' : 'row'
|
130
|
+
}}>
|
131
|
+
<Box sx={{
|
132
|
+
display: 'flex',
|
133
|
+
flexDirection: 'column',
|
134
|
+
gap: 1,
|
135
|
+
minWidth: '250px',
|
136
|
+
position: 'relative'
|
137
|
+
}}>
|
138
|
+
<Typography variant="subtitle2" color="text.secondary">
|
139
|
+
Correction Handlers
|
140
|
+
</Typography>
|
141
|
+
|
142
|
+
{availableHandlers.map(handler => (
|
143
|
+
<Tooltip
|
144
|
+
key={handler.id}
|
145
|
+
title={handler.description}
|
146
|
+
placement="right"
|
147
|
+
>
|
148
|
+
<FormControlLabel
|
149
|
+
control={
|
150
|
+
<Switch
|
151
|
+
checked={enabledHandlers.has(handler.id)}
|
152
|
+
onChange={(e) => onHandlerToggle(handler.id, e.target.checked)}
|
153
|
+
size="small"
|
154
|
+
disabled={isUpdatingHandlers}
|
155
|
+
/>
|
156
|
+
}
|
157
|
+
label={`${handler.name} (${handlerCounts[handler.id] || 0})`}
|
158
|
+
onClick={(e) => {
|
159
|
+
if ((e.target as HTMLElement).tagName !== 'INPUT') {
|
160
|
+
e.preventDefault();
|
161
|
+
e.stopPropagation();
|
162
|
+
onHandlerClick?.(handler.id);
|
163
|
+
}
|
164
|
+
}}
|
165
|
+
sx={{
|
166
|
+
ml: 0,
|
167
|
+
'& .MuiFormControlLabel-label': {
|
168
|
+
fontSize: '0.875rem',
|
169
|
+
cursor: 'pointer'
|
170
|
+
}
|
171
|
+
}}
|
172
|
+
/>
|
173
|
+
</Tooltip>
|
174
|
+
))}
|
175
|
+
|
176
|
+
{isUpdatingHandlers && (
|
177
|
+
<Box sx={{
|
178
|
+
position: 'absolute',
|
179
|
+
top: 0,
|
180
|
+
left: 0,
|
181
|
+
right: 0,
|
182
|
+
bottom: 0,
|
183
|
+
display: 'flex',
|
184
|
+
alignItems: 'center',
|
185
|
+
justifyContent: 'center',
|
186
|
+
backgroundColor: 'rgba(255, 255, 255, 0.7)',
|
187
|
+
borderRadius: 1,
|
188
|
+
zIndex: 1
|
189
|
+
}}>
|
190
|
+
<Box sx={{
|
191
|
+
display: 'flex',
|
192
|
+
alignItems: 'center',
|
193
|
+
gap: 2,
|
194
|
+
padding: 2,
|
195
|
+
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
196
|
+
borderRadius: 1,
|
197
|
+
boxShadow: 1
|
198
|
+
}}>
|
199
|
+
<CircularProgress size={24} />
|
200
|
+
<Typography variant="body2" color="text.secondary">
|
201
|
+
Updating corrections...
|
202
|
+
</Typography>
|
203
|
+
</Box>
|
204
|
+
</Box>
|
205
|
+
)}
|
206
|
+
</Box>
|
207
|
+
<Box sx={{ flexGrow: 1 }}>
|
208
|
+
<CorrectionMetrics
|
209
|
+
// Anchor metrics
|
210
|
+
anchorCount={data.metadata.anchor_sequences_count}
|
211
|
+
multiSourceAnchors={data.anchor_sequences?.filter(anchor =>
|
212
|
+
anchor?.reference_word_ids &&
|
213
|
+
Object.keys(anchor.reference_word_ids).length > 1
|
214
|
+
).length ?? 0}
|
215
|
+
anchorWordCount={data.anchor_sequences?.reduce((sum, anchor) =>
|
216
|
+
sum + (anchor.transcribed_word_ids?.length || 0), 0) ?? 0}
|
217
|
+
// Updated gap metrics
|
218
|
+
correctedGapCount={correctedGapCount}
|
219
|
+
uncorrectedGapCount={uncorrectedGapCount}
|
220
|
+
uncorrectedGaps={uncorrectedGaps}
|
221
|
+
// Updated correction type counts
|
222
|
+
replacedCount={replacedCount}
|
223
|
+
addedCount={addedCount}
|
224
|
+
deletedCount={deletedCount}
|
225
|
+
onMetricClick={onMetricClick}
|
226
|
+
totalWords={data.metadata.total_words}
|
227
|
+
/>
|
228
|
+
</Box>
|
229
|
+
</Box>
|
230
|
+
|
231
|
+
<Box sx={{
|
232
|
+
display: 'flex',
|
233
|
+
flexDirection: isMobile ? 'column' : 'row',
|
234
|
+
gap: 5,
|
235
|
+
alignItems: 'flex-start',
|
236
|
+
justifyContent: 'flex-start',
|
237
|
+
mb: 3
|
238
|
+
}}>
|
239
|
+
<ModeSelector
|
240
|
+
effectiveMode={effectiveMode}
|
241
|
+
onChange={onModeChange}
|
242
|
+
/>
|
243
|
+
<AudioPlayer
|
244
|
+
apiClient={apiClient}
|
245
|
+
onTimeUpdate={onTimeUpdate}
|
246
|
+
audioHash={audioHash}
|
247
|
+
/>
|
248
|
+
</Box>
|
249
|
+
</>
|
250
|
+
)
|
251
|
+
}
|