lyrics-transcriber 0.43.1__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.
Files changed (49) hide show
  1. lyrics_transcriber/core/controller.py +58 -24
  2. lyrics_transcriber/correction/anchor_sequence.py +22 -8
  3. lyrics_transcriber/correction/corrector.py +47 -3
  4. lyrics_transcriber/correction/handlers/llm.py +15 -12
  5. lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
  6. lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
  7. lyrics_transcriber/frontend/dist/assets/{index-D0Gr3Ep7.js → index-DVoI6Z16.js} +10799 -7490
  8. lyrics_transcriber/frontend/dist/assets/index-DVoI6Z16.js.map +1 -0
  9. lyrics_transcriber/frontend/dist/index.html +1 -1
  10. lyrics_transcriber/frontend/src/App.tsx +4 -4
  11. lyrics_transcriber/frontend/src/api.ts +37 -0
  12. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
  13. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +14 -10
  14. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +62 -56
  15. lyrics_transcriber/frontend/src/components/EditModal.tsx +232 -237
  16. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  17. lyrics_transcriber/frontend/src/components/GlobalSyncEditor.tsx +675 -0
  18. lyrics_transcriber/frontend/src/components/Header.tsx +141 -101
  19. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +146 -80
  20. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +22 -13
  21. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +1 -0
  22. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +29 -12
  23. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +21 -4
  24. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +29 -15
  25. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +34 -16
  26. lyrics_transcriber/frontend/src/components/WordDivider.tsx +186 -0
  27. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +89 -41
  28. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +9 -2
  29. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +28 -3
  30. lyrics_transcriber/frontend/src/components/shared/types.ts +17 -2
  31. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +63 -14
  32. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +192 -0
  33. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +267 -0
  34. lyrics_transcriber/frontend/src/main.tsx +7 -1
  35. lyrics_transcriber/frontend/src/theme.ts +177 -0
  36. lyrics_transcriber/frontend/src/types.ts +1 -1
  37. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  38. lyrics_transcriber/lyrics/base_lyrics_provider.py +2 -2
  39. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  40. lyrics_transcriber/output/generator.py +40 -12
  41. lyrics_transcriber/review/server.py +238 -8
  42. {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.44.0.dist-info}/METADATA +3 -2
  43. {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.44.0.dist-info}/RECORD +46 -40
  44. lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +0 -1
  45. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +0 -252
  46. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +0 -110
  47. {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.44.0.dist-info}/LICENSE +0 -0
  48. {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.44.0.dist-info}/WHEEL +0 -0
  49. {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.44.0.dist-info}/entry_points.txt +0 -0
