lyrics-transcriber 0.44.0__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 (22) hide show
  1. lyrics_transcriber/frontend/dist/assets/{index-DVoI6Z16.js → index-ZCT0s9MG.js} +2635 -1967
  2. lyrics_transcriber/frontend/dist/assets/index-ZCT0s9MG.js.map +1 -0
  3. lyrics_transcriber/frontend/dist/index.html +1 -1
  4. lyrics_transcriber/frontend/src/App.tsx +1 -1
  5. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
  6. lyrics_transcriber/frontend/src/components/EditModal.tsx +376 -303
  7. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +373 -0
  8. lyrics_transcriber/frontend/src/components/EditWordList.tsx +308 -0
  9. lyrics_transcriber/frontend/src/components/Header.tsx +7 -7
  10. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +458 -62
  11. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +7 -7
  12. lyrics_transcriber/frontend/src/components/WordDivider.tsx +4 -3
  13. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +1 -2
  14. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +68 -46
  15. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  16. {lyrics_transcriber-0.44.0.dist-info → lyrics_transcriber-0.45.0.dist-info}/METADATA +1 -1
  17. {lyrics_transcriber-0.44.0.dist-info → lyrics_transcriber-0.45.0.dist-info}/RECORD +20 -18
  18. lyrics_transcriber/frontend/dist/assets/index-DVoI6Z16.js.map +0 -1
  19. lyrics_transcriber/frontend/src/components/GlobalSyncEditor.tsx +0 -675
  20. {lyrics_transcriber-0.44.0.dist-info → lyrics_transcriber-0.45.0.dist-info}/LICENSE +0 -0
  21. {lyrics_transcriber-0.44.0.dist-info → lyrics_transcriber-0.45.0.dist-info}/WHEEL +0 -0
  22. {lyrics_transcriber-0.44.0.dist-info → lyrics_transcriber-0.45.0.dist-info}/entry_points.txt +0 -0
@@ -5,25 +5,148 @@ import {
5
5
  DialogActions,
6
6
  IconButton,
7
7
  Box,
8
- TextField,
9
- Button,
8
+ CircularProgress,
10
9
  Typography
11
10
  } from '@mui/material'
12
11
  import CloseIcon from '@mui/icons-material/Close'
13
- import DeleteIcon from '@mui/icons-material/Delete'
14
- import SplitIcon from '@mui/icons-material/CallSplit'
15
- import RestoreIcon from '@mui/icons-material/RestoreFromTrash'
16
- import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
17
12
  import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
18
- import CancelIcon from '@mui/icons-material/Cancel'
19
13
  import StopIcon from '@mui/icons-material/Stop'
20
- import HistoryIcon from '@mui/icons-material/History'
21
14
  import { LyricsSegment, Word } from '../types'
22
- import { useState, useEffect, useCallback } from 'react'
23
- import TimelineEditor from './TimelineEditor'
15
+ import { useState, useEffect, useCallback, useMemo, memo } from 'react'
24
16
  import { nanoid } from 'nanoid'
25
- import WordDivider from './WordDivider'
26
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
+ })
27
150
 
