lyrics-transcriber 0.41.0__py3-none-any.whl → 0.43.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 (78) hide show
  1. lyrics_transcriber/core/controller.py +30 -52
  2. lyrics_transcriber/correction/anchor_sequence.py +325 -150
  3. lyrics_transcriber/correction/corrector.py +224 -107
  4. lyrics_transcriber/correction/handlers/base.py +28 -10
  5. lyrics_transcriber/correction/handlers/extend_anchor.py +47 -24
  6. lyrics_transcriber/correction/handlers/levenshtein.py +75 -33
  7. lyrics_transcriber/correction/handlers/llm.py +290 -0
  8. lyrics_transcriber/correction/handlers/no_space_punct_match.py +81 -36
  9. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +46 -26
  10. lyrics_transcriber/correction/handlers/repeat.py +28 -11
  11. lyrics_transcriber/correction/handlers/sound_alike.py +68 -32
  12. lyrics_transcriber/correction/handlers/syllables_match.py +80 -30
  13. lyrics_transcriber/correction/handlers/word_count_match.py +36 -19
  14. lyrics_transcriber/correction/handlers/word_operations.py +68 -22
  15. lyrics_transcriber/correction/text_utils.py +3 -7
  16. lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
  17. lyrics_transcriber/frontend/.yarn/releases/yarn-4.6.0.cjs +934 -0
  18. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  19. lyrics_transcriber/frontend/dist/assets/{index-DKnNJHRK.js → index-D0Gr3Ep7.js} +16509 -9038
  20. lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +1 -0
  21. lyrics_transcriber/frontend/dist/index.html +1 -1
  22. lyrics_transcriber/frontend/package.json +6 -2
  23. lyrics_transcriber/frontend/src/App.tsx +18 -2
  24. lyrics_transcriber/frontend/src/api.ts +103 -6
  25. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +14 -6
  26. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +86 -59
  27. lyrics_transcriber/frontend/src/components/EditModal.tsx +281 -63
  28. lyrics_transcriber/frontend/src/components/FileUpload.tsx +2 -2
  29. lyrics_transcriber/frontend/src/components/Header.tsx +249 -0
  30. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +320 -266
  31. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +120 -0
  32. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +174 -52
  33. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +158 -114
  34. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +59 -78
  35. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +39 -16
  36. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +4 -10
  37. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +134 -68
  38. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +1 -1
  39. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +85 -115
  40. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  41. lyrics_transcriber/frontend/src/components/shared/types.ts +15 -7
  42. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +67 -0
  43. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  44. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +7 -7
  45. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +121 -0
  46. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  47. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  48. lyrics_transcriber/frontend/src/types.js +2 -0
  49. lyrics_transcriber/frontend/src/types.ts +70 -49
  50. lyrics_transcriber/frontend/src/validation.ts +132 -0
  51. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  52. lyrics_transcriber/frontend/yarn.lock +3752 -0
  53. lyrics_transcriber/lyrics/base_lyrics_provider.py +75 -12
  54. lyrics_transcriber/lyrics/file_provider.py +6 -5
  55. lyrics_transcriber/lyrics/genius.py +5 -2
  56. lyrics_transcriber/lyrics/spotify.py +58 -21
  57. lyrics_transcriber/output/ass/config.py +16 -5
  58. lyrics_transcriber/output/cdg.py +1 -1
  59. lyrics_transcriber/output/generator.py +22 -8
  60. lyrics_transcriber/output/plain_text.py +15 -10
  61. lyrics_transcriber/output/segment_resizer.py +16 -3
  62. lyrics_transcriber/output/subtitles.py +27 -1
  63. lyrics_transcriber/output/video.py +107 -1
  64. lyrics_transcriber/review/__init__.py +0 -1
  65. lyrics_transcriber/review/server.py +337 -164
  66. lyrics_transcriber/transcribers/audioshake.py +3 -0
  67. lyrics_transcriber/transcribers/base_transcriber.py +11 -3
  68. lyrics_transcriber/transcribers/whisper.py +11 -1
  69. lyrics_transcriber/types.py +151 -105
  70. lyrics_transcriber/utils/word_utils.py +27 -0
  71. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/METADATA +3 -1
  72. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/RECORD +75 -61
  73. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/WHEEL +1 -1
  74. lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js.map +0 -1
  75. lyrics_transcriber/frontend/package-lock.json +0 -4260
  76. lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +0 -202
  77. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/LICENSE +0 -0
  78. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/entry_points.txt +0 -0
