karaoke-gen 0.75.53__py3-none-any.whl → 0.81.1__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.
- karaoke_gen/audio_fetcher.py +218 -0
- karaoke_gen/instrumental_review/static/index.html +179 -16
- karaoke_gen/karaoke_gen.py +191 -25
- karaoke_gen/lyrics_processor.py +39 -31
- karaoke_gen/utils/__init__.py +26 -0
- karaoke_gen/utils/cli_args.py +9 -1
- karaoke_gen/utils/gen_cli.py +1 -1
- karaoke_gen/utils/remote_cli.py +33 -6
- {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.81.1.dist-info}/METADATA +80 -4
- {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.81.1.dist-info}/RECORD +50 -43
- lyrics_transcriber/core/config.py +8 -0
- lyrics_transcriber/core/controller.py +43 -1
- lyrics_transcriber/correction/agentic/providers/config.py +6 -0
- lyrics_transcriber/correction/agentic/providers/model_factory.py +24 -1
- lyrics_transcriber/correction/agentic/router.py +17 -13
- lyrics_transcriber/frontend/.gitignore +1 -0
- lyrics_transcriber/frontend/e2e/agentic-corrections.spec.ts +207 -0
- lyrics_transcriber/frontend/e2e/fixtures/agentic-correction-data.json +226 -0
- lyrics_transcriber/frontend/index.html +5 -1
- lyrics_transcriber/frontend/package-lock.json +4553 -0
- lyrics_transcriber/frontend/package.json +7 -1
- lyrics_transcriber/frontend/playwright.config.ts +69 -0
- lyrics_transcriber/frontend/public/nomad-karaoke-logo.svg +5 -0
- lyrics_transcriber/frontend/src/App.tsx +88 -59
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +55 -21
- lyrics_transcriber/frontend/src/components/AppHeader.tsx +65 -0
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +39 -35
- lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +9 -9
- lyrics_transcriber/frontend/src/components/EditModal.tsx +1 -1
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +1 -1
- lyrics_transcriber/frontend/src/components/Header.tsx +96 -3
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +120 -3
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +22 -21
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +12 -2
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +3 -3
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +122 -35
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +2 -2
- lyrics_transcriber/frontend/src/components/shared/constants.ts +15 -5
- lyrics_transcriber/frontend/src/components/shared/types.ts +6 -0
- lyrics_transcriber/frontend/src/main.tsx +1 -7
- lyrics_transcriber/frontend/src/theme.ts +337 -135
- lyrics_transcriber/frontend/vite.config.ts +5 -0
- lyrics_transcriber/frontend/yarn.lock +1005 -1046
- lyrics_transcriber/output/generator.py +50 -3
- lyrics_transcriber/review/server.py +1 -1
- lyrics_transcriber/transcribers/local_whisper.py +260 -0
- {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.81.1.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.81.1.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.81.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -30,7 +30,7 @@ const SegmentTimeline = styled(Box)({
|
|
|
30
30
|
const TimelineRuler = styled(Box)({
|
|
31
31
|
position: 'relative',
|
|
32
32
|
height: '20px',
|
|
33
|
-
borderBottom: '1px solid #
|
|
33
|
+
borderBottom: '1px solid #2a2a2a', // slate-700 for dark mode
|
|
34
34
|
marginBottom: '4px'
|
|
35
35
|
})
|
|
36
36
|
|
|
@@ -38,14 +38,14 @@ const TimelineMark = styled(Box)({
|
|
|
38
38
|
position: 'absolute',
|
|
39
39
|
width: '1px',
|
|
40
40
|
height: '8px',
|
|
41
|
-
backgroundColor: '#
|
|
41
|
+
backgroundColor: '#666666', // slate-500 for dark mode
|
|
42
42
|
bottom: 0
|
|
43
43
|
})
|
|
44
44
|
|
|
45
45
|
const TimelineLabel = styled(Typography)({
|
|
46
46
|
position: 'absolute',
|
|
47
47
|
fontSize: '0.65rem',
|
|
48
|
-
color: '#
|
|
48
|
+
color: '#888888', // slate-400 for dark mode
|
|
49
49
|
bottom: '10px',
|
|
50
50
|
transform: 'translateX(-50%)',
|
|
51
51
|
whiteSpace: 'nowrap'
|
|
@@ -58,7 +58,7 @@ const WordsBar = styled(Box)({
|
|
|
58
58
|
alignItems: 'stretch',
|
|
59
59
|
minWidth: '100%',
|
|
60
60
|
touchAction: 'pan-y', // Better mobile scrolling
|
|
61
|
-
backgroundColor: '#
|
|
61
|
+
backgroundColor: '#0f0f0f', // slate-900 for dark mode
|
|
62
62
|
borderRadius: '4px',
|
|
63
63
|
marginBottom: '8px'
|
|
64
64
|
})
|
|
@@ -84,7 +84,7 @@ const WordBar = styled(Box, {
|
|
|
84
84
|
? COLORS.corrected
|
|
85
85
|
: isGap
|
|
86
86
|
? COLORS.uncorrectedGap
|
|
87
|
-
: '#
|
|
87
|
+
: '#2a2a2a', // slate-700 for dark mode default
|
|
88
88
|
border: isLong ? '2px solid #f44336' : 'none',
|
|
89
89
|
boxShadow: isLong ? '0 0 4px rgba(244, 67, 54, 0.5)' : 'none',
|
|
90
90
|
'&:hover': {
|
|
@@ -101,13 +101,13 @@ const WordBar = styled(Box, {
|
|
|
101
101
|
|
|
102
102
|
const OriginalWordLabel = styled(Typography)({
|
|
103
103
|
fontSize: '0.65rem',
|
|
104
|
-
color: '#
|
|
104
|
+
color: '#888888', // slate-400 for dark mode
|
|
105
105
|
lineHeight: 1.1,
|
|
106
106
|
marginBottom: '3px',
|
|
107
107
|
textDecoration: 'line-through',
|
|
108
108
|
opacity: 0.85,
|
|
109
109
|
fontWeight: 500,
|
|
110
|
-
backgroundColor: 'rgba(
|
|
110
|
+
backgroundColor: 'rgba(15, 23, 42, 0.8)', // slate-900 with opacity for dark mode
|
|
111
111
|
padding: '1px 3px',
|
|
112
112
|
borderRadius: '2px'
|
|
113
113
|
})
|
|
@@ -226,7 +226,7 @@ export default function DurationTimelineView({
|
|
|
226
226
|
whiteSpace: 'nowrap',
|
|
227
227
|
width: '100%',
|
|
228
228
|
textAlign: 'center',
|
|
229
|
-
color: correction ? '#
|
|
229
|
+
color: correction ? '#4ade80' : '#e5e5e5' // green-400 or slate-50 for dark mode
|
|
230
230
|
}}
|
|
231
231
|
>
|
|
232
232
|
{word.text}
|
|
@@ -235,7 +235,7 @@ export default function DurationTimelineView({
|
|
|
235
235
|
<Typography
|
|
236
236
|
sx={{
|
|
237
237
|
fontSize: '0.6rem',
|
|
238
|
-
color: 'rgba(
|
|
238
|
+
color: 'rgba(248, 250, 252, 0.6)', // slate-50 with opacity for dark mode
|
|
239
239
|
lineHeight: 1,
|
|
240
240
|
marginTop: '3px',
|
|
241
241
|
fontWeight: 600
|
|
@@ -624,7 +624,7 @@ export default function EditModal({
|
|
|
624
624
|
position: 'absolute',
|
|
625
625
|
top: 0,
|
|
626
626
|
left: 0,
|
|
627
|
-
backgroundColor: 'rgba(
|
|
627
|
+
backgroundColor: 'rgba(30, 41, 59, 0.95)', // slate-800 with opacity for dark mode
|
|
628
628
|
zIndex: 10
|
|
629
629
|
}}>
|
|
630
630
|
<CircularProgress size={60} thickness={4} />
|
|
@@ -312,7 +312,7 @@ export default function EditWordList({
|
|
|
312
312
|
width: '8px',
|
|
313
313
|
},
|
|
314
314
|
'&::-webkit-scrollbar-thumb': {
|
|
315
|
-
backgroundColor: 'rgba(
|
|
315
|
+
backgroundColor: 'rgba(248, 250, 252, 0.2)', // slate-50 for dark mode
|
|
316
316
|
borderRadius: '4px',
|
|
317
317
|
},
|
|
318
318
|
scrollbarWidth: 'thin',
|
|
@@ -8,6 +8,9 @@ import RedoIcon from '@mui/icons-material/Redo'
|
|
|
8
8
|
import TimerIcon from '@mui/icons-material/Timer'
|
|
9
9
|
import RestoreIcon from '@mui/icons-material/Restore'
|
|
10
10
|
import RateReviewIcon from '@mui/icons-material/RateReview'
|
|
11
|
+
import VisibilityIcon from '@mui/icons-material/Visibility'
|
|
12
|
+
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
|
13
|
+
import HighlightOffIcon from '@mui/icons-material/HighlightOff'
|
|
11
14
|
import { CorrectionData, InteractionMode } from '../types'
|
|
12
15
|
import CorrectionMetrics from './CorrectionMetrics'
|
|
13
16
|
import AgenticCorrectionMetrics from './AgenticCorrectionMetrics'
|
|
@@ -44,6 +47,13 @@ interface HeaderProps {
|
|
|
44
47
|
canRedo: boolean
|
|
45
48
|
annotationsEnabled?: boolean
|
|
46
49
|
onAnnotationsToggle?: (enabled: boolean) => void
|
|
50
|
+
// Review mode props
|
|
51
|
+
reviewMode?: boolean
|
|
52
|
+
onReviewModeToggle?: (enabled: boolean) => void
|
|
53
|
+
// Batch action props
|
|
54
|
+
onAcceptAllCorrections?: () => void
|
|
55
|
+
onAcceptHighConfidenceCorrections?: () => void
|
|
56
|
+
onRevertAllCorrections?: () => void
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
export default function Header({
|
|
@@ -70,6 +80,11 @@ export default function Header({
|
|
|
70
80
|
canRedo,
|
|
71
81
|
annotationsEnabled = true,
|
|
72
82
|
onAnnotationsToggle,
|
|
83
|
+
reviewMode = false,
|
|
84
|
+
onReviewModeToggle,
|
|
85
|
+
onAcceptAllCorrections,
|
|
86
|
+
onAcceptHighConfidenceCorrections,
|
|
87
|
+
onRevertAllCorrections,
|
|
73
88
|
}: HeaderProps) {
|
|
74
89
|
const theme = useTheme()
|
|
75
90
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
|
@@ -141,9 +156,28 @@ export default function Header({
|
|
|
141
156
|
Nomad Karaoke: Lyrics Transcription Review
|
|
142
157
|
</Typography>
|
|
143
158
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
159
|
+
{!isReadOnly && isAgenticMode && onReviewModeToggle && (
|
|
160
|
+
<Tooltip title={reviewMode
|
|
161
|
+
? "Hide inline correction actions"
|
|
162
|
+
: "Show inline actions on all corrections for quick review"
|
|
163
|
+
}>
|
|
164
|
+
<Chip
|
|
165
|
+
icon={<VisibilityIcon />}
|
|
166
|
+
label={reviewMode ? "Review Mode" : "Review Off"}
|
|
167
|
+
onClick={() => onReviewModeToggle(!reviewMode)}
|
|
168
|
+
color={reviewMode ? "secondary" : "default"}
|
|
169
|
+
variant={reviewMode ? "filled" : "outlined"}
|
|
170
|
+
size="small"
|
|
171
|
+
sx={{
|
|
172
|
+
cursor: 'pointer',
|
|
173
|
+
'& .MuiChip-icon': { fontSize: '1rem' }
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
</Tooltip>
|
|
177
|
+
)}
|
|
144
178
|
{!isReadOnly && onAnnotationsToggle && (
|
|
145
|
-
<Tooltip title={annotationsEnabled
|
|
146
|
-
? "Click to disable annotation prompts when editing"
|
|
179
|
+
<Tooltip title={annotationsEnabled
|
|
180
|
+
? "Click to disable annotation prompts when editing"
|
|
147
181
|
: "Click to enable annotation prompts when editing"
|
|
148
182
|
}>
|
|
149
183
|
<Chip
|
|
@@ -153,7 +187,7 @@ export default function Header({
|
|
|
153
187
|
color={annotationsEnabled ? "primary" : "default"}
|
|
154
188
|
variant={annotationsEnabled ? "filled" : "outlined"}
|
|
155
189
|
size="small"
|
|
156
|
-
sx={{
|
|
190
|
+
sx={{
|
|
157
191
|
cursor: 'pointer',
|
|
158
192
|
'& .MuiChip-icon': { fontSize: '1rem' }
|
|
159
193
|
}}
|
|
@@ -281,6 +315,65 @@ export default function Header({
|
|
|
281
315
|
</Box>
|
|
282
316
|
</Box>
|
|
283
317
|
|
|
318
|
+
{/* Batch Actions Panel - shown when review mode is enabled */}
|
|
319
|
+
{!isReadOnly && reviewMode && isAgenticMode && data.corrections?.length > 0 && (
|
|
320
|
+
<Paper sx={{ p: 0.8, mb: 1, backgroundColor: 'action.hover' }}>
|
|
321
|
+
<Box sx={{
|
|
322
|
+
display: 'flex',
|
|
323
|
+
flexDirection: isMobile ? 'column' : 'row',
|
|
324
|
+
gap: 1,
|
|
325
|
+
alignItems: isMobile ? 'stretch' : 'center',
|
|
326
|
+
justifyContent: 'space-between'
|
|
327
|
+
}}>
|
|
328
|
+
<Typography variant="subtitle2" sx={{ fontSize: '0.75rem', color: 'text.secondary' }}>
|
|
329
|
+
Batch Actions ({data.corrections.length} corrections)
|
|
330
|
+
</Typography>
|
|
331
|
+
<Box sx={{
|
|
332
|
+
display: 'flex',
|
|
333
|
+
gap: 1,
|
|
334
|
+
flexWrap: 'wrap'
|
|
335
|
+
}}>
|
|
336
|
+
{onAcceptHighConfidenceCorrections && (
|
|
337
|
+
<Button
|
|
338
|
+
variant="contained"
|
|
339
|
+
size="small"
|
|
340
|
+
color="success"
|
|
341
|
+
startIcon={<CheckCircleIcon />}
|
|
342
|
+
onClick={onAcceptHighConfidenceCorrections}
|
|
343
|
+
sx={{ fontSize: '0.75rem', py: 0.5 }}
|
|
344
|
+
>
|
|
345
|
+
Accept High Confidence ({data.corrections.filter(c => c.confidence >= 0.8).length})
|
|
346
|
+
</Button>
|
|
347
|
+
)}
|
|
348
|
+
{onAcceptAllCorrections && (
|
|
349
|
+
<Button
|
|
350
|
+
variant="outlined"
|
|
351
|
+
size="small"
|
|
352
|
+
color="success"
|
|
353
|
+
startIcon={<CheckCircleIcon />}
|
|
354
|
+
onClick={onAcceptAllCorrections}
|
|
355
|
+
sx={{ fontSize: '0.75rem', py: 0.5 }}
|
|
356
|
+
>
|
|
357
|
+
Accept All
|
|
358
|
+
</Button>
|
|
359
|
+
)}
|
|
360
|
+
{onRevertAllCorrections && (
|
|
361
|
+
<Button
|
|
362
|
+
variant="outlined"
|
|
363
|
+
size="small"
|
|
364
|
+
color="error"
|
|
365
|
+
startIcon={<HighlightOffIcon />}
|
|
366
|
+
onClick={onRevertAllCorrections}
|
|
367
|
+
sx={{ fontSize: '0.75rem', py: 0.5 }}
|
|
368
|
+
>
|
|
369
|
+
Revert All
|
|
370
|
+
</Button>
|
|
371
|
+
)}
|
|
372
|
+
</Box>
|
|
373
|
+
</Box>
|
|
374
|
+
</Paper>
|
|
375
|
+
)}
|
|
376
|
+
|
|
284
377
|
<Paper sx={{ p: 0.8, mb: 1 }}>
|
|
285
378
|
<Box sx={{
|
|
286
379
|
display: 'flex',
|
|
@@ -88,6 +88,12 @@ interface MemoizedTranscriptionViewProps {
|
|
|
88
88
|
anchors: AnchorSequence[]
|
|
89
89
|
disableHighlighting: boolean
|
|
90
90
|
onDataChange?: (updatedData: CorrectionData) => void
|
|
91
|
+
// Review mode props
|
|
92
|
+
reviewMode: boolean
|
|
93
|
+
onRevertCorrection: (wordId: string) => void
|
|
94
|
+
onEditCorrection: (wordId: string) => void
|
|
95
|
+
onAcceptCorrection: (wordId: string) => void
|
|
96
|
+
onShowCorrectionDetail: (wordId: string) => void
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
// Create a memoized TranscriptionView component
|
|
@@ -103,7 +109,12 @@ const MemoizedTranscriptionView = memo(function MemoizedTranscriptionView({
|
|
|
103
109
|
currentTime,
|
|
104
110
|
anchors,
|
|
105
111
|
disableHighlighting,
|
|
106
|
-
onDataChange
|
|
112
|
+
onDataChange,
|
|
113
|
+
reviewMode,
|
|
114
|
+
onRevertCorrection,
|
|
115
|
+
onEditCorrection,
|
|
116
|
+
onAcceptCorrection,
|
|
117
|
+
onShowCorrectionDetail
|
|
107
118
|
}: MemoizedTranscriptionViewProps) {
|
|
108
119
|
return (
|
|
109
120
|
<TranscriptionView
|
|
@@ -118,6 +129,11 @@ const MemoizedTranscriptionView = memo(function MemoizedTranscriptionView({
|
|
|
118
129
|
currentTime={disableHighlighting ? undefined : currentTime}
|
|
119
130
|
anchors={anchors}
|
|
120
131
|
onDataChange={onDataChange}
|
|
132
|
+
reviewMode={reviewMode}
|
|
133
|
+
onRevertCorrection={onRevertCorrection}
|
|
134
|
+
onEditCorrection={onEditCorrection}
|
|
135
|
+
onAcceptCorrection={onAcceptCorrection}
|
|
136
|
+
onShowCorrectionDetail={onShowCorrectionDetail}
|
|
121
137
|
/>
|
|
122
138
|
);
|
|
123
139
|
});
|
|
@@ -202,6 +218,13 @@ interface MemoizedHeaderProps {
|
|
|
202
218
|
onUnCorrectAll: () => void
|
|
203
219
|
annotationsEnabled: boolean
|
|
204
220
|
onAnnotationsToggle: (enabled: boolean) => void
|
|
221
|
+
// Review mode props
|
|
222
|
+
reviewMode: boolean
|
|
223
|
+
onReviewModeToggle: (enabled: boolean) => void
|
|
224
|
+
// Batch action props
|
|
225
|
+
onAcceptAllCorrections: () => void
|
|
226
|
+
onAcceptHighConfidenceCorrections: () => void
|
|
227
|
+
onRevertAllCorrections: () => void
|
|
205
228
|
}
|
|
206
229
|
|
|
207
230
|
// Create a memoized Header component
|
|
@@ -228,7 +251,12 @@ const MemoizedHeader = memo(function MemoizedHeader({
|
|
|
228
251
|
canRedo,
|
|
229
252
|
onUnCorrectAll,
|
|
230
253
|
annotationsEnabled,
|
|
231
|
-
onAnnotationsToggle
|
|
254
|
+
onAnnotationsToggle,
|
|
255
|
+
reviewMode,
|
|
256
|
+
onReviewModeToggle,
|
|
257
|
+
onAcceptAllCorrections,
|
|
258
|
+
onAcceptHighConfidenceCorrections,
|
|
259
|
+
onRevertAllCorrections
|
|
232
260
|
}: MemoizedHeaderProps) {
|
|
233
261
|
return (
|
|
234
262
|
<Header
|
|
@@ -255,6 +283,11 @@ const MemoizedHeader = memo(function MemoizedHeader({
|
|
|
255
283
|
onUnCorrectAll={onUnCorrectAll}
|
|
256
284
|
annotationsEnabled={annotationsEnabled}
|
|
257
285
|
onAnnotationsToggle={onAnnotationsToggle}
|
|
286
|
+
reviewMode={reviewMode}
|
|
287
|
+
onReviewModeToggle={onReviewModeToggle}
|
|
288
|
+
onAcceptAllCorrections={onAcceptAllCorrections}
|
|
289
|
+
onAcceptHighConfidenceCorrections={onAcceptHighConfidenceCorrections}
|
|
290
|
+
onRevertAllCorrections={onRevertAllCorrections}
|
|
258
291
|
/>
|
|
259
292
|
);
|
|
260
293
|
});
|
|
@@ -291,6 +324,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
|
291
324
|
const [isFindReplaceModalOpen, setIsFindReplaceModalOpen] = useState(false)
|
|
292
325
|
const [isTimingOffsetModalOpen, setIsTimingOffsetModalOpen] = useState(false)
|
|
293
326
|
const [timingOffsetMs, setTimingOffsetMs] = useState(0)
|
|
327
|
+
|
|
328
|
+
// Review mode state for agentic corrections
|
|
329
|
+
const [reviewMode, setReviewMode] = useState(false)
|
|
294
330
|
|
|
295
331
|
// Annotation collection state
|
|
296
332
|
const [annotations, setAnnotations] = useState<Omit<CorrectionAnnotation, 'annotation_id' | 'timestamp'>[]>([])
|
|
@@ -793,11 +829,82 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
|
793
829
|
// For now, just log acceptance
|
|
794
830
|
// In the future, this could be tracked in the annotation system
|
|
795
831
|
console.log('Accepted correction for word:', wordId)
|
|
796
|
-
|
|
832
|
+
|
|
797
833
|
// TODO: Track acceptance in annotation system
|
|
798
834
|
// This could be used to build confidence in the AI's corrections over time
|
|
799
835
|
}, [])
|
|
800
836
|
|
|
837
|
+
// Batch action handlers for review mode
|
|
838
|
+
const handleAcceptAllCorrections = useCallback(() => {
|
|
839
|
+
// Accept all corrections - for now just log, could track in annotation system
|
|
840
|
+
data.corrections?.forEach(c => {
|
|
841
|
+
console.log('Batch accepted correction:', c.corrected_word_id || c.word_id)
|
|
842
|
+
})
|
|
843
|
+
console.log(`Accepted all ${data.corrections?.length || 0} corrections`)
|
|
844
|
+
}, [data.corrections])
|
|
845
|
+
|
|
846
|
+
const handleAcceptHighConfidenceCorrections = useCallback(() => {
|
|
847
|
+
// Accept corrections with confidence >= 80%
|
|
848
|
+
const highConfidence = data.corrections?.filter(c => c.confidence >= 0.8) || []
|
|
849
|
+
highConfidence.forEach(c => {
|
|
850
|
+
console.log('Batch accepted high-confidence correction:', c.corrected_word_id || c.word_id)
|
|
851
|
+
})
|
|
852
|
+
console.log(`Accepted ${highConfidence.length} high-confidence corrections`)
|
|
853
|
+
}, [data.corrections])
|
|
854
|
+
|
|
855
|
+
const handleRevertAllCorrections = useCallback(() => {
|
|
856
|
+
if (!window.confirm(`Are you sure you want to revert all ${data.corrections?.length || 0} corrections? This cannot be undone.`)) {
|
|
857
|
+
return
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Revert all corrections by reverting each one
|
|
861
|
+
// Process in reverse order to avoid ID conflicts
|
|
862
|
+
const corrections = [...(data.corrections || [])].reverse()
|
|
863
|
+
|
|
864
|
+
let newData = data
|
|
865
|
+
for (const correction of corrections) {
|
|
866
|
+
const wordId = correction.corrected_word_id || correction.word_id
|
|
867
|
+
|
|
868
|
+
// Find the segment containing the corrected word
|
|
869
|
+
const segmentIndex = newData.corrected_segments.findIndex(segment =>
|
|
870
|
+
segment.words.some(w => w.id === wordId)
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
if (segmentIndex === -1) continue
|
|
874
|
+
|
|
875
|
+
const segment = newData.corrected_segments[segmentIndex]
|
|
876
|
+
|
|
877
|
+
// Replace the corrected word with the original
|
|
878
|
+
const newWords = segment.words.map(word => {
|
|
879
|
+
if (word.id === wordId) {
|
|
880
|
+
return {
|
|
881
|
+
...word,
|
|
882
|
+
text: correction.original_word,
|
|
883
|
+
id: correction.word_id
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return word
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
const newText = newWords.map(w => w.text).join(' ')
|
|
890
|
+
const newSegment = { ...segment, words: newWords, text: newText }
|
|
891
|
+
const newSegments = newData.corrected_segments.map((seg, idx) =>
|
|
892
|
+
idx === segmentIndex ? newSegment : seg
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
newData = {
|
|
896
|
+
...newData,
|
|
897
|
+
corrected_segments: newSegments,
|
|
898
|
+
corrections: newData.corrections?.filter(c =>
|
|
899
|
+
c.corrected_word_id !== wordId && c.word_id !== wordId
|
|
900
|
+
) || []
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
updateDataWithHistory(newData, 'revert all corrections')
|
|
905
|
+
console.log(`Reverted all ${corrections.length} corrections`)
|
|
906
|
+
}, [data, updateDataWithHistory])
|
|
907
|
+
|
|
801
908
|
const handleShowCorrectionDetail = useCallback((wordId: string) => {
|
|
802
909
|
// Find the correction for this word
|
|
803
910
|
const correction = data.corrections?.find(c =>
|
|
@@ -1177,6 +1284,11 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
|
1177
1284
|
onUnCorrectAll={handleUnCorrectAll}
|
|
1178
1285
|
annotationsEnabled={annotationsEnabled}
|
|
1179
1286
|
onAnnotationsToggle={handleAnnotationsToggle}
|
|
1287
|
+
reviewMode={reviewMode}
|
|
1288
|
+
onReviewModeToggle={setReviewMode}
|
|
1289
|
+
onAcceptAllCorrections={handleAcceptAllCorrections}
|
|
1290
|
+
onAcceptHighConfidenceCorrections={handleAcceptHighConfidenceCorrections}
|
|
1291
|
+
onRevertAllCorrections={handleRevertAllCorrections}
|
|
1180
1292
|
/>
|
|
1181
1293
|
|
|
1182
1294
|
<Grid container direction={isMobile ? 'column' : 'row'}>
|
|
@@ -1198,6 +1310,11 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
|
1198
1310
|
// needs to update history
|
|
1199
1311
|
updateDataWithHistory(updatedData, 'direct data change');
|
|
1200
1312
|
}}
|
|
1313
|
+
reviewMode={reviewMode}
|
|
1314
|
+
onRevertCorrection={handleRevertCorrection}
|
|
1315
|
+
onEditCorrection={handleEditCorrection}
|
|
1316
|
+
onAcceptCorrection={handleAcceptCorrection}
|
|
1317
|
+
onShowCorrectionDetail={handleShowCorrectionDetail}
|
|
1201
1318
|
/>
|
|
1202
1319
|
{!isReadOnly && apiClient && (
|
|
1203
1320
|
<Box sx={{
|
|
@@ -33,16 +33,17 @@ const CANVAS_PADDING = 8
|
|
|
33
33
|
const TEXT_ABOVE_BLOCK = 14
|
|
34
34
|
const RESIZE_HANDLE_SIZE = 8
|
|
35
35
|
const RESIZE_HANDLE_HITAREA = 12
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
36
|
+
// Dark theme colors matching karaoke-gen
|
|
37
|
+
const PLAYHEAD_COLOR = '#f97316' // orange-500 for better visibility
|
|
38
|
+
const WORD_BLOCK_COLOR = '#dc2626' // red-600 for dark mode
|
|
39
|
+
const WORD_BLOCK_SELECTED_COLOR = '#b91c1c' // red-700 for dark mode
|
|
40
|
+
const WORD_BLOCK_CURRENT_COLOR = '#ef4444' // red-500 for dark mode
|
|
41
|
+
const WORD_TEXT_CURRENT_COLOR = '#fca5a5' // red-300 for dark mode
|
|
42
|
+
const UPCOMING_WORD_BG = '#2a2a2a' // slate-700 for dark mode
|
|
43
|
+
const UPCOMING_WORD_TEXT = '#e5e5e5' // slate-50 for dark mode
|
|
44
|
+
const TIME_BAR_BG = '#1a1a1a' // slate-800 for dark mode
|
|
45
|
+
const TIME_BAR_TEXT = '#888888' // slate-400 for dark mode
|
|
46
|
+
const TIMELINE_BG = '#0f0f0f' // slate-900 for dark mode
|
|
46
47
|
|
|
47
48
|
// Drag modes
|
|
48
49
|
type DragMode = 'none' | 'selection' | 'resize' | 'move'
|
|
@@ -268,7 +269,7 @@ const TimelineCanvas = memo(function TimelineCanvas({
|
|
|
268
269
|
const x = timeToX(t)
|
|
269
270
|
|
|
270
271
|
ctx.beginPath()
|
|
271
|
-
ctx.strokeStyle = '#
|
|
272
|
+
ctx.strokeStyle = '#64748b' // slate-500 for dark mode
|
|
272
273
|
ctx.lineWidth = 1
|
|
273
274
|
ctx.moveTo(x, TIME_BAR_HEIGHT - 6)
|
|
274
275
|
ctx.lineTo(x, TIME_BAR_HEIGHT)
|
|
@@ -280,7 +281,7 @@ const TimelineCanvas = memo(function TimelineCanvas({
|
|
|
280
281
|
}
|
|
281
282
|
|
|
282
283
|
ctx.beginPath()
|
|
283
|
-
ctx.strokeStyle = '#
|
|
284
|
+
ctx.strokeStyle = '#2a2a2a' // slate-700 for dark mode
|
|
284
285
|
ctx.lineWidth = 1
|
|
285
286
|
ctx.moveTo(0, TIME_BAR_HEIGHT)
|
|
286
287
|
ctx.lineTo(canvasWidth, TIME_BAR_HEIGHT)
|
|
@@ -314,20 +315,20 @@ const TimelineCanvas = memo(function TimelineCanvas({
|
|
|
314
315
|
|
|
315
316
|
// Draw selection border
|
|
316
317
|
if (isSelected) {
|
|
317
|
-
ctx.strokeStyle = '#
|
|
318
|
+
ctx.strokeStyle = '#f97316' // orange-500 for dark mode selection
|
|
318
319
|
ctx.lineWidth = 2
|
|
319
320
|
ctx.strokeRect(bounds.startX, bounds.y, bounds.blockWidth, WORD_BLOCK_HEIGHT)
|
|
320
|
-
|
|
321
|
-
// Draw resize handle (
|
|
321
|
+
|
|
322
|
+
// Draw resize handle (orange dot on right edge) for selected words when hovered
|
|
322
323
|
if (isHovered || selectedWordIds.size === 1) {
|
|
323
324
|
const handleX = bounds.startX + bounds.blockWidth - RESIZE_HANDLE_SIZE / 2
|
|
324
325
|
const handleY = bounds.y + WORD_BLOCK_HEIGHT / 2
|
|
325
|
-
|
|
326
|
+
|
|
326
327
|
ctx.beginPath()
|
|
327
|
-
ctx.fillStyle = '#
|
|
328
|
+
ctx.fillStyle = '#f97316' // orange-500 for dark mode
|
|
328
329
|
ctx.arc(handleX, handleY, RESIZE_HANDLE_SIZE / 2, 0, Math.PI * 2)
|
|
329
330
|
ctx.fill()
|
|
330
|
-
ctx.strokeStyle = '#
|
|
331
|
+
ctx.strokeStyle = '#0f0f0f' // slate-900 for dark mode
|
|
331
332
|
ctx.lineWidth = 1
|
|
332
333
|
ctx.stroke()
|
|
333
334
|
}
|
|
@@ -368,7 +369,7 @@ const TimelineCanvas = memo(function TimelineCanvas({
|
|
|
368
369
|
|
|
369
370
|
if (textStartX < canvasWidth - 10) {
|
|
370
371
|
const isCurrent = word.id === currentWordId
|
|
371
|
-
ctx.fillStyle = isCurrent ? WORD_TEXT_CURRENT_COLOR : '#
|
|
372
|
+
ctx.fillStyle = isCurrent ? WORD_TEXT_CURRENT_COLOR : '#f8fafc' // slate-50 for dark mode
|
|
372
373
|
ctx.fillText(word.text, textStartX, textY)
|
|
373
374
|
rightmostTextEnd = textStartX + textWidth
|
|
374
375
|
}
|
|
@@ -405,7 +406,7 @@ const TimelineCanvas = memo(function TimelineCanvas({
|
|
|
405
406
|
|
|
406
407
|
ctx.beginPath()
|
|
407
408
|
ctx.fillStyle = PLAYHEAD_COLOR
|
|
408
|
-
ctx.strokeStyle = '#
|
|
409
|
+
ctx.strokeStyle = '#0f0f0f' // slate-900 for dark mode
|
|
409
410
|
ctx.lineWidth = 1
|
|
410
411
|
ctx.moveTo(playheadX - 6, 2)
|
|
411
412
|
ctx.lineTo(playheadX + 6, 2)
|
|
@@ -422,7 +423,7 @@ const TimelineCanvas = memo(function TimelineCanvas({
|
|
|
422
423
|
ctx.stroke()
|
|
423
424
|
|
|
424
425
|
ctx.beginPath()
|
|
425
|
-
ctx.strokeStyle = 'rgba(0,0,0,0.
|
|
426
|
+
ctx.strokeStyle = 'rgba(0,0,0,0.6)' // Darker shadow for dark mode visibility
|
|
426
427
|
ctx.lineWidth = 1
|
|
427
428
|
ctx.moveTo(playheadX + 1, TIME_BAR_HEIGHT)
|
|
428
429
|
ctx.lineTo(playheadX + 1, height)
|
|
@@ -54,7 +54,12 @@ export default function TranscriptionView({
|
|
|
54
54
|
onPlaySegment,
|
|
55
55
|
currentTime = 0,
|
|
56
56
|
anchors = [],
|
|
57
|
-
onDataChange
|
|
57
|
+
onDataChange,
|
|
58
|
+
reviewMode = false,
|
|
59
|
+
onRevertCorrection,
|
|
60
|
+
onEditCorrection,
|
|
61
|
+
onAcceptCorrection,
|
|
62
|
+
onShowCorrectionDetail
|
|
58
63
|
}: TranscriptionViewProps) {
|
|
59
64
|
const [selectedSegmentIndex, setSelectedSegmentIndex] = useState<number | null>(null)
|
|
60
65
|
const [viewMode, setViewMode] = useState<'text' | 'duration'>('text')
|
|
@@ -182,7 +187,7 @@ export default function TranscriptionView({
|
|
|
182
187
|
width: '100%',
|
|
183
188
|
mb: 0,
|
|
184
189
|
'&:hover': {
|
|
185
|
-
backgroundColor: 'rgba(
|
|
190
|
+
backgroundColor: 'rgba(248, 250, 252, 0.04)' // slate-50 hover for dark mode
|
|
186
191
|
}
|
|
187
192
|
}}>
|
|
188
193
|
<SegmentControls>
|
|
@@ -237,6 +242,11 @@ export default function TranscriptionView({
|
|
|
237
242
|
currentTime={currentTime}
|
|
238
243
|
gaps={data.gap_sequences}
|
|
239
244
|
corrections={data.corrections}
|
|
245
|
+
reviewMode={reviewMode}
|
|
246
|
+
onRevertCorrection={onRevertCorrection}
|
|
247
|
+
onEditCorrection={onEditCorrection}
|
|
248
|
+
onAcceptCorrection={onAcceptCorrection}
|
|
249
|
+
onShowCorrectionDetail={onShowCorrectionDetail}
|
|
240
250
|
/>
|
|
241
251
|
</TextContainer>
|
|
242
252
|
</Box>
|
|
@@ -18,7 +18,7 @@ interface WordDividerProps {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const buttonTextStyle = {
|
|
21
|
-
color: 'rgba(
|
|
21
|
+
color: 'rgba(248, 250, 252, 0.8)', // slate-50 with opacity for dark mode
|
|
22
22
|
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
|
23
23
|
fontWeight: 400,
|
|
24
24
|
fontSize: '0.7rem',
|
|
@@ -58,7 +58,7 @@ export default function WordDivider({
|
|
|
58
58
|
height: '20px',
|
|
59
59
|
my: -0.5,
|
|
60
60
|
width: '50%',
|
|
61
|
-
backgroundColor: '#
|
|
61
|
+
backgroundColor: '#1a1a1a', // slate-800 for dark mode
|
|
62
62
|
...sx
|
|
63
63
|
}}
|
|
64
64
|
>
|
|
@@ -66,7 +66,7 @@ export default function WordDivider({
|
|
|
66
66
|
display: 'flex',
|
|
67
67
|
alignItems: 'center',
|
|
68
68
|
gap: 1,
|
|
69
|
-
backgroundColor: '#
|
|
69
|
+
backgroundColor: '#1a1a1a', // slate-800 for dark mode
|
|
70
70
|
padding: '0 8px',
|
|
71
71
|
zIndex: 1
|
|
72
72
|
}}>
|