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.
Files changed (50) hide show
  1. karaoke_gen/audio_fetcher.py +218 -0
  2. karaoke_gen/instrumental_review/static/index.html +179 -16
  3. karaoke_gen/karaoke_gen.py +191 -25
  4. karaoke_gen/lyrics_processor.py +39 -31
  5. karaoke_gen/utils/__init__.py +26 -0
  6. karaoke_gen/utils/cli_args.py +9 -1
  7. karaoke_gen/utils/gen_cli.py +1 -1
  8. karaoke_gen/utils/remote_cli.py +33 -6
  9. {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.81.1.dist-info}/METADATA +80 -4
  10. {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.81.1.dist-info}/RECORD +50 -43
  11. lyrics_transcriber/core/config.py +8 -0
  12. lyrics_transcriber/core/controller.py +43 -1
  13. lyrics_transcriber/correction/agentic/providers/config.py +6 -0
  14. lyrics_transcriber/correction/agentic/providers/model_factory.py +24 -1
  15. lyrics_transcriber/correction/agentic/router.py +17 -13
  16. lyrics_transcriber/frontend/.gitignore +1 -0
  17. lyrics_transcriber/frontend/e2e/agentic-corrections.spec.ts +207 -0
  18. lyrics_transcriber/frontend/e2e/fixtures/agentic-correction-data.json +226 -0
  19. lyrics_transcriber/frontend/index.html +5 -1
  20. lyrics_transcriber/frontend/package-lock.json +4553 -0
  21. lyrics_transcriber/frontend/package.json +7 -1
  22. lyrics_transcriber/frontend/playwright.config.ts +69 -0
  23. lyrics_transcriber/frontend/public/nomad-karaoke-logo.svg +5 -0
  24. lyrics_transcriber/frontend/src/App.tsx +88 -59
  25. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +55 -21
  26. lyrics_transcriber/frontend/src/components/AppHeader.tsx +65 -0
  27. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +39 -35
  28. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +9 -9
  29. lyrics_transcriber/frontend/src/components/EditModal.tsx +1 -1
  30. lyrics_transcriber/frontend/src/components/EditWordList.tsx +1 -1
  31. lyrics_transcriber/frontend/src/components/Header.tsx +96 -3
  32. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +120 -3
  33. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +22 -21
  34. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
  35. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +12 -2
  36. lyrics_transcriber/frontend/src/components/WordDivider.tsx +3 -3
  37. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +122 -35
  38. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +2 -2
  39. lyrics_transcriber/frontend/src/components/shared/constants.ts +15 -5
  40. lyrics_transcriber/frontend/src/components/shared/types.ts +6 -0
  41. lyrics_transcriber/frontend/src/main.tsx +1 -7
  42. lyrics_transcriber/frontend/src/theme.ts +337 -135
  43. lyrics_transcriber/frontend/vite.config.ts +5 -0
  44. lyrics_transcriber/frontend/yarn.lock +1005 -1046
  45. lyrics_transcriber/output/generator.py +50 -3
  46. lyrics_transcriber/review/server.py +1 -1
  47. lyrics_transcriber/transcribers/local_whisper.py +260 -0
  48. {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.81.1.dist-info}/WHEEL +0 -0
  49. {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.81.1.dist-info}/entry_points.txt +0 -0
  50. {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 #ccc',
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: '#999',
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: '#666',
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: '#f5f5f5',
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
- : '#e0e0e0',
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: '#888',
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(255, 255, 255, 0.8)',
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 ? '#1b5e20' : 'inherit'
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(0,0,0,0.6)',
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(255, 255, 255, 0.9)',
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(0,0,0,0.2)',
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
- const PLAYHEAD_COLOR = '#ffffff'
37
- const WORD_BLOCK_COLOR = '#d32f2f'
38
- const WORD_BLOCK_SELECTED_COLOR = '#b71c1c'
39
- const WORD_BLOCK_CURRENT_COLOR = '#f44336'
40
- const WORD_TEXT_CURRENT_COLOR = '#d32f2f'
41
- const UPCOMING_WORD_BG = '#fff9c4'
42
- const UPCOMING_WORD_TEXT = '#000000'
43
- const TIME_BAR_BG = '#f5f5f5'
44
- const TIME_BAR_TEXT = '#666666'
45
- const TIMELINE_BG = '#e0e0e0'
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 = '#999999'
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 = '#cccccc'
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 = '#ffffff'
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 (white dot on right edge) for selected words when hovered
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 = '#ffffff'
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 = '#666666'
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 : '#333333'
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 = '#333333'
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.4)'
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)
@@ -221,7 +221,7 @@ export default function ReferenceView({
221
221
  width: '100%',
222
222
  mb: 0,
223
223
  '&:hover': {
224
- backgroundColor: 'rgba(0, 0, 0, 0.03)'
224
+ backgroundColor: 'rgba(248, 250, 252, 0.04)' // slate-50 hover for dark mode
225
225
  }
226
226
  }}
227
227
  >
@@ -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(0, 0, 0, 0.03)'
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(0, 0, 0, 0.6)',
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: '#fff',
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: '#fff',
69
+ backgroundColor: '#1a1a1a', // slate-800 for dark mode
70
70
  padding: '0 8px',
71
71
  zIndex: 1
72
72
  }}>