@@ -20,8 +20,10 @@ import RestoreIcon from '@mui/icons-material/RestoreFromTrash'
20
20
  import MoreVertIcon from '@mui/icons-material/MoreVert'
21
21
  import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
22
22
  import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
23
+ import CancelIcon from '@mui/icons-material/Cancel'
24
+ import StopIcon from '@mui/icons-material/Stop'
23
25
  import { LyricsSegment, Word } from '../types'
24
- import { useState, useEffect } from 'react'
26
+ import { useState, useEffect, useCallback } from 'react'
25
27
  import TimelineEditor from './TimelineEditor'
26
28
  import { nanoid } from 'nanoid'
27
29
 
@@ -35,6 +37,9 @@ interface EditModalProps {
35
37
  onPlaySegment?: (startTime: number) => void
36
38
  currentTime?: number
37
39
  onDelete?: (segmentIndex: number) => void
40
+ onAddSegment?: (segmentIndex: number) => void
41
+ onSplitSegment?: (segmentIndex: number, afterWordIndex: number) => void
42
+ setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
38
43
  }
39
44
 
40
45
  export default function EditModal({
@@ -47,43 +52,166 @@ export default function EditModal({
47
52
  onPlaySegment,
48
53
  currentTime = 0,
49
54
  onDelete,
55
+ onAddSegment,
56
+ onSplitSegment,
57
+ setModalSpacebarHandler,
50
58
  }: EditModalProps) {
59
+ // All useState hooks
51
60
  const [editedSegment, setEditedSegment] = useState<LyricsSegment | null>(segment)
52
61
  const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null)
53
62
  const [selectedWordIndex, setSelectedWordIndex] = useState<number | null>(null)
54
63
  const [replacementText, setReplacementText] = useState('')
64
+ const [isManualSyncing, setIsManualSyncing] = useState(false)
65
+ const [syncWordIndex, setSyncWordIndex] = useState<number>(-1)
66
+ const [isPlaying, setIsPlaying] = useState(false)
55
67
 
56
- // Reset edited segment when modal opens with new segment
68
+ // Define updateSegment first since other hooks depend on it
69
+ const updateSegment = useCallback((newWords: Word[]) => {
70
+ if (!editedSegment) return;
71
+
72
+ const validStartTimes = newWords.map(w => w.start_time).filter((t): t is number => t !== null)
73
+ const validEndTimes = newWords.map(w => w.end_time).filter((t): t is number => t !== null)
74
+
75
+ const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null
76
+ const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null
77
+
78
+ setEditedSegment({
79
+ ...editedSegment,
80
+ words: newWords,
81
+ text: newWords.map(w => w.text).join(' '),
82
+ start_time: segmentStartTime,
83
+ end_time: segmentEndTime
84
+ })
85
+ }, [editedSegment])
86
+
87
+ // Other useCallback hooks
88
+ const cleanupManualSync = useCallback(() => {
89
+ setIsManualSyncing(false)
90
+ setSyncWordIndex(-1)
91
+ }, [])
92
+
93
+ const handleClose = useCallback(() => {
94
+ cleanupManualSync()
95
+ onClose()
96
+ }, [onClose, cleanupManualSync])
97
+
98
+ // All useEffect hooks
57
99
  useEffect(() => {
58
100
  setEditedSegment(segment)
59
101
  }, [segment])
60
102
 
103
+ // Update the spacebar handler when modal state changes
104
+ useEffect(() => {
105
+ 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
+ }
146
+ })
147
+ } else {
148
+ setModalSpacebarHandler(undefined)
149
+ }
150
+
151
+ return () => {
152
+ setModalSpacebarHandler(undefined)
153
+ }
154
+ }, [
155
+ open,
156
+ isManualSyncing,
157
+ editedSegment,
158
+ syncWordIndex,
159
+ currentTime,
160
+ onPlaySegment,
161
+ updateSegment,
162
+ setModalSpacebarHandler
163
+ ])
164
+
165
+ // Auto-stop sync if we go past the end time
166
+ useEffect(() => {
167
+ if (!editedSegment) return
168
+
169
+ const endTime = editedSegment.end_time ?? 0
170
+
171
+ if (window.isAudioPlaying && currentTime > endTime) {
172
+ console.log('Stopping playback: current time exceeded end time')
173
+ window.toggleAudioPlayback?.()
174
+ setIsManualSyncing(false)
175
+ setSyncWordIndex(-1)
176
+ }
177
+
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])
191
+
192
+ // Add a function to get safe time values
193
+ const getSafeTimeRange = (segment: LyricsSegment | null) => {
194
+ if (!segment) return { start: 0, end: 1 }; // Default 1-second range
195
+ const start = segment.start_time ?? 0;
196
+ const end = segment.end_time ?? (start + 1);
197
+ return { start, end };
198
+ }
199
+
200
+ // Early return after all hooks and function definitions
61
201
  if (!segment || segmentIndex === null || !editedSegment || !originalSegment) return null
