lyrics-transcriber 0.43.1__py3-none-any.whl → 0.45.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 (51) 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-ZCT0s9MG.js} +10174 -6197
  8. lyrics_transcriber/frontend/dist/assets/index-ZCT0s9MG.js.map +1 -0
  9. lyrics_transcriber/frontend/dist/index.html +1 -1
  10. lyrics_transcriber/frontend/src/App.tsx +5 -5
  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/EditActionBar.tsx +68 -0
  16. lyrics_transcriber/frontend/src/components/EditModal.tsx +467 -399
  17. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +373 -0
  18. lyrics_transcriber/frontend/src/components/EditWordList.tsx +308 -0
  19. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  20. lyrics_transcriber/frontend/src/components/Header.tsx +141 -101
  21. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +569 -107
  22. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +22 -13
  23. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +1 -0
  24. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +29 -12
  25. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +21 -4
  26. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +29 -15
  27. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +36 -18
  28. lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
  29. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +89 -41
  30. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +9 -2
  31. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +27 -3
  32. lyrics_transcriber/frontend/src/components/shared/types.ts +17 -2
  33. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +90 -19
  34. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +192 -0
  35. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +267 -0
  36. lyrics_transcriber/frontend/src/main.tsx +7 -1
  37. lyrics_transcriber/frontend/src/theme.ts +177 -0
  38. lyrics_transcriber/frontend/src/types.ts +1 -1
  39. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  40. lyrics_transcriber/lyrics/base_lyrics_provider.py +2 -2
  41. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  42. lyrics_transcriber/output/generator.py +40 -12
  43. lyrics_transcriber/review/server.py +238 -8
  44. {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/METADATA +3 -2
  45. {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/RECORD +48 -40
  46. lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +0 -1
  47. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +0 -252
  48. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +0 -110
  49. {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/LICENSE +0 -0
  50. {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/WHEEL +0 -0
  51. {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/entry_points.txt +0 -0
@@ -5,27 +5,148 @@ import {
5
5
  DialogActions,
6
6
  IconButton,
7
7
  Box,
8
- TextField,
9
- Button,
10
- Typography,
11
- Menu,
12
- MenuItem,
8
+ CircularProgress,
9
+ Typography
13
10
  } from '@mui/material'
14
11
  import CloseIcon from '@mui/icons-material/Close'
15
- import AddIcon from '@mui/icons-material/Add'
16
- import DeleteIcon from '@mui/icons-material/Delete'
17
- import MergeIcon from '@mui/icons-material/CallMerge'
18
- import SplitIcon from '@mui/icons-material/CallSplit'
19
- import RestoreIcon from '@mui/icons-material/RestoreFromTrash'
20
- import MoreVertIcon from '@mui/icons-material/MoreVert'
21
- import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
22
12
  import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
23
- import CancelIcon from '@mui/icons-material/Cancel'
24
13
  import StopIcon from '@mui/icons-material/Stop'
25
14
  import { LyricsSegment, Word } from '../types'
26
- import { useState, useEffect, useCallback } from 'react'
27
- import TimelineEditor from './TimelineEditor'
15
+ import { useState, useEffect, useCallback, useMemo, memo } from 'react'
28
16
  import { nanoid } from 'nanoid'
17
+ import useManualSync from '../hooks/useManualSync'
18
+ import EditTimelineSection from './EditTimelineSection'
19
+ import EditWordList from './EditWordList'
20
+ import EditActionBar from './EditActionBar'
21
+
22
+ // Extract TimelineSection into a separate memoized component
23
+ interface TimelineSectionProps {
24
+ words: Word[]
25
+ timeRange: { start: number, end: number }
26
+ originalSegment: LyricsSegment
27
+ editedSegment: LyricsSegment
28
+ currentTime: number
29
+ isManualSyncing: boolean
30
+ syncWordIndex: number
31
+ isSpacebarPressed: boolean
32
+ onWordUpdate: (index: number, updates: Partial<Word>) => void
33
+ onPlaySegment?: (startTime: number) => void
34
+ startManualSync: () => void
35
+ isGlobal: boolean
36
+ }
37
+
38
+ const MemoizedTimelineSection = memo(function TimelineSection({
39
+ words,
40
+ timeRange,
41
+ originalSegment,
42
+ editedSegment,
43
+ currentTime,
44
+ isManualSyncing,
45
+ syncWordIndex,
46
+ isSpacebarPressed,
47
+ onWordUpdate,
48
+ onPlaySegment,
49
+ startManualSync,
50
+ isGlobal
51
+ }: TimelineSectionProps) {
52
+ return (
53
+ <EditTimelineSection
54
+ words={words}
55
+ startTime={timeRange.start}
56
+ endTime={timeRange.end}
57
+ originalStartTime={originalSegment.start_time}
58
+ originalEndTime={originalSegment.end_time}
59
+ currentStartTime={editedSegment.start_time}
60
+ currentEndTime={editedSegment.end_time}
61
+ currentTime={currentTime}
62
+ isManualSyncing={isManualSyncing}
63
+ syncWordIndex={syncWordIndex}
64
+ isSpacebarPressed={isSpacebarPressed}
65
+ onWordUpdate={onWordUpdate}
66
+ onPlaySegment={onPlaySegment}
67
+ startManualSync={startManualSync}
68
+ isGlobal={isGlobal}
69
+ />
70
+ )
71
+ })
72
+
73
+ // Extract WordList into a separate memoized component
74
+ interface WordListProps {
75
+ words: Word[]
76
+ onWordUpdate: (index: number, updates: Partial<Word>) => void
77
+ onSplitWord: (index: number) => void
78
+ onMergeWords: (index: number) => void
79
+ onAddWord: (index?: number) => void
80
+ onRemoveWord: (index: number) => void
81
+ onSplitSegment?: (wordIndex: number) => void
82
+ onAddSegment?: (beforeIndex: number) => void
83
+ onMergeSegment?: (mergeWithNext: boolean) => void
84
+ isGlobal: boolean
85
+ }
86
+
87
+ const MemoizedWordList = memo(function WordList({
88
+ words,
89
+ onWordUpdate,
90
+ onSplitWord,
91
+ onMergeWords,
92
+ onAddWord,
93
+ onRemoveWord,
94
+ onSplitSegment,
95
+ onAddSegment,
96
+ onMergeSegment,
97
+ isGlobal
98
+ }: WordListProps) {
99
+ return (
100
+ <EditWordList
101
+ words={words}
102
+ onWordUpdate={onWordUpdate}
103
+ onSplitWord={onSplitWord}
104
+ onMergeWords={onMergeWords}
105
+ onAddWord={onAddWord}
106
+ onRemoveWord={onRemoveWord}
107
+ onSplitSegment={onSplitSegment}
108
+ onAddSegment={onAddSegment}
109
+ onMergeSegment={onMergeSegment}
110
+ isGlobal={isGlobal}
111
+ />
112
+ )
113
+ })
114
+
115
+ // Extract ActionBar into a separate memoized component
116
+ interface ActionBarProps {
117
+ onReset: () => void
118
+ onRevertToOriginal?: () => void
119
+ onDelete?: () => void
120
+ onClose: () => void
121
+ onSave: () => void
122
+ editedSegment: LyricsSegment | null
123
+ originalTranscribedSegment?: LyricsSegment | null
124
+ isGlobal: boolean
125
+ }
126
+
127
+ const MemoizedActionBar = memo(function ActionBar({
128
+ onReset,
129
+ onRevertToOriginal,
130
+ onDelete,
131
+ onClose,
132
+ onSave,
133
+ editedSegment,
134
+ originalTranscribedSegment,
135
+ isGlobal
136
+ }: ActionBarProps) {
137
+ return (
138
+ <EditActionBar
139
+ onReset={onReset}
140
+ onRevertToOriginal={onRevertToOriginal}
141
+ onDelete={onDelete}
142
+ onClose={onClose}
143
+ onSave={onSave}
144
+ editedSegment={editedSegment}
145
+ originalTranscribedSegment={originalTranscribedSegment}
146
+ isGlobal={isGlobal}
147
+ />
148
+ )
149
+ })
29
150
 
30
151
  interface EditModalProps {
31
152
  open: boolean
@@ -39,7 +160,11 @@ interface EditModalProps {
39
160
  onDelete?: (segmentIndex: number) => void
40
161
  onAddSegment?: (segmentIndex: number) => void
41
162
  onSplitSegment?: (segmentIndex: number, afterWordIndex: number) => void
163
+ onMergeSegment?: (segmentIndex: number, mergeWithNext: boolean) => void
42
164
  setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
165
+ originalTranscribedSegment?: LyricsSegment | null
166
+ isGlobal?: boolean
167
+ isLoading?: boolean
43
168
  }
44
169
 
45
170
  export default function EditModal({
@@ -54,18 +179,26 @@ export default function EditModal({
54
179
  onDelete,
55
180
  onAddSegment,
56
181
  onSplitSegment,
182
+ onMergeSegment,
57
183
  setModalSpacebarHandler,
184
+ originalTranscribedSegment,
185
+ isGlobal = false,
186
+ isLoading = false
58
187
  }: EditModalProps) {
59
- // All useState hooks
188
+ console.log('EditModal - Render', {
189
+ open,
190
+ isGlobal,
191
+ isLoading,
192
+ hasSegment: !!segment,
193
+ segmentIndex,
194
+ hasOriginalSegment: !!originalSegment,
195
+ hasOriginalTranscribedSegment: !!originalTranscribedSegment
196
+ });
197
+
60
198
  const [editedSegment, setEditedSegment] = useState<LyricsSegment | null>(segment)
61
- const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null)
62
- const [selectedWordIndex, setSelectedWordIndex] = useState<number | null>(null)
63
- const [replacementText, setReplacementText] = useState('')
64
- const [isManualSyncing, setIsManualSyncing] = useState(false)
65
- const [syncWordIndex, setSyncWordIndex] = useState<number>(-1)
66
199
  const [isPlaying, setIsPlaying] = useState(false)
67
200
 
68
- // Define updateSegment first since other hooks depend on it
201
+ // Define updateSegment first since the hook depends on it
69
202
  const updateSegment = useCallback((newWords: Word[]) => {
70
203
  if (!editedSegment) return;
71
204
 
@@ -84,84 +217,86 @@ export default function EditModal({
84
217
  })
85
218
  }, [editedSegment])
86
219
 
87
- // Other useCallback hooks
88
- const cleanupManualSync = useCallback(() => {
89
- setIsManualSyncing(false)
90
- setSyncWordIndex(-1)
91
- }, [])
220
+ // Use the manual sync hook
221
+ const {
222
+ isManualSyncing,
223
+ syncWordIndex,
224
+ startManualSync,
225
+ cleanupManualSync,
226
+ handleSpacebar,
227
+ isSpacebarPressed
228
+ } = useManualSync({
229
+ editedSegment,
230
+ currentTime,
231
+ onPlaySegment,
232
+ updateSegment
233
+ })
92
234
 
93
235
  const handleClose = useCallback(() => {
236
+ console.log('EditModal - handleClose called');
94
237
  cleanupManualSync()
95
238
  onClose()
96
239
  }, [onClose, cleanupManualSync])
97
240
 
98
- // All useEffect hooks
99
- useEffect(() => {
100
- setEditedSegment(segment)
101
- }, [segment])
102
-
103
241
  // Update the spacebar handler when modal state changes
104
242
  useEffect(() => {
243
+ const spacebarHandler = handleSpacebar // Capture the current handler
244
+
105
245
  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
- }
246
+ console.log('EditModal - Setting up modal spacebar handler', {
247
+ hasPlaySegment: !!onPlaySegment,
248
+ editedSegmentId: editedSegment?.id,
249
+ handlerFunction: spacebarHandler.toString().slice(0, 100),
250
+ isLoading
146
251
  })
147
- } else {
148
- setModalSpacebarHandler(undefined)
149
- }
150
252
 
151
- return () => {
152
- setModalSpacebarHandler(undefined)
253
+ // Create a function that will be called by the global event listeners
254
+ const handleKeyEvent = (e: KeyboardEvent) => {
255
+ if (e.code === 'Space') {
256
+ spacebarHandler(e)
257
+ }
258
+ }
259
+
260
+ setModalSpacebarHandler(() => handleKeyEvent)
261
+
262
+ // Only cleanup when the effect is re-run or the modal is closed
263
+ return () => {
264
+ if (!open) {
265
+ console.log('EditModal - Cleanup: clearing modal spacebar handler')
266
+ setModalSpacebarHandler(undefined)
267
+ }
268
+ }
153
269
  }
154
270
  }, [
155
271
  open,
156
- isManualSyncing,
157
- editedSegment,
158
- syncWordIndex,
159
- currentTime,
272
+ handleSpacebar,
273
+ setModalSpacebarHandler,
274
+ editedSegment?.id,
160
275
  onPlaySegment,
161
- updateSegment,
162
- setModalSpacebarHandler
276
+ isLoading
163
277
  ])
164
278
 
279
+ // Update isPlaying when currentTime changes
280
+ useEffect(() => {
281
+ if (editedSegment) {
282
+ const startTime = editedSegment.start_time ?? 0
283
+ const endTime = editedSegment.end_time ?? 0
284
+ const isWithinSegment = currentTime >= startTime && currentTime <= endTime
285
+
286
+ setIsPlaying(isWithinSegment && window.isAudioPlaying === true)
287
+ }
288
+ }, [currentTime, editedSegment])
289
+
290
+ // All useEffect hooks
291
+ useEffect(() => {
292
+ console.log('EditModal - segment changed', {
293
+ hasSegment: !!segment,
294
+ segmentId: segment?.id,
295
+ wordCount: segment?.words.length
296
+ });
297
+ setEditedSegment(segment)
298
+ }, [segment])
299
+
165
300
  // Auto-stop sync if we go past the end time
166
301
  useEffect(() => {
167
302
  if (!editedSegment) return
@@ -171,48 +306,32 @@ export default function EditModal({
171
306
  if (window.isAudioPlaying && currentTime > endTime) {
172
307
  console.log('Stopping playback: current time exceeded end time')
173
308
  window.toggleAudioPlayback?.()
174
- setIsManualSyncing(false)
175
- setSyncWordIndex(-1)
309
+ cleanupManualSync()
176
310
  }
177
311
 
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])
312
+ }, [isManualSyncing, editedSegment, currentTime, cleanupManualSync])
191
313
 
192
314
  // Add a function to get safe time values
193
- const getSafeTimeRange = (segment: LyricsSegment | null) => {
315
+ const getSafeTimeRange = useCallback((segment: LyricsSegment | null) => {
194
316
  if (!segment) return { start: 0, end: 1 }; // Default 1-second range
195
317
  const start = segment.start_time ?? 0;
196
318
  const end = segment.end_time ?? (start + 1);
197
319
  return { start, end };
198
- }
199
-
200
- // Early return after all hooks and function definitions
201
- if (!segment || segmentIndex === null || !editedSegment || !originalSegment) return null
202
-
203
- // Get safe time values for TimelineEditor
204
- const timeRange = getSafeTimeRange(editedSegment)
320
+ }, [])
205
321
 
206
- const handleWordChange = (index: number, updates: Partial<Word>) => {
322
+ // Define all handler functions with useCallback before the early return
323
+ const handleWordChange = useCallback((index: number, updates: Partial<Word>) => {
324
+ if (!editedSegment) return;
207
325
  const newWords = [...editedSegment.words]
208
326
  newWords[index] = {
209
327
  ...newWords[index],
210
328
  ...updates
211
329
  }
212
330
  updateSegment(newWords)
213
- }
331
+ }, [editedSegment, updateSegment])
214
332
 
215
- const handleAddWord = (index?: number) => {
333
+ const handleAddWord = useCallback((index?: number) => {
334
+ if (!editedSegment) return;
216
335
  const newWords = [...editedSegment.words]
217
336
  let newWord: Word
218
337
 
@@ -250,45 +369,47 @@ export default function EditModal({
250
369
  }
251
370
 
252
371
  updateSegment(newWords)
253
- }
372
+ }, [editedSegment, updateSegment])
254
373
 
255
- const handleSplitWord = (index: number) => {
374
+ const handleSplitWord = useCallback((index: number) => {
375
+ if (!editedSegment) return;
256
376
  const word = editedSegment.words[index]
257
377
  const startTime = word.start_time ?? 0
258
378
  const endTime = word.end_time ?? startTime + 0.5
259
- const midTime = (startTime + endTime) / 2
260
- const words = word.text.split(/\s+/)
379
+ const totalDuration = endTime - startTime
380
+
381
+ // Split on any number of spaces and filter out empty strings
382
+ const words = word.text.split(/\s+/).filter(w => w.length > 0)
261
383
 
262
384
  if (words.length <= 1) {
263
- // Split single word in half
385
+ // If no spaces found, split the word in half as before
264
386
  const firstHalf = word.text.slice(0, Math.ceil(word.text.length / 2))
265
387
  const secondHalf = word.text.slice(Math.ceil(word.text.length / 2))
266
388
  words[0] = firstHalf
267
389
  words[1] = secondHalf
268
390
  }
269
391
 
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
- )
392
+ // Calculate time per word
393
+ const timePerWord = totalDuration / words.length
287
394
 
288
- updateSegment(newWords)
289
- }
395
+ // Create new word objects with evenly distributed times
396
+ const newWords = words.map((text, i) => ({
397
+ id: nanoid(),
398
+ text,
399
+ start_time: startTime + (i * timePerWord),
400
+ end_time: startTime + ((i + 1) * timePerWord),
401
+ confidence: 1.0
402
+ }))
403
+
404
+ // Replace the original word with the new words
405
+ const allWords = [...editedSegment.words]
406
+ allWords.splice(index, 1, ...newWords)
290
407
 
291
- const handleMergeWords = (index: number) => {
408
+ updateSegment(allWords)
409
+ }, [editedSegment, updateSegment])
410
+
411
+ const handleMergeWords = useCallback((index: number) => {
412
+ if (!editedSegment) return;
292
413
  if (index >= editedSegment.words.length - 1) return
293
414
 
294
415
  const word1 = editedSegment.words[index]
@@ -304,116 +425,79 @@ export default function EditModal({
304
425
  })
305
426
 
306
427
  updateSegment(newWords)
307
- }
428
+ }, [editedSegment, updateSegment])
308
429
 
309
- const handleRemoveWord = (index: number) => {
430
+ const handleRemoveWord = useCallback((index: number) => {
431
+ if (!editedSegment) return;
310
432
  const newWords = editedSegment.words.filter((_, i) => i !== index)
311
433
  updateSegment(newWords)
312
- }
313
-
314
- const handleReset = () => {
315
- setEditedSegment(JSON.parse(JSON.stringify(originalSegment)))
316
- }
317
-
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)
326
- }
327
-
328
- const handleSave = () => {
329
- if (editedSegment) {
330
- console.log('EditModal - Saving segment:', {
331
- segmentIndex,
332
- originalText: segment?.text,
333
- editedText: editedSegment.text,
334
- wordCount: editedSegment.words.length,
335
- timeRange: `${editedSegment.start_time?.toFixed(4) ?? 'N/A'} - ${editedSegment.end_time?.toFixed(4) ?? 'N/A'}`
336
- })
337
- onSave(editedSegment)
338
- onClose()
339
- }
340
- }
434
+ }, [editedSegment, updateSegment])
341
435
 
342
- const handleReplaceAllWords = () => {
343
- if (!editedSegment) return
436
+ const handleReset = useCallback(() => {
437
+ if (!originalSegment) return
344
438
 
345
- const newWords = replacementText.trim().split(/\s+/)
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
439
+ console.log('EditModal - Resetting to original:', {
440
+ isGlobal,
441
+ originalSegmentId: originalSegment.id,
442
+ originalWordCount: originalSegment.words.length
443
+ })
349
444
 
350
- let updatedWords: Word[]
445
+ setEditedSegment(JSON.parse(JSON.stringify(originalSegment)))
446
+ }, [originalSegment, isGlobal])
351
447
 
352
- if (newWords.length === editedSegment.words.length) {
353
- // If word count matches, keep original timestamps and IDs
354
- updatedWords = editedSegment.words.map((word, index) => ({
355
- id: word.id, // Keep original ID
356
- text: newWords[index],
357
- start_time: word.start_time,
358
- end_time: word.end_time,
359
- confidence: 1.0
360
- }))
361
- } else {
362
- // If word count differs, distribute time evenly and generate new IDs
363
- const avgWordDuration = segmentDuration / newWords.length
364
- updatedWords = newWords.map((text, index) => ({
365
- id: nanoid(), // Generate new ID
366
- text,
367
- start_time: startTime + (index * avgWordDuration),
368
- end_time: startTime + ((index + 1) * avgWordDuration),
369
- confidence: 1.0
370
- }))
371
- }
448
+ const handleRevertToOriginal = useCallback(() => {
449
+ if (!originalTranscribedSegment) return
372
450
 
373
- updateSegment(updatedWords)
374
- setReplacementText('') // Clear the input after replacing
375
- }
451
+ console.log('EditModal - Reverting to original transcribed:', {
452
+ isGlobal,
453
+ originalTranscribedSegmentId: originalTranscribedSegment.id,
454
+ originalTranscribedWordCount: originalTranscribedSegment.words.length
455
+ })
376
456
 
377
- const handleKeyDown = (event: React.KeyboardEvent) => {
378
- if (event.key === 'Enter' && !event.shiftKey) {
379
- event.preventDefault()
380
- handleSave()
381
- }
382
- }
457
+ setEditedSegment(JSON.parse(JSON.stringify(originalTranscribedSegment)))
458
+ }, [originalTranscribedSegment, isGlobal])
459
+
460
+ const handleSave = useCallback(() => {
461
+ if (!editedSegment || !segment) return;
462
+
463
+ console.log('EditModal - Saving segment:', {
464
+ isGlobal,
465
+ segmentIndex,
466
+ originalText: segment?.text,
467
+ editedText: editedSegment.text,
468
+ wordCount: editedSegment.words.length,
469
+ firstWord: editedSegment.words[0],
470
+ lastWord: editedSegment.words[editedSegment.words.length - 1],
471
+ timeRange: `${editedSegment.start_time?.toFixed(4) ?? 'N/A'} - ${editedSegment.end_time?.toFixed(4) ?? 'N/A'}`
472
+ })
473
+ onSave(editedSegment)
474
+ onClose()
475
+ }, [editedSegment, isGlobal, segmentIndex, segment, onSave, onClose])
383
476
 
384
- const handleDelete = () => {
477
+ const handleDelete = useCallback(() => {
385
478
  if (segmentIndex !== null) {
386
479
  onDelete?.(segmentIndex)
387
480
  onClose()
388
481
  }
389
- }
482
+ }, [segmentIndex, onDelete, onClose])
390
483
 
391
- const handleSplitSegment = (wordIndex: number) => {
484
+ const handleSplitSegment = useCallback((wordIndex: number) => {
392
485
  if (segmentIndex !== null && editedSegment) {
393
486
  handleSave() // Save current changes first
394
487
  onSplitSegment?.(segmentIndex, wordIndex)
395
488
  }
396
- }
489
+ }, [segmentIndex, editedSegment, handleSave, onSplitSegment])
397
490
 
398
- // Add this new function to handle manual sync
399
- const startManualSync = () => {
400
- if (isManualSyncing) {
401
- setIsManualSyncing(false)
402
- setSyncWordIndex(-1)
403
- return
491
+ const handleMergeSegment = useCallback((mergeWithNext: boolean) => {
492
+ if (segmentIndex !== null && editedSegment) {
493
+ handleSave() // Save current changes first
494
+ onMergeSegment?.(segmentIndex, mergeWithNext)
495
+ onClose()
404
496
  }
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
- }
497
+ }, [segmentIndex, editedSegment, handleSave, onMergeSegment, onClose])
414
498
 
415
499
  // Handle play/stop button click
416
- const handlePlayButtonClick = () => {
500
+ const handlePlayButtonClick = useCallback(() => {
417
501
  if (!segment?.start_time || !onPlaySegment) return
418
502
 
419
503
  if (isPlaying) {
@@ -425,19 +509,37 @@ export default function EditModal({
425
509
  // Start playback
426
510
  onPlaySegment(segment.start_time)
427
511
  }
428
- }
429
-
430
- return (
431
- <Dialog
432
- open={open}
433
- onClose={handleClose}
434
- maxWidth="md"
435
- fullWidth
436
- onKeyDown={handleKeyDown}
437
- >
512
+ }, [segment?.start_time, onPlaySegment, isPlaying])
513
+
514
+ // Calculate timeRange before the early return
515
+ const timeRange = useMemo(() => {
516
+ if (!editedSegment) return { start: 0, end: 1 };
517
+ return getSafeTimeRange(editedSegment);
518
+ }, [getSafeTimeRange, editedSegment]);
519
+
520
+ // Memoize the dialog title to prevent re-renders
521
+ const dialogTitle = useMemo(() => {
522
+ console.log('EditModal - Rendering dialog title', { isLoading, isGlobal });
523
+
524
+ if (isLoading) {
525
+ return (
526
+ <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
527
+ <Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
528
+ Loading {isGlobal ? 'All Words' : `Segment ${segmentIndex}`}...
529
+ </Box>
530
+ <IconButton onClick={onClose} sx={{ ml: 'auto' }}>
531
+ <CloseIcon />
532
+ </IconButton>
533
+ </DialogTitle>
534
+ );
535
+ }
536
+
537
+ if (!segment) return null;
538
+
539
+ return (
438
540
  <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
439
541
  <Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
440
- Edit Segment {segmentIndex}
542
+ Edit {isGlobal ? 'All Words' : `Segment ${segmentIndex}`}
441
543
  {segment?.start_time !== null && onPlaySegment && (
442
544
  <IconButton
443
545
  size="small"
@@ -456,179 +558,145 @@ export default function EditModal({
456
558
  <CloseIcon />
457
559
  </IconButton>
458
560
  </DialogTitle>
459
- <DialogContent dividers>
460
- <Box sx={{ mb: 2 }}>
461
- <TimelineEditor
462
- words={editedSegment.words}
463
- startTime={timeRange.start}
464
- endTime={timeRange.end}
465
- onWordUpdate={handleWordChange}
466
- currentTime={currentTime}
467
- onPlaySegment={onPlaySegment}
468
- />
469
- </Box>
561
+ );
562
+ }, [isGlobal, segmentIndex, segment, onPlaySegment, handlePlayButtonClick, isPlaying, onClose, isLoading])
470
563
 
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>
564
+ // Early return after all hooks and function definitions
565
+ if (!isLoading && (!segment || !editedSegment || !originalSegment)) {
566
+ console.log('EditModal - Early return: missing required data', {
567
+ hasSegment: !!segment,
568
+ hasEditedSegment: !!editedSegment,
569
+ hasOriginalSegment: !!originalSegment,
570
+ isLoading
571
+ });
572
+ return null;
573
+ }
574
+ if (!isLoading && !isGlobal && segmentIndex === null) {
575
+ console.log('EditModal - Early return: non-global mode with null segmentIndex');
576
+ return null;
577
+ }
495
578
 
496
- <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 3 }}>
497
- {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"
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>
531
- <IconButton onClick={(e) => handleWordMenu(e, index)}>
532
- <MoreVertIcon />
533
- </IconButton>
534
- </Box>
535
- ))}
536
- </Box>
579
+ console.log('EditModal - Rendering dialog content', {
580
+ isLoading,
581
+ hasEditedSegment: !!editedSegment,
582
+ hasOriginalSegment: !!originalSegment
583
+ });
537
584
 
538
- <Box sx={{ display: 'flex', gap: 2 }}>
539
- <TextField
540
- label="Replace all words"
541
- value={replacementText}
542
- onChange={(e) => setReplacementText(e.target.value)}
543
- fullWidth
544
- placeholder="Type or paste replacement words here"
545
- size="small"
546
- />
547
- <Button
548
- variant="contained"
549
- startIcon={<AutoFixHighIcon />}
550
- onClick={handleReplaceAllWords}
551
- disabled={!replacementText.trim()}
552
- >
553
- Replace All
554
- </Button>
555
- </Box>
585
+ return (
586
+ <Dialog
587
+ open={open}
588
+ onClose={handleClose}
589
+ maxWidth="md"
590
+ fullWidth
591
+ onKeyDown={(e) => {
592
+ if (e.key === 'Enter' && !e.shiftKey && !isLoading) {
593
+ e.preventDefault()
594
+ handleSave()
595
+ }
596
+ }}
597
+ PaperProps={{
598
+ sx: {
599
+ height: '90vh',
600
+ margin: '5vh 0'
601
+ }
602
+ }}
603
+ >
604
+ {dialogTitle}
605
+
606
+ <DialogContent
607
+ dividers
608
+ sx={{
609
+ display: 'flex',
610
+ flexDirection: 'column',
611
+ flexGrow: 1,
612
+ overflow: 'hidden',
613
+ position: 'relative'
614
+ }}
615
+ >
616
+ {isLoading && (
617
+ <Box sx={{
618
+ display: 'flex',
619
+ flexDirection: 'column',
620
+ alignItems: 'center',
621
+ justifyContent: 'center',
622
+ height: '100%',
623
+ width: '100%',
624
+ position: 'absolute',
625
+ top: 0,
626
+ left: 0,
627
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
628
+ zIndex: 10
629
+ }}>
630
+ <CircularProgress size={60} thickness={4} />
631
+ <Typography variant="h6" sx={{ mt: 2, fontWeight: 'bold' }}>
632
+ Loading {isGlobal ? 'all words' : 'segment'}...
633
+ </Typography>
634
+ <Typography variant="body2" sx={{ mt: 1, maxWidth: '80%', textAlign: 'center' }}>
635
+ {isGlobal ? 'This may take a few seconds for songs with many words.' : 'Please wait...'}
636
+ </Typography>
637
+ </Box>
638
+ )}
639
+
640
+ {!isLoading && editedSegment && originalSegment && (
641
+ <>
642
+ <MemoizedTimelineSection
643
+ words={editedSegment.words}
644
+ timeRange={timeRange}
645
+ originalSegment={originalSegment}
646
+ editedSegment={editedSegment}
647
+ currentTime={currentTime}
648
+ isManualSyncing={isManualSyncing}
649
+ syncWordIndex={syncWordIndex}
650
+ isSpacebarPressed={isSpacebarPressed}
651
+ onWordUpdate={handleWordChange}
652
+ onPlaySegment={onPlaySegment}
653
+ startManualSync={startManualSync}
654
+ isGlobal={isGlobal}
655
+ />
656
+
657
+ <MemoizedWordList
658
+ words={editedSegment.words}
659
+ onWordUpdate={handleWordChange}
660
+ onSplitWord={handleSplitWord}
661
+ onMergeWords={handleMergeWords}
662
+ onAddWord={handleAddWord}
663
+ onRemoveWord={handleRemoveWord}
664
+ onSplitSegment={handleSplitSegment}
665
+ onAddSegment={onAddSegment}
666
+ onMergeSegment={handleMergeSegment}
667
+ isGlobal={isGlobal}
668
+ />
669
+ </>
670
+ )}
671
+
672
+ {!isLoading && (!editedSegment || !originalSegment) && (
673
+ <Box sx={{
674
+ display: 'flex',
675
+ alignItems: 'center',
676
+ justifyContent: 'center',
677
+ height: '100%'
678
+ }}>
679
+ <Typography variant="h6">
680
+ No segment data available
681
+ </Typography>
682
+ </Box>
683
+ )}
556
684
  </DialogContent>
685
+
557
686
  <DialogActions>
558
- <Button
559
- startIcon={<RestoreIcon />}
560
- onClick={handleReset}
561
- color="warning"
562
- >
563
- Reset
564
- </Button>
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
- }}>
586
- Save Changes
587
- </Button>
687
+ {!isLoading && editedSegment && (
688
+ <MemoizedActionBar
689
+ onReset={handleReset}
690
+ onRevertToOriginal={handleRevertToOriginal}
691
+ onDelete={handleDelete}
692
+ onClose={handleClose}
693
+ onSave={handleSave}
694
+ editedSegment={editedSegment}
695
+ originalTranscribedSegment={originalTranscribedSegment}
696
+ isGlobal={isGlobal}
697
+ />
698
+ )}
588
699
  </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
700
  </Dialog>
633
701
  )
634
702
  }