@@ -1,3 +1,4 @@
1
+ import { useState, useEffect, useCallback } from 'react'
1
2
  import {
2
3
  AnchorSequence,
3
4
  CorrectionData,
@@ -7,9 +8,7 @@ import {
7
8
  LyricsSegment
8
9
  } from '../types'
9
10
  import { Box, Button, Grid, useMediaQuery, useTheme } from '@mui/material'
10
- import { useCallback, useState, useEffect } from 'react'
11
11
  import { ApiClient } from '../api'
12
- import DetailsModal from './DetailsModal'
13
12
  import ReferenceView from './ReferenceView'
14
13
  import TranscriptionView from './TranscriptionView'
15
14
  import { WordClickInfo, FlashType } from './shared/types'
@@ -19,12 +18,18 @@ import {
19
18
  addSegmentBefore,
20
19
  splitSegment,
21
20
  deleteSegment,
22
- updateSegment
21
+ updateSegment,
22
+ mergeSegment,
23
+ findAndReplace
23
24
  } from './shared/utils/segmentOperations'
24
25
  import { loadSavedData, saveData, clearSavedData } from './shared/utils/localStorage'
25
- import { setupKeyboardHandlers, setModalHandler } from './shared/utils/keyboardHandlers'
26
+ import { setupKeyboardHandlers, setModalHandler, getModalState } from './shared/utils/keyboardHandlers'
26
27
  import Header from './Header'
27
- import { findWordById, getWordsFromIds } from './shared/utils/wordUtils'
28
+ import { getWordsFromIds } from './shared/utils/wordUtils'
29
+ import AddLyricsModal from './AddLyricsModal'
30
+ import { RestoreFromTrash, OndemandVideo } from '@mui/icons-material'
31
+ import FindReplaceModal from './FindReplaceModal'
32
+ import GlobalSyncEditor from './GlobalSyncEditor'
28
33
 
29
34
  // Add type for window augmentation at the top of the file
30
35
  declare global {
@@ -73,9 +78,8 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
73
78
  const [isReviewComplete, setIsReviewComplete] = useState(false)
74
79
  const [data, setData] = useState(initialData)
75
80
  const [originalData] = useState(() => JSON.parse(JSON.stringify(initialData)))
76
- const [interactionMode, setInteractionMode] = useState<InteractionMode>('details')
81
+ const [interactionMode, setInteractionMode] = useState<InteractionMode>('edit')
77
82
  const [isShiftPressed, setIsShiftPressed] = useState(false)
78
- const [isCtrlPressed, setIsCtrlPressed] = useState(false)
79
83
  const [editModalSegment, setEditModalSegment] = useState<{
80
84
  segment: LyricsSegment
81
85
  index: number
@@ -85,6 +89,11 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
85
89
  const [currentAudioTime, setCurrentAudioTime] = useState(0)
86
90
  const [isUpdatingHandlers, setIsUpdatingHandlers] = useState(false)
87
91
  const [flashingHandler, setFlashingHandler] = useState<string | null>(null)
92
+ const [isAddingLyrics, setIsAddingLyrics] = useState(false)
93
+ const [isAddLyricsModalOpen, setIsAddLyricsModalOpen] = useState(false)
94
+ const [isAnyModalOpen, setIsAnyModalOpen] = useState(false)
95
+ const [isFindReplaceModalOpen, setIsFindReplaceModalOpen] = useState(false)
96
+ const [isGlobalSyncEditorOpen, setIsGlobalSyncEditorOpen] = useState(false)
88
97
  const theme = useTheme()
89
98
  const isMobile = useMediaQuery(theme.breakpoints.down('md'))
90
99
 
@@ -121,29 +130,50 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
121
130
 
122
131
  // Keyboard handlers
123
132
  useEffect(() => {
124
- console.log('Setting up keyboard handlers in LyricsAnalyzer')
133
+ const { currentModalHandler } = getModalState()
134
+
135
+ console.log('LyricsAnalyzer - Setting up keyboard effect', {
136
+ isAnyModalOpen,
137
+ hasSpacebarHandler: !!currentModalHandler
138
+ })
125
139
 
126
140
  const { handleKeyDown, handleKeyUp } = setupKeyboardHandlers({
127
141
  setIsShiftPressed,
128
- setIsCtrlPressed
129
142
  })
130
143
 
131
- console.log('Adding keyboard event listeners')
144
+ // Always add keyboard listeners
145
+ console.log('LyricsAnalyzer - Adding keyboard event listeners')
132
146
  window.addEventListener('keydown', handleKeyDown)
133
147
  window.addEventListener('keyup', handleKeyUp)
134
148
 
149
+ // Reset modifier states when a modal opens
150
+ if (isAnyModalOpen) {
151
+ setIsShiftPressed(false)
152
+ }
153
+
154
+ // Cleanup function
135
155
  return () => {
136
- console.log('Removing keyboard event listeners')
156
+ console.log('LyricsAnalyzer - Cleanup effect running')
137
157
  window.removeEventListener('keydown', handleKeyDown)
138
158
  window.removeEventListener('keyup', handleKeyUp)
139
159
  document.body.style.userSelect = ''
140
160
  }
141
- }, [setIsShiftPressed, setIsCtrlPressed])
161
+ }, [setIsShiftPressed, isAnyModalOpen])
162
+
163
+ // Update modal state tracking
164
+ useEffect(() => {
165
+ const modalOpen = Boolean(
166
+ modalContent ||
167
+ editModalSegment ||
168
+ isReviewModalOpen ||
169
+ isAddLyricsModalOpen ||
170
+ isFindReplaceModalOpen
171
+ )
172
+ setIsAnyModalOpen(modalOpen)
173
+ }, [modalContent, editModalSegment, isReviewModalOpen, isAddLyricsModalOpen, isFindReplaceModalOpen])
142
174
 
143
175
  // Calculate effective mode based on modifier key states
144
- const effectiveMode = isShiftPressed ? 'highlight' :
145
- isCtrlPressed ? 'edit' :
146
- interactionMode
176
+ const effectiveMode = isShiftPressed ? 'highlight' : interactionMode
147
177
 
148
178
  const handleFlash = useCallback((type: FlashType, info?: HighlightInfo) => {
149
179
  setFlashingType(null)
@@ -217,10 +247,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
217
247
  start_time: sourceWords[0]?.start_time ?? null,
218
248
  end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
219
249
  }
220
- return [
221
- source,
222
- getWordsFromIds([tempSourceSegment], ids)
223
- ]
250
+ return [source, getWordsFromIds([tempSourceSegment], ids)]
224
251
  })