62
202
 
63
- const handleWordChange = (index: number, field: keyof Word, value: string | number) => {
203
+ // Get safe time values for TimelineEditor
204
+ const timeRange = getSafeTimeRange(editedSegment)
205
+
206
+ const handleWordChange = (index: number, updates: Partial<Word>) => {
64
207
  const newWords = [...editedSegment.words]
65
208
  newWords[index] = {
66
209
  ...newWords[index],
67
- [field]: field === 'start_time' || field === 'end_time'
68
- ? parseFloat(Number(value).toFixed(4))
69
- : value
210
+ ...updates
70
211
  }
71
212
  updateSegment(newWords)
72
213
  }
73
214
 
74
- const updateSegment = (newWords: Word[]) => {
75
- const segmentStartTime = Math.min(...newWords.map(w => w.start_time))
76
- const segmentEndTime = Math.max(...newWords.map(w => w.end_time))
77
-
78
- setEditedSegment({
79
- ...editedSegment,
80
- words: newWords,
81
- text: newWords.map(w => w.text).join(' '),
82
- start_time: segmentStartTime,
83
- end_time: segmentEndTime
84
- })
85
- }
86
-
87
215
  const handleAddWord = (index?: number) => {
88
216
  const newWords = [...editedSegment.words]
89
217
  let newWord: Word
@@ -91,11 +219,12 @@ export default function EditModal({
91
219
  if (index === undefined) {
92
220
  // Add at end
93
221
  const lastWord = newWords[newWords.length - 1]
222
+ const lastEndTime = lastWord.end_time ?? 0
94
223
  newWord = {
95
224
  id: nanoid(),
96
225
  text: '',
97
- start_time: lastWord.end_time,
98
- end_time: lastWord.end_time + 0.5,
226
+ start_time: lastEndTime,
227
+ end_time: lastEndTime + 0.5,
99
228
  confidence: 1.0
100
229
  }
101
230
  newWords.push(newWord)
@@ -104,8 +233,11 @@ export default function EditModal({
104
233
  const prevWord = newWords[index]
105
234
  const nextWord = newWords[index + 1]
106
235
  const midTime = prevWord ?
107
- (nextWord ? (prevWord.end_time + nextWord.start_time) / 2 : prevWord.end_time + 0.5) :
108
- (nextWord ? nextWord.start_time - 0.5 : 0)
236
+ (nextWord ?
237
+ ((prevWord.end_time ?? 0) + (nextWord.start_time ?? 0)) / 2 :
238
+ (prevWord.end_time ?? 0) + 0.5
239
+ ) :
240
+ (nextWord ? (nextWord.start_time ?? 0) - 0.5 : 0)
109
241
 
110
242
  newWord = {
111
243
  id: nanoid(),
@@ -122,7 +254,9 @@ export default function EditModal({
122
254
 
123
255
  const handleSplitWord = (index: number) => {
124
256
  const word = editedSegment.words[index]
125
- const midTime = (word.start_time + word.end_time) / 2
257
+ const startTime = word.start_time ?? 0
258
+ const endTime = word.end_time ?? startTime + 0.5
259
+ const midTime = (startTime + endTime) / 2
126
260
  const words = word.text.split(/\s+/)
127
261
 
128
262
  if (words.length <= 1) {
@@ -138,7 +272,7 @@ export default function EditModal({
138
272
  {
139
273
  id: nanoid(),
140
274
  text: words[0],
141
- start_time: word.start_time,
275
+ start_time: startTime,
142
276
  end_time: midTime,
143
277
  confidence: 1.0
144
278
  },
@@ -146,7 +280,7 @@ export default function EditModal({
146
280
  id: nanoid(),
147
281
  text: words[1],
148
282
  start_time: midTime,
149
- end_time: word.end_time,
283
+ end_time: endTime,
150
284
  confidence: 1.0
151
285
  }
152
286
  )
@@ -164,8 +298,8 @@ export default function EditModal({
164
298
  newWords.splice(index, 2, {
165
299
  id: nanoid(),
166
300
  text: `${word1.text} ${word2.text}`.trim(),
167
- start_time: word1.start_time,
168
- end_time: word2.end_time,
301
+ start_time: word1.start_time ?? null,
302
+ end_time: word2.end_time ?? null,
169
303
  confidence: 1.0
170
304
  })
171
305
 
@@ -198,7 +332,7 @@ export default function EditModal({
198
332
  originalText: segment?.text,
199
333
  editedText: editedSegment.text,
200
334
  wordCount: editedSegment.words.length,
201
- timeRange: `${editedSegment.start_time.toFixed(4)} - ${editedSegment.end_time.toFixed(4)}`
335
+ timeRange: `${editedSegment.start_time?.toFixed(4) ?? 'N/A'} - ${editedSegment.end_time?.toFixed(4) ?? 'N/A'}`
202
336
  })
203
337
  onSave(editedSegment)
204
338
  onClose()
@@ -209,7 +343,9 @@ export default function EditModal({
209
343
  if (!editedSegment) return
210
344
 
211
345
  const newWords = replacementText.trim().split(/\s+/)
212
- const segmentDuration = editedSegment.end_time - editedSegment.start_time
346
+ const startTime = editedSegment.start_time ?? 0
347
+ const endTime = editedSegment.end_time ?? (startTime + newWords.length) // Default to 1 second per word
348
+ const segmentDuration = endTime - startTime
213
349
 
214
350
  let updatedWords: Word[]
215
351
 
@@ -228,8 +364,8 @@ export default function EditModal({
228
364
  updatedWords = newWords.map((text, index) => ({
229
365
  id: nanoid(), // Generate new ID
230
366
  text,
231
- start_time: editedSegment.start_time + (index * avgWordDuration),
232
- end_time: editedSegment.start_time + ((index + 1) * avgWordDuration),
367
+ start_time: startTime + (index * avgWordDuration),
368
+ end_time: startTime + ((index + 1) * avgWordDuration),
233
369
  confidence: 1.0
234
370
  }))
235
371
  }
@@ -252,10 +388,49 @@ export default function EditModal({
252
388
  }
253
389
  }
254
390
 
391
+ const handleSplitSegment = (wordIndex: number) => {
392
+ if (segmentIndex !== null && editedSegment) {
393
+ handleSave() // Save current changes first
394
+ onSplitSegment?.(segmentIndex, wordIndex)
395
+ }
396
+ }
397
+
398
+ // Add this new function to handle manual sync
399
+ const startManualSync = () => {
400
+ if (isManualSyncing) {
401
+ setIsManualSyncing(false)
402
+ setSyncWordIndex(-1)
403
+ return
404
+ }
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
+ }
414
+
415
+ // Handle play/stop button click
416
+ const handlePlayButtonClick = () => {
417
+ if (!segment?.start_time || !onPlaySegment) return
418
+
419
+ if (isPlaying) {
420
+ // Stop playback
421
+ if (window.toggleAudioPlayback) {
422
+ window.toggleAudioPlayback()
423
+ }
424
+ } else {
425
+ // Start playback
426
+ onPlaySegment(segment.start_time)
427
+ }
428
+ }
429
+
255
430
  return (
256
431
  <Dialog
257
432
  open={open}
258
- onClose={onClose}
433
+ onClose={handleClose}
259
434
  maxWidth="md"
260
435
  fullWidth
261
436
  onKeyDown={handleKeyDown}
@@ -263,13 +438,17 @@ export default function EditModal({
263
438
  <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
264
439
  <Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
265
440
  Edit Segment {segmentIndex}
266
- {segment?.start_time !== undefined && onPlaySegment && (
441
+ {segment?.start_time !== null && onPlaySegment && (
267
442
  <IconButton
268
443
  size="small"
269
- onClick={() => onPlaySegment(segment.start_time)}
444
+ onClick={handlePlayButtonClick}
270
445
  sx={{ padding: '4px' }}
271
446
  >
272
- <PlayCircleOutlineIcon />
447
+ {isPlaying ? (
448
+ <StopIcon />
449
+ ) : (
450
+ <PlayCircleOutlineIcon />
451
+ )}
273
452
  </IconButton>
274
453
  )}
275
454
  </Box>
@@ -281,23 +460,38 @@ export default function EditModal({
281
460
  <Box sx={{ mb: 2 }}>
282
461
  <TimelineEditor
283
462
  words={editedSegment.words}
284
- startTime={editedSegment.start_time}
285
- endTime={editedSegment.end_time}
286
- onWordUpdate={(index, updates) => {
287
- const newWords = [...editedSegment.words]
288
- newWords[index] = { ...newWords[index], ...updates }
289
- updateSegment(newWords)
290
- }}
463
+ startTime={timeRange.start}
464
+ endTime={timeRange.end}
465
+ onWordUpdate={handleWordChange}
291
466
  currentTime={currentTime}
292
467
  onPlaySegment={onPlaySegment}
293
468
  />
294
469
  </Box>
295
470
 
296
- <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
297
- Original Time Range: {originalSegment.start_time.toFixed(2)} - {originalSegment.end_time.toFixed(2)}
298
- <br />
299
- Current Time Range: {editedSegment.start_time.toFixed(2)} - {editedSegment.end_time.toFixed(2)}
300
- </Typography>
471
+ <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
472
+ <Typography variant="body2" color="text.secondary">
473
+ Original Time Range: {originalSegment.start_time?.toFixed(2) ?? 'N/A'} - {originalSegment.end_time?.toFixed(2) ?? 'N/A'}
474
+ <br />
475
+ Current Time Range: {editedSegment.start_time?.toFixed(2) ?? 'N/A'} - {editedSegment.end_time?.toFixed(2) ?? 'N/A'}
476
+ </Typography>
477
+
478
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
479
+ <Button
480
+ variant={isManualSyncing ? "outlined" : "contained"}
481
+ onClick={startManualSync}
482
+ disabled={!onPlaySegment}
483
+ startIcon={isManualSyncing ? <CancelIcon /> : <PlayCircleOutlineIcon />}
484
+ color={isManualSyncing ? "error" : "primary"}
485
+ >
486
+ {isManualSyncing ? "Cancel Sync" : "Manual Sync"}
487
+ </Button>
488
+ {isManualSyncing && (
489
+ <Typography variant="body2">
490
+ Press spacebar for word {syncWordIndex + 1} of {editedSegment?.words.length}
491
+ </Typography>
492
+ )}
493
+ </Box>
494
+ </Box>
301
495
 
302
496
  <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 3 }}>
303
497
  {editedSegment.words.map((word, index) => (
@@ -305,14 +499,14 @@ export default function EditModal({
305
499
  <TextField
306
500
  label={`Word ${index}`}
307
501
  value={word.text}
308
- onChange={(e) => handleWordChange(index, 'text', e.target.value)}
502
+ onChange={(e) => handleWordChange(index, { text: e.target.value })}
309
503
  fullWidth
310
504
  size="small"
311
505
  />
312
506
  <TextField
313
507
  label="Start Time"
314
- value={word.start_time.toFixed(2)}
315
- onChange={(e) => handleWordChange(index, 'start_time', parseFloat(e.target.value))}
508
+ value={word.start_time?.toFixed(2) ?? ''}
509
+ onChange={(e) => handleWordChange(index, { start_time: parseFloat(e.target.value) })}
316
510
  type="number"
317
511
  inputProps={{ step: 0.01 }}
318
512
  sx={{ width: '150px' }}
@@ -320,13 +514,20 @@ export default function EditModal({
320
514
  />
321
515
  <TextField
322
516
  label="End Time"
323
- value={word.end_time.toFixed(2)}
324
- onChange={(e) => handleWordChange(index, 'end_time', parseFloat(e.target.value))}
517
+ value={word.end_time?.toFixed(2) ?? ''}
518
+ onChange={(e) => handleWordChange(index, { end_time: parseFloat(e.target.value) })}
325
519
  type="number"
326
520
  inputProps={{ step: 0.01 }}
327
521
  sx={{ width: '150px' }}
328
522
  size="small"
329
523
  />
524
+ <IconButton
525
+ onClick={() => handleRemoveWord(index)}
526
+ disabled={editedSegment.words.length <= 1}
527
+ sx={{ color: 'error.main' }}
528
+ >
529
+ <DeleteIcon fontSize="small" />
530
+ </IconButton>
330
531
  <IconButton onClick={(e) => handleWordMenu(e, index)}>
331
532
  <MoreVertIcon />
332
533
  </IconButton>
@@ -361,16 +562,27 @@ export default function EditModal({
361
562
  >
362
563
  Reset
363
564
  </Button>
364
- <Button
365
- startIcon={<DeleteIcon />}
366
- onClick={handleDelete}
367
- color="error"
368
- sx={{ mr: 'auto' }}
369
- >
370
- Delete Segment
371
- </Button>
372
- <Button onClick={onClose}>Cancel</Button>
373
- <Button onClick={handleSave} variant="contained">
565
+ <Box sx={{ mr: 'auto', display: 'flex', gap: 1 }}>
566
+ <Button
567
+ startIcon={<AddIcon />}
568
+ onClick={() => segmentIndex !== null && onAddSegment?.(segmentIndex)}
569
+ color="primary"
570
+ >
571
+ Add Segment Before
572
+ </Button>
573
+ <Button
574
+ startIcon={<DeleteIcon />}
575
+ onClick={handleDelete}
576
+ color="error"
577
+ >
578
+ Delete Segment
579
+ </Button>
580
+ </Box>
581
+ <Button onClick={handleClose}>Cancel</Button>
582
+ <Button onClick={() => {
583
+ cleanupManualSync()
584
+ onSave(editedSegment)
585
+ }}>
374
586
  Save Changes
375
587
  </Button>
376
588
  </DialogActions>
@@ -392,6 +604,12 @@ export default function EditModal({
392
604
  }}>
393
605
  <SplitIcon sx={{ mr: 1 }} /> Split Word
394
606
  </MenuItem>
607
+ <MenuItem onClick={() => {
608
+ handleSplitSegment(selectedWordIndex!)
609
+ handleMenuClose()
610
+ }}>
611
+ <SplitIcon sx={{ mr: 1 }} /> Split Segment After Word
612
+ </MenuItem>
395
613
  <MenuItem
396
614
  onClick={() => {
397
615
  handleMergeWords(selectedWordIndex!)
@@ -1,10 +1,10 @@
1
1
  import { ChangeEvent, DragEvent, useState } from 'react'
2
2
  import { Paper, Typography } from '@mui/material'
3
3
  import CloudUploadIcon from '@mui/icons-material/CloudUpload'
4
- import { LyricsData } from '../types'
4
+ import { CorrectionData } from '../types'
5
5
 
6
6
  interface FileUploadProps {
7
- onUpload: (data: LyricsData) => void
7
+ onUpload: (data: CorrectionData) => void
8
8
  }
9
9
 
10
10
  export default function FileUpload({ onUpload }: FileUploadProps) {