28
151
  interface EditModalProps {
29
152
  open: boolean
@@ -40,6 +163,8 @@ interface EditModalProps {
40
163
  onMergeSegment?: (segmentIndex: number, mergeWithNext: boolean) => void
41
164
  setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
42
165
  originalTranscribedSegment?: LyricsSegment | null
166
+ isGlobal?: boolean
167
+ isLoading?: boolean
43
168
  }
44
169
 
45
170
  export default function EditModal({
@@ -56,10 +181,21 @@ export default function EditModal({
56
181
  onSplitSegment,
57
182
  onMergeSegment,
58
183
  setModalSpacebarHandler,
59
- originalTranscribedSegment
184
+ originalTranscribedSegment,
185
+ isGlobal = false,
186
+ isLoading = false
60
187
  }: EditModalProps) {
188
+ console.log('EditModal - Render', {
189
+ open,
190
+ isGlobal,
191
+ isLoading,
192
+ hasSegment: !!segment,
193
+ segmentIndex,
194
+ hasOriginalSegment: !!originalSegment,
195
+ hasOriginalTranscribedSegment: !!originalTranscribedSegment
196
+ });
197
+
61
198
  const [editedSegment, setEditedSegment] = useState<LyricsSegment | null>(segment)
62
- const [replacementText, setReplacementText] = useState('')
63
199
  const [isPlaying, setIsPlaying] = useState(false)
64
200
 
65
201
  // Define updateSegment first since the hook depends on it
@@ -81,7 +217,7 @@ export default function EditModal({
81
217
  })
82
218
  }, [editedSegment])
83
219
 
84
- // Use the new hook
220
+ // Use the manual sync hook
85
221
  const {
86
222
  isManualSyncing,
87
223
  syncWordIndex,
@@ -97,6 +233,7 @@ export default function EditModal({
97
233
  })
98
234
 
99
235
  const handleClose = useCallback(() => {
236
+ console.log('EditModal - handleClose called');
100
237
  cleanupManualSync()
101
238
  onClose()
102
239
  }, [onClose, cleanupManualSync])
@@ -109,16 +246,17 @@ export default function EditModal({
109
246
  console.log('EditModal - Setting up modal spacebar handler', {
110
247
  hasPlaySegment: !!onPlaySegment,
111
248
  editedSegmentId: editedSegment?.id,
112
- handlerFunction: spacebarHandler.toString().slice(0, 100)
249
+ handlerFunction: spacebarHandler.toString().slice(0, 100),
250
+ isLoading
113
251
  })
114
-
252
+
115
253
  // Create a function that will be called by the global event listeners
116
254
  const handleKeyEvent = (e: KeyboardEvent) => {
117
255
  if (e.code === 'Space') {
118
256
  spacebarHandler(e)
119
257
  }
120
258
  }
121
-
259
+
122
260
  setModalSpacebarHandler(() => handleKeyEvent)
123
261
 
124
262
  // Only cleanup when the effect is re-run or the modal is closed
@@ -134,7 +272,8 @@ export default function EditModal({
134
272
  handleSpacebar,
135
273
  setModalSpacebarHandler,
136
274
  editedSegment?.id,
137
- onPlaySegment
275
+ onPlaySegment,
276
+ isLoading
138
277
  ])
139
278
 
140
279
  // Update isPlaying when currentTime changes
@@ -150,6 +289,11 @@ export default function EditModal({
150
289
 
151
290
  // All useEffect hooks
152
291
  useEffect(() => {
292
+ console.log('EditModal - segment changed', {
293
+ hasSegment: !!segment,
294
+ segmentId: segment?.id,
295
+ wordCount: segment?.words.length
296
+ });
153
297
  setEditedSegment(segment)
154
298
  }, [segment])
155
299
 
@@ -168,29 +312,26 @@ export default function EditModal({
168
312
  }, [isManualSyncing, editedSegment, currentTime, cleanupManualSync])
169
313
 
170
314
  // Add a function to get safe time values
171
- const getSafeTimeRange = (segment: LyricsSegment | null) => {
315
+ const getSafeTimeRange = useCallback((segment: LyricsSegment | null) => {
172
316
  if (!segment) return { start: 0, end: 1 }; // Default 1-second range
173
317
  const start = segment.start_time ?? 0;
174
318
  const end = segment.end_time ?? (start + 1);
175
319
  return { start, end };
176
- }
320
+ }, [])
177
321
 
178
- // Early return after all hooks and function definitions
179
- if (!segment || segmentIndex === null || !editedSegment || !originalSegment) return null
180
-
181
- // Get safe time values for TimelineEditor
182
- const timeRange = getSafeTimeRange(editedSegment)
183
-
184
- 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;
185
325
  const newWords = [...editedSegment.words]
186
326
  newWords[index] = {
187
327
  ...newWords[index],
188
328
  ...updates
189
329
  }
190
330
  updateSegment(newWords)
191
- }
331
+ }, [editedSegment, updateSegment])
192
332
 
193
- const handleAddWord = (index?: number) => {
333
+ const handleAddWord = useCallback((index?: number) => {
334
+ if (!editedSegment) return;
194
335
  const newWords = [...editedSegment.words]
195
336
  let newWord: Word
196
337
 
@@ -228,9 +369,10 @@ export default function EditModal({
228
369
  }
229
370
 
230
371
  updateSegment(newWords)
231
- }
372
+ }, [editedSegment, updateSegment])
232
373
 
233
- const handleSplitWord = (index: number) => {
374
+ const handleSplitWord = useCallback((index: number) => {
375
+ if (!editedSegment) return;
234
376
  const word = editedSegment.words[index]
235
377
  const startTime = word.start_time ?? 0
236
378
  const endTime = word.end_time ?? startTime + 0.5
@@ -264,9 +406,10 @@ export default function EditModal({
264
406
  allWords.splice(index, 1, ...newWords)
265
407
 
266
408
  updateSegment(allWords)
267
- }
409
+ }, [editedSegment, updateSegment])
268
410
 
269
- const handleMergeWords = (index: number) => {
411
+ const handleMergeWords = useCallback((index: number) => {
412
+ if (!editedSegment) return;
270
413
  if (index >= editedSegment.words.length - 1) return
271
414
 
272
415
  const word1 = editedSegment.words[index]
@@ -282,103 +425,79 @@ export default function EditModal({
282
425
  })
283
426
 
284
427
  updateSegment(newWords)
285
- }
428
+ }, [editedSegment, updateSegment])
286
429
 
287
- const handleRemoveWord = (index: number) => {
430
+ const handleRemoveWord = useCallback((index: number) => {
431
+ if (!editedSegment) return;
288
432
  const newWords = editedSegment.words.filter((_, i) => i !== index)
289
433
  updateSegment(newWords)
290
- }
291
-
292
- const handleReset = () => {
293
- setEditedSegment(JSON.parse(JSON.stringify(originalSegment)))
294
- }
434
+ }, [editedSegment, updateSegment])
295
435
 
296
- const handleRevertToOriginal = () => {
297
- if (originalTranscribedSegment) {
298
- setEditedSegment(JSON.parse(JSON.stringify(originalTranscribedSegment)))
299
- }
300
- }
436
+ const handleReset = useCallback(() => {
437
+ if (!originalSegment) return
301
438
 
302
- const handleSave = () => {
303
- if (editedSegment) {
304
- console.log('EditModal - Saving segment:', {
305
- segmentIndex,
306
- originalText: segment?.text,
307
- editedText: editedSegment.text,
308
- wordCount: editedSegment.words.length,
309
- timeRange: `${editedSegment.start_time?.toFixed(4) ?? 'N/A'} - ${editedSegment.end_time?.toFixed(4) ?? 'N/A'}`
310
- })
311
- onSave(editedSegment)
312
- onClose()
313
- }
314
- }
315
-
316
- const handleReplaceAllWords = () => {
317
- if (!editedSegment) return
439
+ console.log('EditModal - Resetting to original:', {
440
+ isGlobal,
441
+ originalSegmentId: originalSegment.id,
442
+ originalWordCount: originalSegment.words.length
443
+ })
318
444
 
319
- const newWords = replacementText.trim().split(/\s+/)
320
- const startTime = editedSegment.start_time ?? 0
321
- const endTime = editedSegment.end_time ?? (startTime + newWords.length) // Default to 1 second per word
322
- const segmentDuration = endTime - startTime
445
+ setEditedSegment(JSON.parse(JSON.stringify(originalSegment)))
446
+ }, [originalSegment, isGlobal])
323
447
 
324
- let updatedWords: Word[]
448
+ const handleRevertToOriginal = useCallback(() => {
449
+ if (!originalTranscribedSegment) return
325
450
 
326
- if (newWords.length === editedSegment.words.length) {
327
- // If word count matches, keep original timestamps and IDs
328
- updatedWords = editedSegment.words.map((word, index) => ({
329
- id: word.id, // Keep original ID
330
- text: newWords[index],
331
- start_time: word.start_time,
332
- end_time: word.end_time,
333
- confidence: 1.0
334
- }))
335
- } else {
336
- // If word count differs, distribute time evenly and generate new IDs
337
- const avgWordDuration = segmentDuration / newWords.length
338
- updatedWords = newWords.map((text, index) => ({
339
- id: nanoid(), // Generate new ID
340
- text,
341
- start_time: startTime + (index * avgWordDuration),
342
- end_time: startTime + ((index + 1) * avgWordDuration),
343
- confidence: 1.0
344
- }))
345
- }
346
-
347
- updateSegment(updatedWords)
348
- setReplacementText('') // Clear the input after replacing
349
- }
451
+ console.log('EditModal - Reverting to original transcribed:', {
452
+ isGlobal,
453
+ originalTranscribedSegmentId: originalTranscribedSegment.id,
454
+ originalTranscribedWordCount: originalTranscribedSegment.words.length
455
+ })
350
456
 
351
- const handleKeyDown = (event: React.KeyboardEvent) => {
352
- if (event.key === 'Enter' && !event.shiftKey) {
353
- event.preventDefault()
354
- handleSave()
355
- }
356
- }
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])
357
476
 
358
- const handleDelete = () => {
477
+ const handleDelete = useCallback(() => {
359
478
  if (segmentIndex !== null) {
360
479
  onDelete?.(segmentIndex)
361
480
  onClose()
362
481
  }
363
- }
482
+ }, [segmentIndex, onDelete, onClose])
364
483
 
365
- const handleSplitSegment = (wordIndex: number) => {
484
+ const handleSplitSegment = useCallback((wordIndex: number) => {
366
485
  if (segmentIndex !== null && editedSegment) {
367
486
  handleSave() // Save current changes first
368
487
  onSplitSegment?.(segmentIndex, wordIndex)
369
488
  }
370
- }
489
+ }, [segmentIndex, editedSegment, handleSave, onSplitSegment])
371
490
 
372
- const handleMergeSegment = (mergeWithNext: boolean) => {
491
+ const handleMergeSegment = useCallback((mergeWithNext: boolean) => {
373
492
  if (segmentIndex !== null && editedSegment) {
374
493
  handleSave() // Save current changes first
375
494
  onMergeSegment?.(segmentIndex, mergeWithNext)
376
495
  onClose()
377
496
  }
378
- }
497
+ }, [segmentIndex, editedSegment, handleSave, onMergeSegment, onClose])
379
498
 
380
499
  // Handle play/stop button click
381
- const handlePlayButtonClick = () => {
500
+ const handlePlayButtonClick = useCallback(() => {
382
501
  if (!segment?.start_time || !onPlaySegment) return
383
502
 
384
503
  if (isPlaying) {
@@ -390,25 +509,37 @@ export default function EditModal({
390
509
  // Start playback
391
510
  onPlaySegment(segment.start_time)
392
511
  }
393
- }
394
-
395
- return (
396
- <Dialog
397
- open={open}
398
- onClose={handleClose}
399
- maxWidth="md"
400
- fullWidth
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
- }}
408
- >
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 (
409
540
  <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
410
541
  <Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
411
- Edit Segment {segmentIndex}
542
+ Edit {isGlobal ? 'All Words' : `Segment ${segmentIndex}`}
412
543
  {segment?.start_time !== null && onPlaySegment && (
413
544
  <IconButton
414
545
  size="small"
@@ -427,202 +558,144 @@ export default function EditModal({
427
558
  <CloseIcon />
428
559
  </IconButton>
429
560
  </DialogTitle>
561
+ );
562
+ }, [isGlobal, segmentIndex, segment, onPlaySegment, handlePlayButtonClick, isPlaying, onClose, isLoading])
563
+
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
+ }
578
+
579
+ console.log('EditModal - Rendering dialog content', {
580
+ isLoading,
581
+ hasEditedSegment: !!editedSegment,
582
+ hasOriginalSegment: !!originalSegment
583
+ });
584
+
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
+
430
606
  <DialogContent
431
607
  dividers
432
608
  sx={{
433
609
  display: 'flex',
434
610
  flexDirection: 'column',
435
- flexGrow: 1, // Allow content to fill available space
436
- overflow: 'hidden' // Prevent double scrollbars
611
+ flexGrow: 1,
612
+ overflow: 'hidden',
613
+ position: 'relative'
437
614
  }}
438
615
  >
439
- <Box sx={{ mb: 0 }}>
440
- <TimelineEditor
441
- words={editedSegment.words}
442
- startTime={timeRange.start}
443
- endTime={timeRange.end}
444
- onWordUpdate={handleWordChange}
445
- currentTime={currentTime}
446
- onPlaySegment={onPlaySegment}
447
- />
448
- </Box>
449
-
450
- <Box sx={{ mb: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
451
- <Typography variant="body2" color="text.secondary">
452
- Original Time Range: {originalSegment?.start_time?.toFixed(2) ?? 'N/A'} - {originalSegment?.end_time?.toFixed(2) ?? 'N/A'}
453
- <br />
454
- Current Time Range: {editedSegment?.start_time?.toFixed(2) ?? 'N/A'} - {editedSegment?.end_time?.toFixed(2) ?? 'N/A'}
455
- </Typography>
456
-
457
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
458
- <Button
459
- variant={isManualSyncing ? "outlined" : "contained"}
460
- onClick={startManualSync}
461
- disabled={!onPlaySegment}
462
- startIcon={isManualSyncing ? <CancelIcon /> : <PlayCircleOutlineIcon />}
463
- color={isManualSyncing ? "error" : "primary"}
464
- >
465
- {isManualSyncing ? "Cancel Sync" : "Manual Sync"}
466
- </Button>
467
- {isManualSyncing && (
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>
478
- )}
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>
479
637
  </Box>
480
- </Box>
481
-
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
- />
638
+ )}
504
639
 
505
- {editedSegment.words.map((word, index) => (
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 }}
570
- />
571
- </Box>
572
- ))}
573
- </Box>
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
+ )}
574
671
 
575
- <Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
576
- <TextField
577
- value={replacementText}
578
- onChange={(e) => setReplacementText(e.target.value)}
579
- placeholder="Replace all words"
580
- size="small"
581
- sx={{ flexGrow: 1, maxWidth: 'calc(100% - 140px)' }} // Reserve space for the button
582
- />
583
- <Button
584
- onClick={handleReplaceAllWords}
585
- startIcon={<AutoFixHighIcon />}
586
- size="small"
587
- sx={{ whiteSpace: 'nowrap' }}
588
- >
589
- Replace All
590
- </Button>
591
- </Box>
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
+ )}
592
684
  </DialogContent>
685
+
593
686
  <DialogActions>
594
- <Button
595
- startIcon={<RestoreIcon />}
596
- onClick={handleReset}
597
- color="warning"
598
- >
599
- Reset
600
- </Button>
601
- {originalTranscribedSegment && (
602
- <Button
603
- onClick={handleRevertToOriginal}
604
- startIcon={<HistoryIcon />}
605
- >
606
- Un-Correct
607
- </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
+ />
608
698
  )}
609
- <Box sx={{ mr: 'auto' }}>
610
- <Button
611
- startIcon={<DeleteIcon />}
612
- onClick={handleDelete}
613
- color="error"
614
- >
615
- Delete Segment
616
- </Button>
617
- </Box>
618
- <Button onClick={handleClose}>Cancel</Button>
619
- <Button
620
- onClick={handleSave}
621
- variant="contained"
622
- disabled={!editedSegment || editedSegment.words.length === 0}
623
- >
624
- Save
625
- </Button>
626
699
  </DialogActions>
627
700
  </Dialog>
628
701
  )