225
252
  );
226
253
 
@@ -236,13 +263,11 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
236
263
 
237
264
  // Find if this word is part of a gap sequence
238
265
  const gap = data.gap_sequences?.find(g =>
239
- g.transcribed_word_ids.includes(info.word_id) ||
240
- Object.values(g.reference_word_ids).some(ids =>
241
- ids.includes(info.word_id)
242
- )
266
+ g.transcribed_word_ids.includes(info.word_id)
243
267
  );
244
268
 
245
269
  if (gap) {
270
+ // Create a temporary segment containing all words
246
271
  const allWords = data.corrected_segments.flatMap(s => s.words)
247
272
  const tempSegment: LyricsSegment = {
248
273
  id: 'temp',
@@ -267,10 +292,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
267
292
  start_time: sourceWords[0]?.start_time ?? null,
268
293
  end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
269
294
  }
270
- return [
271
- source,
272
- getWordsFromIds([tempSourceSegment], ids)
273
- ]
295
+ return [source, getWordsFromIds([tempSourceSegment], ids)]
274
296
  })
275
297
  );
276
298
 
@@ -297,30 +319,6 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
297
319
  originalSegment: JSON.parse(JSON.stringify(segment))
298
320
  });
299
321
  }
300
- } else if (effectiveMode === 'details') {
301
- if (info.type === 'anchor' && info.anchor) {
302
- const word = findWordById(data.corrected_segments, info.word_id);
303
- setModalContent({
304
- type: 'anchor',
305
- data: {
306
- ...info.anchor,
307
- wordId: info.word_id,
308
- word: word?.text,
309
- anchor_sequences: data.anchor_sequences
310
- }
311
- });
312
- } else if (info.type === 'gap' && info.gap) {
313
- const word = findWordById(data.corrected_segments, info.word_id);
314
- setModalContent({
315
- type: 'gap',
316
- data: {
317
- ...info.gap,
318
- wordId: info.word_id,
319
- word: word?.text || '',
320
- anchor_sequences: data.anchor_sequences
321
- }
322
- });
323
- }
324
322
  }
325
323
  }, [data, effectiveMode, setModalContent]);
326
324
 
@@ -372,7 +370,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
372
370
  setModalContent(null)
373
371
  setFlashingType(null)
374
372
  setHighlightInfo(null)
375
- setInteractionMode('details')
373
+ setInteractionMode('edit')
376
374
  }
377
375
  }, [initialData])
378
376
 
@@ -389,6 +387,12 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
389
387
  }
390
388
  }, [data])
391
389
 
