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
@@ -7,25 +7,23 @@ import {
7
7
  Box,
8
8
  TextField,
9
9
  Button,
10
- Typography,
11
- Menu,
12
- MenuItem,
10
+ Typography
13
11
  } from '@mui/material'
14
12
  import CloseIcon from '@mui/icons-material/Close'
15
- import AddIcon from '@mui/icons-material/Add'
16
13
  import DeleteIcon from '@mui/icons-material/Delete'
17
- import MergeIcon from '@mui/icons-material/CallMerge'
18
14
  import SplitIcon from '@mui/icons-material/CallSplit'
19
15
  import RestoreIcon from '@mui/icons-material/RestoreFromTrash'
20
- import MoreVertIcon from '@mui/icons-material/MoreVert'
21
16
  import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
22
17
  import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
23
18
  import CancelIcon from '@mui/icons-material/Cancel'
24
19
  import StopIcon from '@mui/icons-material/Stop'
20
+ import HistoryIcon from '@mui/icons-material/History'
25
21
  import { LyricsSegment, Word } from '../types'
26
22
  import { useState, useEffect, useCallback } from 'react'
27
23
  import TimelineEditor from './TimelineEditor'
28
24
  import { nanoid } from 'nanoid'
25
+ import WordDivider from './WordDivider'
26
+ import useManualSync from '../hooks/useManualSync'
29
27
 