390
+ const handleMergeSegment = useCallback((segmentIndex: number, mergeWithNext: boolean) => {
391
+ const newData = mergeSegment(data, segmentIndex, mergeWithNext)
392
+ setData(newData)
393
+ setEditModalSegment(null)
394
+ }, [data])
395
+
392
396
  const handleHandlerToggle = useCallback(async (handler: string, enabled: boolean) => {
393
397
  if (!apiClient) return
394
398
 
@@ -443,14 +447,35 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
443
447
 
444
448
  // Wrap setModalSpacebarHandler in useCallback
445
449
  const handleSetModalSpacebarHandler = useCallback((handler: (() => (e: KeyboardEvent) => void) | undefined) => {
450
+ console.log('LyricsAnalyzer - Setting modal handler:', {
451
+ hasHandler: !!handler
452
+ })
446
453
  // Update the global modal handler
447
454
  setModalHandler(handler ? handler() : undefined, !!handler)
448
455
  }, [])
449
456
 
457
+ // Add new handler for adding lyrics
458
+ const handleAddLyrics = useCallback(async (source: string, lyrics: string) => {
459
+ if (!apiClient) return
460
+
461
+ try {
462
+ setIsAddingLyrics(true)
463
+ const newData = await apiClient.addLyrics(source, lyrics)
464
+ setData(newData)
465
+ } finally {
466
+ setIsAddingLyrics(false)
467
+ }
468
+ }, [apiClient])
469
+
470
+ const handleFindReplace = (findText: string, replaceText: string, options: { caseSensitive: boolean, useRegex: boolean, fullTextMode: boolean }) => {
471
+ const newData = findAndReplace(data, findText, replaceText, options)
472
+ setData(newData)
473
+ }
474
+
450
475
  return (
451
476
  <Box sx={{
452
- p: 3,
453
- pb: 6,
477
+ p: 1,
478
+ pb: 3,
454
479
  maxWidth: '100%',
455
480
  overflowX: 'hidden'
456
481
  }}>
@@ -471,9 +496,12 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
471
496
  onHandlerToggle={handleHandlerToggle}
472
497
  isUpdatingHandlers={isUpdatingHandlers}
473
498
  onHandlerClick={handleHandlerClick}
499
+ onAddLyrics={() => setIsAddLyricsModalOpen(true)}
500
+ onFindReplace={() => setIsFindReplaceModalOpen(true)}
501
+ onEditSync={() => setIsGlobalSyncEditorOpen(true)}
474
502
  />
475
503
 
476
- <Grid container spacing={2} direction={isMobile ? 'column' : 'row'}>
504
+ <Grid container spacing={1} direction={isMobile ? 'column' : 'row'}>
477
505
  <Grid item xs={12} md={6}>
478
506
  <TranscriptionView
479
507
  data={data}
@@ -487,6 +515,32 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
487
515
  currentTime={currentAudioTime}
488
516
  anchors={data.anchor_sequences}
489
517
  />
518
+ {!isReadOnly && apiClient && (
519
+ <Box sx={{
520
+ mt: 2,
521
+ mb: 3,
522
+ display: 'flex',
523
+ justifyContent: 'space-between',
524
+ width: '100%'
525
+ }}>
526
+ <Button
527
+ variant="outlined"
528
+ color="warning"
529
+ onClick={handleResetCorrections}
530
+ startIcon={<RestoreFromTrash />}
531
+ >
532
+ Reset Corrections
533
+ </Button>
534
+ <Button
535
+ variant="contained"
536
+ onClick={handleFinishReview}
537
+ disabled={isReviewComplete}
538
+ endIcon={<OndemandVideo />}
539
+ >
540
+ {isReviewComplete ? 'Review Complete' : 'Preview Video'}
541
+ </Button>
542
+ </Box>
543
+ )}
490
544
  </Grid>
491
545
  <Grid item xs={12} md={6}>
492
546
  <ReferenceView
@@ -506,14 +560,6 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
506
560
  </Grid>
507
561
  </Grid>
508
562
 
509
- <DetailsModal
510
- open={modalContent !== null}
511
- content={modalContent}
512
- onClose={() => setModalContent(null)}
513
- allCorrections={data.corrections}
514
- referenceLyrics={data.reference_lyrics}
515
- />
516
-
517
563
  <EditModal
518
564
  open={Boolean(editModalSegment)}
519
565
  onClose={() => {
@@ -527,9 +573,17 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
527
573
  onDelete={handleDeleteSegment}
528
574
  onAddSegment={handleAddSegment}
529
575
  onSplitSegment={handleSplitSegment}
576
+ onMergeSegment={handleMergeSegment}
530
577
  onPlaySegment={handlePlaySegment}
531
578
  currentTime={currentAudioTime}
532
579
  setModalSpacebarHandler={handleSetModalSpacebarHandler}
580
+ originalTranscribedSegment={
581
+ editModalSegment?.segment && editModalSegment?.index !== null
582
+ ? originalData.original_segments.find(
583
+ (s: LyricsSegment) => s.id === editModalSegment.segment.id
584
+ ) || null
585
+ : null
586
+ }
533
587
  />
534
588
 
535
589
  <ReviewChangesModal
@@ -542,23 +596,35 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
542
596
  setModalSpacebarHandler={handleSetModalSpacebarHandler}
543
597
  />
544
598
 
545
- {!isReadOnly && apiClient && (
546
- <Box sx={{ mt: 2, mb: 3, display: 'flex', gap: 2 }}>
547
- <Button
548
- variant="contained"
549
- onClick={handleFinishReview}
550
- disabled={isReviewComplete}
551
- >
552
- {isReviewComplete ? 'Review Complete' : 'Finish Review'}
553
- </Button>
554
- <Button
555
- variant="outlined"
556
- color="warning"
557
- onClick={handleResetCorrections}
558
- >
559
- Reset Corrections
560
- </Button>
561
- </Box>
599
+ <AddLyricsModal
600
+ open={isAddLyricsModalOpen}
601
+ onClose={() => setIsAddLyricsModalOpen(false)}
602
+ onSubmit={handleAddLyrics}
603
+ isSubmitting={isAddingLyrics}
604
+ />
605
+
606
+ <FindReplaceModal
607
+ open={isFindReplaceModalOpen}
608
+ onClose={() => setIsFindReplaceModalOpen(false)}
609
+ onReplace={handleFindReplace}
610
+ data={data}
611
+ />
612
+
613
+ {/* Add GlobalSyncEditor modal */}
614
+ {!isReadOnly && (
615
+ <GlobalSyncEditor
616
+ open={isGlobalSyncEditorOpen}
617
+ onClose={() => setIsGlobalSyncEditorOpen(false)}
618
+ segments={data.corrected_segments || []}
619
+ onSave={(updatedSegments) => {
620
+ const newData = { ...data, corrected_segments: updatedSegments };
621
+ setData(newData);
622
+ saveData(newData, initialData);
623
+ }}
624
+ onPlaySegment={handlePlaySegment}
625
+ currentTime={currentAudioTime}
626
+ setModalSpacebarHandler={handleSetModalSpacebarHandler}
627
+ />
562
628
  )}
563
629
  </Box>
564
630
  )
@@ -1,6 +1,5 @@
1
1
  import { ToggleButton, ToggleButtonGroup, Box, Typography } from '@mui/material';
2
2
  import HighlightIcon from '@mui/icons-material/Highlight';
3
- import InfoIcon from '@mui/icons-material/Info';
4
3
  import EditIcon from '@mui/icons-material/Edit';
5
4
  import { InteractionMode } from '../types';
6
5
 
@@ -11,28 +10,38 @@ interface ModeSelectorProps {
11
10
 
12
11
  export default function ModeSelector({ effectiveMode, onChange }: ModeSelectorProps) {
13
12
  return (
14
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
15
- <Typography variant="body2" color="text.secondary">
16
- Click Mode:
13
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.2, height: '32px' }}>
14
+ <Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.8rem' }}>
15
+ Mode:
17
16
  </Typography>
18
17
  <ToggleButtonGroup
19
18
  value={effectiveMode}
20
19
  exclusive
21
20
  onChange={(_, newMode) => newMode && onChange(newMode)}
22
21
  size="small"
22
+ sx={{
23
+ height: '32px',
24
+ '& .MuiToggleButton-root': {
25
+ padding: '3px 8px',
26
+ fontSize: '0.75rem',
27
+ height: '32px'
28
+ }
29
+ }}
23
30
  >
24
- <ToggleButton value="details">
25
- <InfoIcon sx={{ mr: 1 }} />
26
- Details
31
+ <ToggleButton
32
+ value="edit"
33
+ title="Click to edit segments and make corrections in the transcription view"
34
+ >
35
+ <EditIcon sx={{ mr: 0.5, fontSize: '1rem' }} />
36
+ Edit
27
37
  </ToggleButton>
28
- <ToggleButton value="highlight">
29
- <HighlightIcon sx={{ mr: 1 }} />
38
+ <ToggleButton
39
+ value="highlight"
40
+ title="Click words in the transcription view to highlight the matching anchor sequence in the reference lyrics. You can also hold SHIFT to temporarily activate this mode."
41
+ >
42
+ <HighlightIcon sx={{ mr: 0.5, fontSize: '1rem' }} />
30
43
  Highlight
31
44
  </ToggleButton>
32
- <ToggleButton value="edit">
33
- <EditIcon sx={{ mr: 1 }} />
34
- Edit
35
- </ToggleButton>
36
45
  </ToggleButtonGroup>
37
46
  </Box>
38
47
  );
@@ -104,6 +104,7 @@ export default function PreviewVideoSection({
104
104
  <video
105
105
  ref={videoRef}
106
106
  controls
107
+ autoPlay
107
108
  src={previewState.videoUrl}
108
109
  style={{
109
110
  display: 'block',
@@ -12,9 +12,9 @@ import { styled } from '@mui/material/styles'
12
12
  const SegmentControls = styled(Box)({
13
13
  display: 'flex',
14
14
  alignItems: 'center',
15
- gap: '4px',
16
- paddingTop: '3px',
17
- paddingRight: '8px'
15
+ gap: '2px',
16
+ paddingTop: '1px',
17
+ paddingRight: '4px'
18
18
  })
19
19
 
20
20
  const TextContainer = styled(Box)({
@@ -174,27 +174,44 @@ export default function ReferenceView({
174
174
  };
175
175
 
176
176
  return (
177
- <Paper sx={{ p: 2 }}>
178
- <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
179
- <Typography variant="h6">
180
- Reference Text
177
+ <Paper sx={{ p: 0.8, position: 'relative' }}>
178
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.5 }}>
179
+ <Typography variant="h6" sx={{ fontSize: '0.9rem', mb: 0 }}>
180
+ Reference Lyrics
181
181
  </Typography>
182
182
  <SourceSelector
183
+ availableSources={availableSources}
183
184
  currentSource={effectiveCurrentSource}
184
185
  onSourceChange={onSourceChange}
185
- availableSources={availableSources}
186
186
  />
187
187
  </Box>
188
- <Box sx={{ display: 'flex', flexDirection: 'column' }}>
188
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.2 }}>
189
189
  {currentSourceSegments.map((segment, index) => (
190
- <Box key={index} sx={{ display: 'flex', alignItems: 'flex-start', width: '100%' }}>
190
+ <Box
191
+ key={index}
192
+ sx={{
193
+ display: 'flex',
194
+ alignItems: 'flex-start',
195
+ width: '100%',
196
+ mb: 0,
197
+ '&:hover': {
198
+ backgroundColor: 'rgba(0, 0, 0, 0.03)'
199
+ }
200
+ }}
201
+ >
191
202
  <SegmentControls>
192
203
  <IconButton
193
204
  size="small"
194
205
  onClick={() => copyToClipboard(segment.words.map(w => w.text).join(' '))}
195
- sx={{ padding: '2px' }}
206
+ sx={{
207
+ padding: '1px',
208
+ height: '18px',
209
+ width: '18px',
210
+ minHeight: '18px',
211
+ minWidth: '18px'
212
+ }}
196
213
  >
197
- <ContentCopyIcon fontSize="small" />
214
+ <ContentCopyIcon sx={{ fontSize: '0.9rem' }} />
198
215
  </IconButton>
199
216
  </SegmentControls>
200
217
  <TextContainer>
@@ -12,6 +12,7 @@ import { CorrectionData } from '../types'
12
12
  import { useMemo, useRef, useEffect } from 'react'
13
13
  import { ApiClient } from '../api'
14
14
  import PreviewVideoSection from './PreviewVideoSection'
15
+ import { CloudUpload, ArrowBack } from '@mui/icons-material'
15
16
 
16
17
  interface ReviewChangesModalProps {
17
18
  open: boolean
@@ -73,6 +74,13 @@ export default function ReviewChangesModal({
73
74
  // Add ref to video element
74
75
  const videoRef = useRef<HTMLVideoElement>(null)
75
76
 
77
+ // Stop audio playback when modal opens
78
+ useEffect(() => {
79
+ if (open && window.isAudioPlaying && window.toggleAudioPlayback) {
80
+ window.toggleAudioPlayback()
81
+ }
82
+ }, [open])
83
+
76
84
  // Add effect to handle spacebar
77
85
  useEffect(() => {
78
86
  if (open) {
@@ -269,7 +277,7 @@ export default function ReviewChangesModal({
269
277
  maxWidth="md"
270
278
  fullWidth
271
279
  >
272
- <DialogTitle>Review Changes</DialogTitle>
280
+ <DialogTitle>Preview Video (With Vocals)</DialogTitle>
273
281
  <DialogContent
274
282
  dividers
275
283
  sx={{
@@ -288,7 +296,7 @@ export default function ReviewChangesModal({
288
296
  {differences.length === 0 ? (
289
297
  <Box>
290
298
  <Typography color="text.secondary">
291
- No changes detected. You can still submit to continue processing.
299
+ No manual corrections detected. If everything looks good in the preview, click submit and the server will generate the final karaoke video.
292
300
  </Typography>
293
301
  <Typography variant="body2" color="text.secondary">
294
302
  Total segments: {updatedData.corrected_segments.length}
@@ -307,12 +315,21 @@ export default function ReviewChangesModal({
307
315
  </Box>
308
316
  </DialogContent>
309
317
  <DialogActions>
310
- <Button onClick={onClose}>Cancel</Button>
318
+ <Button
319
+ onClick={onClose}
320
+ color="warning"
321
+ startIcon={<ArrowBack />}
322
+ sx={{ mr: 'auto' }}
323
+ >
324
+ Cancel
325
+ </Button>
311
326
  <Button
312
327
  onClick={onSubmit}
313
328
  variant="contained"
329
+ color="success"
330
+ endIcon={<CloudUpload />}
314
331
  >
315
- Submit to Server
332
+ Complete Review
316
333
  </Button>
317
334
  </DialogActions>
318
335
  </Dialog>