30
28
  interface EditModalProps {
31
29
  open: boolean
@@ -39,7 +37,9 @@ interface EditModalProps {
39
37
  onDelete?: (segmentIndex: number) => void
40
38
  onAddSegment?: (segmentIndex: number) => void
41
39
  onSplitSegment?: (segmentIndex: number, afterWordIndex: number) => void
40
+ onMergeSegment?: (segmentIndex: number, mergeWithNext: boolean) => void
42
41
  setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
42
+ originalTranscribedSegment?: LyricsSegment | null
43
43
  }
44
44
 
45
45
  export default function EditModal({
@@ -54,18 +54,15 @@ export default function EditModal({
54
54
  onDelete,
55
55
  onAddSegment,
56
56
  onSplitSegment,
57
+ onMergeSegment,
57
58
  setModalSpacebarHandler,
59
+ originalTranscribedSegment
58
60
  }: EditModalProps) {
59
- // All useState hooks
60
61
  const [editedSegment, setEditedSegment] = useState<LyricsSegment | null>(segment)
61
- const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null)
62
- const [selectedWordIndex, setSelectedWordIndex] = useState<number | null>(null)
63
62
  const [replacementText, setReplacementText] = useState('')
64
- const [isManualSyncing, setIsManualSyncing] = useState(false)
65
- const [syncWordIndex, setSyncWordIndex] = useState<number>(-1)
66
63
  const [isPlaying, setIsPlaying] = useState(false)
67
64
 
68
- // Define updateSegment first since other hooks depend on it
65
+ // Define updateSegment first since the hook depends on it
69
66
  const updateSegment = useCallback((newWords: Word[]) => {
70
67
  if (!editedSegment) return;
71
68
 
@@ -84,84 +81,78 @@ export default function EditModal({
84
81
  })
85
82
  }, [editedSegment])
86
83
 
87
- // Other useCallback hooks
88
- const cleanupManualSync = useCallback(() => {
89
- setIsManualSyncing(false)
90
- setSyncWordIndex(-1)
91
- }, [])
84
+ // Use the new hook
85
+ const {
86
+ isManualSyncing,
87
+ syncWordIndex,
88
+ startManualSync,
89
+ cleanupManualSync,
90
+ handleSpacebar,
91
+ isSpacebarPressed
92
+ } = useManualSync({
93
+ editedSegment,
94
+ currentTime,
95
+ onPlaySegment,
96
+ updateSegment
97
+ })
92
98
 
93
99
  const handleClose = useCallback(() => {
94
100
  cleanupManualSync()
95
101
  onClose()
96
102
  }, [onClose, cleanupManualSync])
97
103
 
98
- // All useEffect hooks
99
- useEffect(() => {
100
- setEditedSegment(segment)
101
- }, [segment])
102
-
103
104
  // Update the spacebar handler when modal state changes
104
105
  useEffect(() => {
106
+ const spacebarHandler = handleSpacebar // Capture the current handler
107
+
105
108
  if (open) {
106
- setModalSpacebarHandler(() => (e: KeyboardEvent) => {
107
- e.preventDefault()
108
- e.stopPropagation()
109
-
110
- if (isManualSyncing && editedSegment) {
111
- // Handle manual sync mode
112
- if (syncWordIndex < editedSegment.words.length) {
113
- const newWords = [...editedSegment.words]
114
- const currentWord = newWords[syncWordIndex]
115
- const prevWord = syncWordIndex > 0 ? newWords[syncWordIndex - 1] : null
116
-
117
- currentWord.start_time = currentTime
118
-
119
- if (prevWord) {
120
- prevWord.end_time = currentTime - 0.01
121
- }
122
-
123
- if (syncWordIndex === editedSegment.words.length - 1) {
124
- currentWord.end_time = editedSegment.end_time
125
- setIsManualSyncing(false)
126
- setSyncWordIndex(-1)
127
- updateSegment(newWords)
128
- } else {
129
- setSyncWordIndex(syncWordIndex + 1)
130
- updateSegment(newWords)
131
- }
132
- }
133
- } else if (editedSegment && onPlaySegment) {
134
- // Toggle segment playback when not in manual sync mode
135
- const startTime = editedSegment.start_time ?? 0
136
- const endTime = editedSegment.end_time ?? 0
137
-
138
- if (currentTime >= startTime && currentTime <= endTime) {
139
- if (window.toggleAudioPlayback) {
140
- window.toggleAudioPlayback()
141
- }
142
- } else {
143
- onPlaySegment(startTime)
144
- }
145
- }
109
+ console.log('EditModal - Setting up modal spacebar handler', {
110
+ hasPlaySegment: !!onPlaySegment,
111
+ editedSegmentId: editedSegment?.id,
112
+ handlerFunction: spacebarHandler.toString().slice(0, 100)
146
113
  })
147
- } else {
148
- setModalSpacebarHandler(undefined)
149
- }
150
-
151
- return () => {
152
- setModalSpacebarHandler(undefined)
114
+
115
+ // Create a function that will be called by the global event listeners
116
+ const handleKeyEvent = (e: KeyboardEvent) => {
117
+ if (e.code === 'Space') {
118
+ spacebarHandler(e)
119
+ }
120
+ }
121
+
122
+ setModalSpacebarHandler(() => handleKeyEvent)
123
+
124
+ // Only cleanup when the effect is re-run or the modal is closed
125
+ return () => {
126
+ if (!open) {
127
+ console.log('EditModal - Cleanup: clearing modal spacebar handler')
128
+ setModalSpacebarHandler(undefined)
129
+ }
130
+ }
153
131
  }
154
132
  }, [
155
133
  open,
156
- isManualSyncing,
157
- editedSegment,
158
- syncWordIndex,
159
- currentTime,
160
- onPlaySegment,
161
- updateSegment,
162
- setModalSpacebarHandler
134
+ handleSpacebar,
135
+ setModalSpacebarHandler,
136
+ editedSegment?.id,
137
+ onPlaySegment
163
138
  ])
164
139
 
140
+ // Update isPlaying when currentTime changes
141
+ useEffect(() => {
142
+ if (editedSegment) {
143
+ const startTime = editedSegment.start_time ?? 0
144
+ const endTime = editedSegment.end_time ?? 0
145
+ const isWithinSegment = currentTime >= startTime && currentTime <= endTime
146
+
147
+ setIsPlaying(isWithinSegment && window.isAudioPlaying === true)
148
+ }
149
+ }, [currentTime, editedSegment])
150
+
151
+ // All useEffect hooks
152
+ useEffect(() => {
153
+ setEditedSegment(segment)
154
+ }, [segment])
155
+
165
156
  // Auto-stop sync if we go past the end time
166
157
  useEffect(() => {
167
158
  if (!editedSegment) return
@@ -171,23 +162,10 @@ export default function EditModal({
171
162
  if (window.isAudioPlaying && currentTime > endTime) {
172
163
  console.log('Stopping playback: current time exceeded end time')
173
164
  window.toggleAudioPlayback?.()
174
- setIsManualSyncing(false)
175
- setSyncWordIndex(-1)
165
+ cleanupManualSync()
176
166
  }
177
167
 
178
- }, [isManualSyncing, editedSegment, currentTime, setSyncWordIndex])
179
-
180
- // Update isPlaying when currentTime changes
181
- useEffect(() => {
182
- if (editedSegment) {
183
- const startTime = editedSegment.start_time ?? 0
184
- const endTime = editedSegment.end_time ?? 0
185
- const isWithinSegment = currentTime >= startTime && currentTime <= endTime
186
-
187
- // Only consider it playing if it's within the segment AND audio is actually playing
188
- setIsPlaying(isWithinSegment && window.isAudioPlaying === true)
189
- }
190
- }, [currentTime, editedSegment])
168
+ }, [isManualSyncing, editedSegment, currentTime, cleanupManualSync])
191
169
 
192
170
  // Add a function to get safe time values
193
171
  const getSafeTimeRange = (segment: LyricsSegment | null) => {
@@ -256,36 +234,36 @@ export default function EditModal({
256
234
  const word = editedSegment.words[index]
257
235
  const startTime = word.start_time ?? 0
258
236
  const endTime = word.end_time ?? startTime + 0.5
259
- const midTime = (startTime + endTime) / 2
260
- const words = word.text.split(/\s+/)
237
+ const totalDuration = endTime - startTime
238
+
239
+ // Split on any number of spaces and filter out empty strings
240
+ const words = word.text.split(/\s+/).filter(w => w.length > 0)
261
241
 
262
242
  if (words.length <= 1) {
263
- // Split single word in half
243
+ // If no spaces found, split the word in half as before
264
244
  const firstHalf = word.text.slice(0, Math.ceil(word.text.length / 2))
265
245
  const secondHalf = word.text.slice(Math.ceil(word.text.length / 2))
266
246
  words[0] = firstHalf
267
247
  words[1] = secondHalf
268
248
  }
269
249
 
270
- const newWords = [...editedSegment.words]
271
- newWords.splice(index, 1,
272
- {
273
- id: nanoid(),
274
- text: words[0],
275
- start_time: startTime,
276
- end_time: midTime,
277
- confidence: 1.0
278
- },
279
- {
280
- id: nanoid(),
281
- text: words[1],
282
- start_time: midTime,
283
- end_time: endTime,
284
- confidence: 1.0
285
- }
286
- )
250
+ // Calculate time per word
251
+ const timePerWord = totalDuration / words.length
287
252
 
288
- updateSegment(newWords)
253
+ // Create new word objects with evenly distributed times
254
+ const newWords = words.map((text, i) => ({
255
+ id: nanoid(),
256
+ text,
257
+ start_time: startTime + (i * timePerWord),
258
+ end_time: startTime + ((i + 1) * timePerWord),
259
+ confidence: 1.0
260
+ }))
261
+
262
+ // Replace the original word with the new words
263
+ const allWords = [...editedSegment.words]
264
+ allWords.splice(index, 1, ...newWords)
265
+
266
+ updateSegment(allWords)
289
267
  }
290
268
 
291
269
  const handleMergeWords = (index: number) => {
@@ -315,14 +293,10 @@ export default function EditModal({
315
293
  setEditedSegment(JSON.parse(JSON.stringify(originalSegment)))
316
294
  }
317
295
 
318
- const handleWordMenu = (event: React.MouseEvent<HTMLElement>, index: number) => {
319
- setMenuAnchorEl(event.currentTarget)
320
- setSelectedWordIndex(index)
321
- }
322
-
323
- const handleMenuClose = () => {
324
- setMenuAnchorEl(null)
325
- setSelectedWordIndex(null)
296
+ const handleRevertToOriginal = () => {
297
+ if (originalTranscribedSegment) {
298
+ setEditedSegment(JSON.parse(JSON.stringify(originalTranscribedSegment)))
299
+ }
326
300
  }
327
301
 
328
302
  const handleSave = () => {
@@ -395,21 +369,12 @@ export default function EditModal({
395
369
  }
396
370
  }
397
371
 
398
- // Add this new function to handle manual sync
399
- const startManualSync = () => {
400
- if (isManualSyncing) {
401
- setIsManualSyncing(false)
402
- setSyncWordIndex(-1)
403
- return
372
+ const handleMergeSegment = (mergeWithNext: boolean) => {
373
+ if (segmentIndex !== null && editedSegment) {
374
+ handleSave() // Save current changes first
375
+ onMergeSegment?.(segmentIndex, mergeWithNext)
376
+ onClose()
404
377
  }
405
-
406
- if (!editedSegment || !onPlaySegment) return
407
-
408
- setIsManualSyncing(true)
409
- setSyncWordIndex(0)
410
- // Start playing 3 seconds before segment start
411
- const startTime = (editedSegment.start_time ?? 0) - 3
412
- onPlaySegment(startTime)
413
378
  }
414
379
 
415
380
  // Handle play/stop button click
@@ -434,6 +399,12 @@ export default function EditModal({
434
399
  maxWidth="md"
435
400
  fullWidth
436
401
  onKeyDown={handleKeyDown}
402
+ PaperProps={{
403
+ sx: {
404
+ height: '90vh', // Take up 90% of viewport height
405
+ margin: '5vh 0' // Add 5vh margin top and bottom
406
+ }
407
+ }}
437
408
  >
438
409
  <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
439
410
  <Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
@@ -456,8 +427,16 @@ export default function EditModal({
456
427
  <CloseIcon />
457
428
  </IconButton>
458
429
  </DialogTitle>
459
- <DialogContent dividers>
460
- <Box sx={{ mb: 2 }}>
430
+ <DialogContent
431
+ dividers
432
+ sx={{
433
+ display: 'flex',
434
+ flexDirection: 'column',
435
+ flexGrow: 1, // Allow content to fill available space
436
+ overflow: 'hidden' // Prevent double scrollbars
437
+ }}
438
+ >
439
+ <Box sx={{ mb: 0 }}>
461
440
  <TimelineEditor
462
441
  words={editedSegment.words}
463
442
  startTime={timeRange.start}
@@ -468,11 +447,11 @@ export default function EditModal({
468
447
  />
469
448
  </Box>
470
449
 
471
- <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
450
+ <Box sx={{ mb: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
472
451
  <Typography variant="body2" color="text.secondary">
473
- Original Time Range: {originalSegment.start_time?.toFixed(2) ?? 'N/A'} - {originalSegment.end_time?.toFixed(2) ?? 'N/A'}
452
+ Original Time Range: {originalSegment?.start_time?.toFixed(2) ?? 'N/A'} - {originalSegment?.end_time?.toFixed(2) ?? 'N/A'}
474
453
  <br />
475
- Current Time Range: {editedSegment.start_time?.toFixed(2) ?? 'N/A'} - {editedSegment.end_time?.toFixed(2) ?? 'N/A'}
454
+ Current Time Range: {editedSegment?.start_time?.toFixed(2) ?? 'N/A'} - {editedSegment?.end_time?.toFixed(2) ?? 'N/A'}
476
455
  </Typography>
477
456
 
478
457
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
@@ -486,69 +465,126 @@ export default function EditModal({
486
465
  {isManualSyncing ? "Cancel Sync" : "Manual Sync"}
487
466
  </Button>
488
467
  {isManualSyncing && (
489
- <Typography variant="body2">
490
- Press spacebar for word {syncWordIndex + 1} of {editedSegment?.words.length}
491
- </Typography>
468
+ <Box>
469
+ <Typography variant="body2">
470
+ Word {syncWordIndex + 1} of {editedSegment?.words.length}: <strong>{editedSegment?.words[syncWordIndex]?.text || ''}</strong>
471
+ </Typography>
472
+ <Typography variant="caption" color="text.secondary">
473
+ {isSpacebarPressed ?
474
+ "Holding spacebar... Release when word ends" :
475
+ "Press spacebar when word starts (tap for short words, hold for long words)"}
476
+ </Typography>
477
+ </Box>
492
478
  )}
493
479
  </Box>
494
480
  </Box>
495
481
 
496
- <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 3 }}>
482
+ <Box sx={{
483
+ display: 'flex',
484
+ flexDirection: 'column',
485
+ gap: 0.5,
486
+ mb: 3,
487
+ pt: 1,
488
+ flexGrow: 1,
489
+ overflowY: 'auto',
490
+ '&::-webkit-scrollbar': {
491
+ display: 'none' // Hide scrollbar for WebKit browsers (Chrome, Safari, etc.)
492
+ },
493
+ msOverflowStyle: 'none', // Hide scrollbar for IE and Edge
494
+ scrollbarWidth: 'none', // Hide scrollbar for Firefox
495
+ }}>
496
+ {/* Initial divider with Add Segment Before button */}
497
+ <WordDivider
498
+ onAddWord={() => handleAddWord(-1)}
499
+ onAddSegmentBefore={() => onAddSegment?.(segmentIndex)}
500
+ onMergeSegment={() => handleMergeSegment(false)}
501
+ isFirst={true}
502
+ sx={{ ml: 15 }}
503
+ />
504
+
497
505
  {editedSegment.words.map((word, index) => (
498
- <Box key={index} sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
499
- <TextField
500
- label={`Word ${index}`}
501
- value={word.text}
502
- onChange={(e) => handleWordChange(index, { text: e.target.value })}
503
- fullWidth
504
- size="small"
505
- />
506
- <TextField
507
- label="Start Time"
508
- value={word.start_time?.toFixed(2) ?? ''}
509
- onChange={(e) => handleWordChange(index, { start_time: parseFloat(e.target.value) })}
510
- type="number"
511
- inputProps={{ step: 0.01 }}
512
- sx={{ width: '150px' }}
513
- size="small"
514
- />
515
- <TextField
516
- label="End Time"
517
- value={word.end_time?.toFixed(2) ?? ''}
518
- onChange={(e) => handleWordChange(index, { end_time: parseFloat(e.target.value) })}
519
- type="number"
520
- inputProps={{ step: 0.01 }}
521
- sx={{ width: '150px' }}
522
- size="small"
506
+ <Box key={word.id}>
507
+ <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
508
+ <TextField
509
+ label={`Word ${index}`}
510
+ value={word.text}
511
+ onChange={(e) => handleWordChange(index, { text: e.target.value })}
512
+ fullWidth
513
+ size="small"
514
+ />
515
+ <TextField
516
+ label="Start Time"
517
+ value={word.start_time?.toFixed(2) ?? ''}
518
+ onChange={(e) => handleWordChange(index, { start_time: parseFloat(e.target.value) })}
519
+ type="number"
520
+ inputProps={{ step: 0.01 }}
521
+ sx={{ width: '150px' }}
522
+ size="small"
523
+ />
524
+ <TextField
525
+ label="End Time"
526
+ value={word.end_time?.toFixed(2) ?? ''}
527
+ onChange={(e) => handleWordChange(index, { end_time: parseFloat(e.target.value) })}
528
+ type="number"
529
+ inputProps={{ step: 0.01 }}
530
+ sx={{ width: '150px' }}
531
+ size="small"
532
+ />
533
+ <IconButton
534
+ onClick={() => handleSplitWord(index)}
535
+ title="Split Word"
536
+ sx={{ color: 'primary.main' }}
537
+ size="small"
538
+ >
539
+ <SplitIcon fontSize="small" />
540
+ </IconButton>
541
+ <IconButton
542
+ onClick={() => handleRemoveWord(index)}
543
+ disabled={editedSegment.words.length <= 1}
544
+ title="Remove Word"
545
+ sx={{ color: 'error.main' }}
546
+ size="small"
547
+ >
548
+ <DeleteIcon fontSize="small" />
549
+ </IconButton>
550
+ </Box>
551
+
552
+ {/* Update the WordDivider usage to include split segment */}
553
+ <WordDivider
554
+ onAddWord={() => handleAddWord(index)}
555
+ onMergeWords={() => handleMergeWords(index)}
556
+ onSplitSegment={() => handleSplitSegment(index)}
557
+ onAddSegmentAfter={
558
+ index === editedSegment.words.length - 1
559
+ ? () => onAddSegment?.(segmentIndex + 1)
560
+ : undefined
561
+ }
562
+ onMergeSegment={
563
+ index === editedSegment.words.length - 1
564
+ ? () => handleMergeSegment(true)
565
+ : undefined
566
+ }
567
+ canMerge={index < editedSegment.words.length - 1}
568
+ isLast={index === editedSegment.words.length - 1}
569
+ sx={{ ml: 15 }}
523
570
  />
524
- <IconButton
525
- onClick={() => handleRemoveWord(index)}
526
- disabled={editedSegment.words.length <= 1}
527
- sx={{ color: 'error.main' }}
528
- >
529
- <DeleteIcon fontSize="small" />
530
- </IconButton>
531
- <IconButton onClick={(e) => handleWordMenu(e, index)}>
532
- <MoreVertIcon />
533
- </IconButton>
534
571
  </Box>
535
572
  ))}
536
573
  </Box>
537
574
 
538
- <Box sx={{ display: 'flex', gap: 2 }}>
575
+ <Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
539
576
  <TextField
540
- label="Replace all words"
541
577
  value={replacementText}
542
578
  onChange={(e) => setReplacementText(e.target.value)}
543
- fullWidth
544
- placeholder="Type or paste replacement words here"
579
+ placeholder="Replace all words"
545
580
  size="small"
581
+ sx={{ flexGrow: 1, maxWidth: 'calc(100% - 140px)' }} // Reserve space for the button
546
582
  />
547
583
  <Button
548
- variant="contained"
549
- startIcon={<AutoFixHighIcon />}
550
584
  onClick={handleReplaceAllWords}
551
- disabled={!replacementText.trim()}
585
+ startIcon={<AutoFixHighIcon />}
586
+ size="small"
587
+ sx={{ whiteSpace: 'nowrap' }}
552
588
  >
553
589
  Replace All
554
590
  </Button>
@@ -562,14 +598,15 @@ export default function EditModal({
562
598
  >
563
599
  Reset
564
600
  </Button>
565
- <Box sx={{ mr: 'auto', display: 'flex', gap: 1 }}>
601
+ {originalTranscribedSegment && (
566
602
  <Button
567
- startIcon={<AddIcon />}
568
- onClick={() => segmentIndex !== null && onAddSegment?.(segmentIndex)}
569
- color="primary"
603
+ onClick={handleRevertToOriginal}
604
+ startIcon={<HistoryIcon />}
570
605
  >
571
- Add Segment Before
606
+ Un-Correct
572
607
  </Button>
608
+ )}
609
+ <Box sx={{ mr: 'auto' }}>
573
610
  <Button
574
611
  startIcon={<DeleteIcon />}
575
612
  onClick={handleDelete}
@@ -579,56 +616,14 @@ export default function EditModal({
579
616
  </Button>
580
617
  </Box>
581
618
  <Button onClick={handleClose}>Cancel</Button>
582
- <Button onClick={() => {
583
- cleanupManualSync()
584
- onSave(editedSegment)
585
- }}>
586
- Save Changes
619
+ <Button
620
+ onClick={handleSave}
621
+ variant="contained"
622
+ disabled={!editedSegment || editedSegment.words.length === 0}
623
+ >
624
+ Save
587
625
  </Button>
588
626
  </DialogActions>
589
-
590
- <Menu
591
- anchorEl={menuAnchorEl}
592
- open={Boolean(menuAnchorEl)}
593
- onClose={handleMenuClose}
594
- >
595
- <MenuItem onClick={() => {
596
- handleAddWord(selectedWordIndex!)
597
- handleMenuClose()
598
- }}>
599
- <AddIcon sx={{ mr: 1 }} /> Add Word After
600
- </MenuItem>
601
- <MenuItem onClick={() => {
602
- handleSplitWord(selectedWordIndex!)
603
- handleMenuClose()
604
- }}>
605
- <SplitIcon sx={{ mr: 1 }} /> Split Word
606
- </MenuItem>
607
- <MenuItem onClick={() => {
608
- handleSplitSegment(selectedWordIndex!)
609
- handleMenuClose()
610
- }}>
611
- <SplitIcon sx={{ mr: 1 }} /> Split Segment After Word
612
- </MenuItem>
613
- <MenuItem
614
- onClick={() => {
615
- handleMergeWords(selectedWordIndex!)
616
- handleMenuClose()
617
- }}
618
- disabled={selectedWordIndex === editedSegment.words.length - 1}
619
- >
620
- <MergeIcon sx={{ mr: 1 }} /> Merge with Next
621
- </MenuItem>
622
- <MenuItem
623
- onClick={() => {
624
- handleRemoveWord(selectedWordIndex!)
625
- handleMenuClose()
626
- }}
627
- disabled={editedSegment.words.length <= 1}
628
- >
629
- <DeleteIcon sx={{ mr: 1 }} color="error" /> Remove
630
- </MenuItem>
631
- </Menu>
632
627
  </Dialog>
633
628
  )
634
629
  }