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
@@ -1,11 +1,13 @@
1
- import { useState, useEffect, useCallback } from 'react'
1
+ import { useState, useEffect, useCallback, useMemo, memo } from 'react'
2
2
  import {
3
3
  AnchorSequence,
4
4
  CorrectionData,
5
5
  GapSequence,
6
6
  HighlightInfo,
7
7
  InteractionMode,
8
- LyricsSegment
8
+ LyricsSegment,
9
+ ReferenceSource,
10
+ WordCorrection
9
11
  } from '../types'
10
12
  import { Box, Button, Grid, useMediaQuery, useTheme } from '@mui/material'
11
13
  import { ApiClient } from '../api'
@@ -29,7 +31,6 @@ import { getWordsFromIds } from './shared/utils/wordUtils'
29
31
  import AddLyricsModal from './AddLyricsModal'
30
32
  import { RestoreFromTrash, OndemandVideo } from '@mui/icons-material'
31
33
  import FindReplaceModal from './FindReplaceModal'
32
- import GlobalSyncEditor from './GlobalSyncEditor'
33
34
 
34
35
  // Add type for window augmentation at the top of the file
35
36
  declare global {
@@ -39,6 +40,7 @@ declare global {
39
40
  }
40
41
  }
41
42
 
43
+ const debugLog = false;
42
44
  export interface LyricsAnalyzerProps {
43
45
  data: CorrectionData
44
46
  onFileLoad: () => void
@@ -64,6 +66,160 @@ export type ModalContent = {
64
66
  }
65
67
  }
66
68
 
69
+ // Define types for the memoized components
70
+ interface MemoizedTranscriptionViewProps {
71
+ data: CorrectionData
72
+ mode: InteractionMode
73
+ onElementClick: (content: ModalContent) => void
74
+ onWordClick: (info: WordClickInfo) => void
75
+ flashingType: FlashType
76
+ flashingHandler: string | null
77
+ highlightInfo: HighlightInfo | null
78
+ onPlaySegment?: (time: number) => void
79
+ currentTime: number
80
+ anchors: AnchorSequence[]
81
+ disableHighlighting: boolean
82
+ }
83
+
84
+ // Create a memoized TranscriptionView component
85
+ const MemoizedTranscriptionView = memo(function MemoizedTranscriptionView({
86
+ data,
87
+ mode,
88
+ onElementClick,
89
+ onWordClick,
90
+ flashingType,
91
+ flashingHandler,
92
+ highlightInfo,
93
+ onPlaySegment,
94
+ currentTime,
95
+ anchors,
96
+ disableHighlighting
97
+ }: MemoizedTranscriptionViewProps) {
98
+ return (
99
+ <TranscriptionView
100
+ data={data}
101
+ mode={mode}
102
+ onElementClick={onElementClick}
103
+ onWordClick={onWordClick}
104
+ flashingType={flashingType}
105
+ flashingHandler={flashingHandler}
106
+ highlightInfo={highlightInfo}
107
+ onPlaySegment={onPlaySegment}
108
+ currentTime={disableHighlighting ? undefined : currentTime}
109
+ anchors={anchors}
110
+ />
111
+ );
112
+ });
113
+
114
+ interface MemoizedReferenceViewProps {
115
+ referenceSources: Record<string, ReferenceSource>
116
+ anchors: AnchorSequence[]
117
+ gaps: GapSequence[]
118
+ mode: InteractionMode
119
+ onElementClick: (content: ModalContent) => void
120
+ onWordClick: (info: WordClickInfo) => void
121
+ flashingType: FlashType
122
+ highlightInfo: HighlightInfo | null
123
+ currentSource: string
124
+ onSourceChange: (source: string) => void
125
+ corrected_segments: LyricsSegment[]
126
+ corrections: WordCorrection[]
127
+ }
128
+
129
+ // Create a memoized ReferenceView component
130
+ const MemoizedReferenceView = memo(function MemoizedReferenceView({
131
+ referenceSources,
132
+ anchors,
133
+ gaps,
134
+ mode,
135
+ onElementClick,
136
+ onWordClick,
137
+ flashingType,
138
+ highlightInfo,
139
+ currentSource,
140
+ onSourceChange,
141
+ corrected_segments,
142
+ corrections
143
+ }: MemoizedReferenceViewProps) {
144
+ return (
145
+ <ReferenceView
146
+ referenceSources={referenceSources}
147
+ anchors={anchors}
148
+ gaps={gaps}
149
+ mode={mode}
150
+ onElementClick={onElementClick}
151
+ onWordClick={onWordClick}
152
+ flashingType={flashingType}
153
+ highlightInfo={highlightInfo}
154
+ currentSource={currentSource}
155
+ onSourceChange={onSourceChange}
156
+ corrected_segments={corrected_segments}
157
+ corrections={corrections}
158
+ />
159
+ );
160
+ });
161
+
162
+ interface MemoizedHeaderProps {
163
+ isReadOnly: boolean
164
+ onFileLoad: () => void
165
+ data: CorrectionData
166
+ onMetricClick: {
167
+ anchor: () => void
168
+ corrected: () => void
169
+ uncorrected: () => void
170
+ }
171
+ effectiveMode: InteractionMode
172
+ onModeChange: (mode: InteractionMode) => void
173
+ apiClient: ApiClient | null
174
+ audioHash: string
175
+ onTimeUpdate: (time: number) => void
176
+ onHandlerToggle: (handler: string, enabled: boolean) => void
177
+ isUpdatingHandlers: boolean
178
+ onHandlerClick?: (handler: string) => void
179
+ onAddLyrics?: () => void
180
+ onFindReplace?: () => void
181
+ onEditAll?: () => void
182
+ }
183
+
184
+ // Create a memoized Header component
185
+ const MemoizedHeader = memo(function MemoizedHeader({
186
+ isReadOnly,
187
+ onFileLoad,
188
+ data,
189
+ onMetricClick,
190
+ effectiveMode,
191
+ onModeChange,
192
+ apiClient,
193
+ audioHash,
194
+ onTimeUpdate,
195
+ onHandlerToggle,
196
+ isUpdatingHandlers,
197
+ onHandlerClick,
198
+ onAddLyrics,
199
+ onFindReplace,
200
+ onEditAll
201
+ }: MemoizedHeaderProps) {
202
+ return (
203
+ <Header
204
+ isReadOnly={isReadOnly}
205
+ onFileLoad={onFileLoad}
206
+ data={data}
207
+ onMetricClick={onMetricClick}
208
+ effectiveMode={effectiveMode}
209
+ onModeChange={onModeChange}
210
+ apiClient={apiClient}
211
+ audioHash={audioHash}
212
+ onTimeUpdate={onTimeUpdate}
213
+ onHandlerToggle={onHandlerToggle}
214
+ isUpdatingHandlers={isUpdatingHandlers}
215
+ onHandlerClick={onHandlerClick}
216
+ onAddLyrics={onAddLyrics}
217
+ onFindReplace={onFindReplace}
218
+ onEditAll={onEditAll}
219
+ />
220
+ );
221
+ });
222
+
67
223
  export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly, audioHash }: LyricsAnalyzerProps) {
68
224
  const [modalContent, setModalContent] = useState<ModalContent | null>(null)
69
225
  const [flashingType, setFlashingType] = useState<FlashType>(null)
@@ -85,6 +241,11 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
85
241
  index: number
86
242
  originalSegment: LyricsSegment
87
243
  } | null>(null)
244
+ const [isEditAllModalOpen, setIsEditAllModalOpen] = useState(false)
245
+ const [globalEditSegment, setGlobalEditSegment] = useState<LyricsSegment | null>(null)
246
+ const [originalGlobalSegment, setOriginalGlobalSegment] = useState<LyricsSegment | null>(null)
247
+ const [originalTranscribedGlobalSegment, setOriginalTranscribedGlobalSegment] = useState<LyricsSegment | null>(null)
248
+ const [isLoadingGlobalEdit, setIsLoadingGlobalEdit] = useState(false)
88
249
  const [isReviewModalOpen, setIsReviewModalOpen] = useState(false)
89
250
  const [currentAudioTime, setCurrentAudioTime] = useState(0)
90
251
  const [isUpdatingHandlers, setIsUpdatingHandlers] = useState(false)
@@ -93,24 +254,25 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
93
254
  const [isAddLyricsModalOpen, setIsAddLyricsModalOpen] = useState(false)
94
255
  const [isAnyModalOpen, setIsAnyModalOpen] = useState(false)
95
256
  const [isFindReplaceModalOpen, setIsFindReplaceModalOpen] = useState(false)
96
- const [isGlobalSyncEditorOpen, setIsGlobalSyncEditorOpen] = useState(false)
97
257
  const theme = useTheme()
98
258
  const isMobile = useMediaQuery(theme.breakpoints.down('md'))
99
259
 
100
260
  // Update debug logging to use new ID-based structure
101
261
  useEffect(() => {
102
- console.log('LyricsAnalyzer Initial Data:', {
103
- hasData: !!initialData,
104
- segmentsCount: initialData?.corrected_segments?.length ?? 0,
105
- anchorsCount: initialData?.anchor_sequences?.length ?? 0,
106
- gapsCount: initialData?.gap_sequences?.length ?? 0,
107
- firstAnchor: initialData?.anchor_sequences?.[0] && {
108
- transcribedWordIds: initialData.anchor_sequences[0].transcribed_word_ids,
109
- referenceWordIds: initialData.anchor_sequences[0].reference_word_ids
110
- },
111
- firstSegment: initialData?.corrected_segments?.[0],
112
- referenceSources: Object.keys(initialData?.reference_lyrics ?? {})
113
- });
262
+ if (debugLog) {
263
+ console.log('LyricsAnalyzer Initial Data:', {
264
+ hasData: !!initialData,
265
+ segmentsCount: initialData?.corrected_segments?.length ?? 0,
266
+ anchorsCount: initialData?.anchor_sequences?.length ?? 0,
267
+ gapsCount: initialData?.gap_sequences?.length ?? 0,
268
+ firstAnchor: initialData?.anchor_sequences?.[0] && {
269
+ transcribedWordIds: initialData.anchor_sequences[0].transcribed_word_ids,
270
+ referenceWordIds: initialData.anchor_sequences[0].reference_word_ids
271
+ },
272
+ firstSegment: initialData?.corrected_segments?.[0],
273
+ referenceSources: Object.keys(initialData?.reference_lyrics ?? {})
274
+ });
275
+ }
114
276
  }, [initialData]);
115
277
 
116
278
  // Load saved data
@@ -132,17 +294,21 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
132
294
  useEffect(() => {
133
295
  const { currentModalHandler } = getModalState()
134
296
 
135
- console.log('LyricsAnalyzer - Setting up keyboard effect', {
136
- isAnyModalOpen,
137
- hasSpacebarHandler: !!currentModalHandler
138
- })
297
+ if (debugLog) {
298
+ console.log('LyricsAnalyzer - Setting up keyboard effect', {
299
+ isAnyModalOpen,
300
+ hasSpacebarHandler: !!currentModalHandler
301
+ })
302
+ }
139
303
 
140
304
  const { handleKeyDown, handleKeyUp } = setupKeyboardHandlers({
141
305
  setIsShiftPressed,
142
306
  })
143
307
 
144
308
  // Always add keyboard listeners
145
- console.log('LyricsAnalyzer - Adding keyboard event listeners')
309
+ if (debugLog) {
310
+ console.log('LyricsAnalyzer - Adding keyboard event listeners')
311
+ }
146
312
  window.addEventListener('keydown', handleKeyDown)
147
313
  window.addEventListener('keyup', handleKeyUp)
148
314
 
@@ -153,7 +319,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
153
319
 
154
320
  // Cleanup function
155
321
  return () => {
156
- console.log('LyricsAnalyzer - Cleanup effect running')
322
+ if (debugLog) {
323
+ console.log('LyricsAnalyzer - Cleanup effect running')
324
+ }
157
325
  window.removeEventListener('keydown', handleKeyDown)
158
326
  window.removeEventListener('keyup', handleKeyUp)
159
327
  document.body.style.userSelect = ''
@@ -167,10 +335,11 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
167
335
  editModalSegment ||
168
336
  isReviewModalOpen ||
169
337
  isAddLyricsModalOpen ||
170
- isFindReplaceModalOpen
338
+ isFindReplaceModalOpen ||
339
+ isEditAllModalOpen
171
340
  )
172
341
  setIsAnyModalOpen(modalOpen)
173
- }, [modalContent, editModalSegment, isReviewModalOpen, isAddLyricsModalOpen, isFindReplaceModalOpen])
342
+ }, [modalContent, editModalSegment, isReviewModalOpen, isAddLyricsModalOpen, isFindReplaceModalOpen, isEditAllModalOpen])
174
343
 
175
344
  // Calculate effective mode based on modifier key states
176
345
  const effectiveMode = isShiftPressed ? 'highlight' : interactionMode
@@ -194,7 +363,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
194
363
  }, [])
195
364
 
196
365
  const handleWordClick = useCallback((info: WordClickInfo) => {
197
- console.log('LyricsAnalyzer handleWordClick:', { info });
366
+ if (debugLog) {
367
+ console.log('LyricsAnalyzer handleWordClick:', { info });
368
+ }
198
369
 
199
370
  if (effectiveMode === 'highlight') {
200
371
  // Find if this word is part of a correction
@@ -342,7 +513,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
342
513
  if (!apiClient) return
343
514
 
344
515
  try {
345
- console.log('Submitting changes to server')
516
+ if (debugLog) {
517
+ console.log('Submitting changes to server')
518
+ }
346
519
  await apiClient.submitCorrections(data)
347
520
 
348
521
  setIsReviewComplete(true)
@@ -431,15 +604,20 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
431
604
  }, [apiClient, data.metadata.enabled_handlers, handleFlash])
432
605
 
433
606
  const handleHandlerClick = useCallback((handler: string) => {
434
- console.log('Handler clicked:', handler);
607
+ if (debugLog) {
608
+ console.log('Handler clicked:', handler);
609
+ }
435
610
  setFlashingHandler(handler);
436
611
  setFlashingType('handler');
437
- console.log('Set flashingHandler to:', handler);
438
- console.log('Set flashingType to: handler');
439
-
612
+ if (debugLog) {
613
+ console.log('Set flashingHandler to:', handler);
614
+ console.log('Set flashingType to: handler');
615
+ }
440
616
  // Clear the flash after a short delay
441
617
  setTimeout(() => {
442
- console.log('Clearing flash state');
618
+ if (debugLog) {
619
+ console.log('Clearing flash state');
620
+ }
443
621
  setFlashingHandler(null);
444
622
  setFlashingType(null);
445
623
  }, 1500);
@@ -447,9 +625,11 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
447
625
 
448
626
  // Wrap setModalSpacebarHandler in useCallback
449
627
  const handleSetModalSpacebarHandler = useCallback((handler: (() => (e: KeyboardEvent) => void) | undefined) => {
450
- console.log('LyricsAnalyzer - Setting modal handler:', {
451
- hasHandler: !!handler
452
- })
628
+ if (debugLog) {
629
+ console.log('LyricsAnalyzer - Setting modal handler:', {
630
+ hasHandler: !!handler
631
+ })
632
+ }
453
633
  // Update the global modal handler
454
634
  setModalHandler(handler ? handler() : undefined, !!handler)
455
635
  }, [])
@@ -472,6 +652,221 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
472
652
  setData(newData)
473
653
  }
474
654
 
655
+ // Add handler for Edit All functionality
656
+ const handleEditAll = useCallback(() => {
657
+ console.log('EditAll - Starting process');
658
+
659
+ // Create empty placeholder segments to prevent the modal from closing
660
+ const placeholderSegment: LyricsSegment = {
661
+ id: 'loading-placeholder',
662
+ words: [],
663
+ text: '',
664
+ start_time: 0,
665
+ end_time: 1
666
+ };
667
+
668
+ // Set placeholder segments first
669
+ setGlobalEditSegment(placeholderSegment);
670
+ setOriginalGlobalSegment(placeholderSegment);
671
+
672
+ // Show loading state
673
+ setIsLoadingGlobalEdit(true);
674
+ console.log('EditAll - Set loading state to true');
675
+
676
+ // Open the modal with placeholder data
677
+ setIsEditAllModalOpen(true);
678
+ console.log('EditAll - Set modal open to true');
679
+
680
+ // Use requestAnimationFrame to ensure the modal with loading state is rendered
681
+ // before doing the expensive operation
682
+ requestAnimationFrame(() => {
683
+ console.log('EditAll - Inside requestAnimationFrame');
684
+
685
+ // Use setTimeout to allow the modal to render before doing the expensive operation
686
+ setTimeout(() => {
687
+ console.log('EditAll - Inside setTimeout, starting data processing');
688
+
689
+ try {
690
+ console.time('EditAll - Data processing');
691
+
692
+ // Create a combined segment with all words from all segments
693
+ const allWords = data.corrected_segments.flatMap(segment => segment.words)
694
+ console.log(`EditAll - Collected ${allWords.length} words from all segments`);
695
+
696
+ // Sort words by start time to maintain chronological order
697
+ const sortedWords = [...allWords].sort((a, b) => {
698
+ const aTime = a.start_time ?? 0
699
+ const bTime = b.start_time ?? 0
700
+ return aTime - bTime
701
+ })
702
+ console.log('EditAll - Sorted words by start time');
703
+
704
+ // Create a global segment containing all words
705
+ const globalSegment: LyricsSegment = {
706
+ id: 'global-edit',
707
+ words: sortedWords,
708
+ text: sortedWords.map(w => w.text).join(' '),
709
+ start_time: sortedWords[0]?.start_time ?? null,
710
+ end_time: sortedWords[sortedWords.length - 1]?.end_time ?? null
711
+ }
712
+ console.log('EditAll - Created global segment');
713
+
714
+ // Store the original global segment for reset functionality
715
+ setGlobalEditSegment(globalSegment)
716
+ console.log('EditAll - Set global edit segment');
717
+
718
+ setOriginalGlobalSegment(JSON.parse(JSON.stringify(globalSegment)))
719
+ console.log('EditAll - Set original global segment');
720
+
721
+ // Create the original transcribed global segment for Un-Correct functionality
722
+ if (originalData.original_segments) {
723
+ console.log('EditAll - Processing original segments for Un-Correct functionality');
724
+
725
+ // Get all words from original segments
726
+ const originalWords = originalData.original_segments.flatMap((segment: LyricsSegment) => segment.words)
727
+ console.log(`EditAll - Collected ${originalWords.length} words from original segments`);
728
+
729
+ // Sort words by start time
730
+ const sortedOriginalWords = [...originalWords].sort((a, b) => {
731
+ const aTime = a.start_time ?? 0
732
+ const bTime = b.start_time ?? 0
733
+ return aTime - bTime
734
+ })
735
+ console.log('EditAll - Sorted original words by start time');
736
+
737
+ // Create the original transcribed global segment
738
+ const originalTranscribedGlobal: LyricsSegment = {
739
+ id: 'original-transcribed-global',
740
+ words: sortedOriginalWords,
741
+ text: sortedOriginalWords.map(w => w.text).join(' '),
742
+ start_time: sortedOriginalWords[0]?.start_time ?? null,
743
+ end_time: sortedOriginalWords[sortedOriginalWords.length - 1]?.end_time ?? null
744
+ }
745
+ console.log('EditAll - Created original transcribed global segment');
746
+
747
+ setOriginalTranscribedGlobalSegment(originalTranscribedGlobal)
748
+ console.log('EditAll - Set original transcribed global segment');
749
+ } else {
750
+ setOriginalTranscribedGlobalSegment(null)
751
+ console.log('EditAll - No original segments found, set original transcribed global segment to null');
752
+ }
753
+
754
+ console.timeEnd('EditAll - Data processing');
755
+ } catch (error) {
756
+ console.error('Error preparing global edit data:', error);
757
+ } finally {
758
+ // Clear loading state
759
+ console.log('EditAll - Finished processing, setting loading state to false');
760
+ setIsLoadingGlobalEdit(false);
761
+ }
762
+ }, 100); // Small delay to allow the modal to render
763
+ });
764
+ }, [data.corrected_segments, originalData.original_segments])
765
+
766
+ // Handle saving the global edit
767
+ const handleSaveGlobalEdit = useCallback((updatedSegment: LyricsSegment) => {
768
+ console.log('Global Edit - Saving with new approach:', {
769
+ updatedSegmentId: updatedSegment.id,
770
+ wordCount: updatedSegment.words.length,
771
+ originalSegmentCount: data.corrected_segments.length,
772
+ originalTotalWordCount: data.corrected_segments.reduce((count, segment) => count + segment.words.length, 0)
773
+ })
774
+
775
+ // Get the updated words from the global segment
776
+ const updatedWords = updatedSegment.words
777
+
778
+ // Create a new array of segments with the same structure as the original
779
+ const updatedSegments = []
780
+ let wordIndex = 0
781
+
782
+ // Distribute words to segments based on the original segment sizes
783
+ for (const segment of data.corrected_segments) {
784
+ const originalWordCount = segment.words.length
785
+
786
+ // Get the words for this segment from the updated global segment
787
+ const segmentWords = []
788
+ const endIndex = Math.min(wordIndex + originalWordCount, updatedWords.length)
789
+
790
+ for (let i = wordIndex; i < endIndex; i++) {
791
+ segmentWords.push(updatedWords[i])
792
+ }
793
+
794
+ // Update the word index for the next segment
795
+ wordIndex = endIndex
796
+
797
+ // If we have words for this segment, create an updated segment
798
+ if (segmentWords.length > 0) {
799
+ // Recalculate segment start and end times
800
+ const validStartTimes = segmentWords.map(w => w.start_time).filter((t): t is number => t !== null)
801
+ const validEndTimes = segmentWords.map(w => w.end_time).filter((t): t is number => t !== null)
802
+
803
+ const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null
804
+ const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null
805
+
806
+ // Create the updated segment
807
+ updatedSegments.push({
808
+ ...segment,
809
+ words: segmentWords,
810
+ text: segmentWords.map(w => w.text).join(' '),
811
+ start_time: segmentStartTime,
812
+ end_time: segmentEndTime
813
+ })
814
+ }
815
+ }
816
+
817
+ // If there are any remaining words, add them to the last segment
818
+ if (wordIndex < updatedWords.length) {
819
+ const remainingWords = updatedWords.slice(wordIndex)
820
+ const lastSegment = updatedSegments[updatedSegments.length - 1]
821
+
822
+ // Combine the remaining words with the last segment
823
+ const combinedWords = [...lastSegment.words, ...remainingWords]
824
+
825
+ // Recalculate segment start and end times
826
+ const validStartTimes = combinedWords.map(w => w.start_time).filter((t): t is number => t !== null)
827
+ const validEndTimes = combinedWords.map(w => w.end_time).filter((t): t is number => t !== null)
828
+
829
+ const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null
830
+ const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null
831
+
832
+ // Update the last segment
833
+ updatedSegments[updatedSegments.length - 1] = {
834
+ ...lastSegment,
835
+ words: combinedWords,
836
+ text: combinedWords.map(w => w.text).join(' '),
837
+ start_time: segmentStartTime,
838
+ end_time: segmentEndTime
839
+ }
840
+ }
841
+
842
+ console.log('Global Edit - Updated Segments with new approach:', {
843
+ segmentCount: updatedSegments.length,
844
+ firstSegmentWordCount: updatedSegments[0]?.words.length,
845
+ totalWordCount: updatedSegments.reduce((count, segment) => count + segment.words.length, 0),
846
+ originalTotalWordCount: data.corrected_segments.reduce((count, segment) => count + segment.words.length, 0)
847
+ })
848
+
849
+ // Update the data with the new segments
850
+ setData({
851
+ ...data,
852
+ corrected_segments: updatedSegments
853
+ })
854
+
855
+ // Close the modal
856
+ setIsEditAllModalOpen(false)
857
+ setGlobalEditSegment(null)
858
+ }, [data])
859
+
860
+ // Memoize the metric click handlers
861
+ const metricClickHandlers = useMemo(() => ({
862
+ anchor: () => handleFlash('anchor'),
863
+ corrected: () => handleFlash('corrected'),
864
+ uncorrected: () => handleFlash('uncorrected')
865
+ }), [handleFlash]);
866
+
867
+ // Determine if any modal is open to disable highlighting
868
+ const isAnyModalOpenMemo = useMemo(() => isAnyModalOpen, [isAnyModalOpen]);
869
+
475
870
  return (
476
871
  <Box sx={{
477
872
  p: 1,
@@ -479,15 +874,11 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
479
874
  maxWidth: '100%',
480
875
  overflowX: 'hidden'
481
876
  }}>
482
- <Header
877
+ <MemoizedHeader
483
878
  isReadOnly={isReadOnly}
484
879
  onFileLoad={onFileLoad}
485
880
  data={data}
486
- onMetricClick={{
487
- anchor: () => handleFlash('anchor'),
488
- corrected: () => handleFlash('corrected'),
489
- uncorrected: () => handleFlash('uncorrected')
490
- }}
881
+ onMetricClick={metricClickHandlers}
491
882
  effectiveMode={effectiveMode}
492
883
  onModeChange={setInteractionMode}
493
884
  apiClient={apiClient}
@@ -498,12 +889,12 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
498
889
  onHandlerClick={handleHandlerClick}
499
890
  onAddLyrics={() => setIsAddLyricsModalOpen(true)}
500
891
  onFindReplace={() => setIsFindReplaceModalOpen(true)}
501
- onEditSync={() => setIsGlobalSyncEditorOpen(true)}
892
+ onEditAll={handleEditAll}
502
893
  />
503
894
 
504
- <Grid container spacing={1} direction={isMobile ? 'column' : 'row'}>
895
+ <Grid container direction={isMobile ? 'column' : 'row'}>
505
896
  <Grid item xs={12} md={6}>
506
- <TranscriptionView
897
+ <MemoizedTranscriptionView
507
898
  data={data}
508
899
  mode={effectiveMode}
509
900
  onElementClick={setModalContent}
@@ -514,6 +905,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
514
905
  onPlaySegment={handlePlaySegment}
515
906
  currentTime={currentAudioTime}
516
907
  anchors={data.anchor_sequences}
908
+ disableHighlighting={isAnyModalOpenMemo}
517
909
  />
518
910
  {!isReadOnly && apiClient && (
519
911
  <Box sx={{
@@ -543,7 +935,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
543
935
  )}
544
936
  </Grid>
545
937
  <Grid item xs={12} md={6}>
546
- <ReferenceView
938
+ <MemoizedReferenceView
547
939
  referenceSources={data.reference_lyrics}
548
940
  anchors={data.anchor_sequences}
549
941
  gaps={data.gap_sequences}
@@ -560,6 +952,27 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
560
952
  </Grid>
561
953
  </Grid>
562
954
 
955
+ <EditModal
956
+ open={isEditAllModalOpen}
957
+ onClose={() => {
958
+ setIsEditAllModalOpen(false)
959
+ setGlobalEditSegment(null)
960
+ setOriginalGlobalSegment(null)
961
+ setOriginalTranscribedGlobalSegment(null)
962
+ handleSetModalSpacebarHandler(undefined)
963
+ }}
964
+ segment={globalEditSegment}
965
+ segmentIndex={null}
966
+ originalSegment={originalGlobalSegment}
967
+ onSave={handleSaveGlobalEdit}
968
+ onPlaySegment={handlePlaySegment}
969
+ currentTime={currentAudioTime}
970
+ setModalSpacebarHandler={handleSetModalSpacebarHandler}
971
+ originalTranscribedSegment={originalTranscribedGlobalSegment}
972
+ isGlobal={true}
973
+ isLoading={isLoadingGlobalEdit}
974
+ />
975
+
563
976
  <EditModal
564
977
  open={Boolean(editModalSegment)}
565
978
  onClose={() => {
@@ -581,7 +994,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
581
994
  editModalSegment?.segment && editModalSegment?.index !== null
582
995
  ? originalData.original_segments.find(
583
996
  (s: LyricsSegment) => s.id === editModalSegment.segment.id
584
- ) || null
997
+ ) || null
585
998
  : null
586
999
  }
587
1000
  />
@@ -609,23 +1022,6 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
609
1022
  onReplace={handleFindReplace}
610
1023
  data={data}
611
1024
  />
612
-
613
- {/* Add GlobalSyncEditor modal */}
614
- {!isReadOnly && (
615
- <GlobalSyncEditor
616
- open={isGlobalSyncEditorOpen}
617
- onClose={() => setIsGlobalSyncEditorOpen(false)}
618
- segments={data.corrected_segments || []}
619
- onSave={(updatedSegments) => {
620
- const newData = { ...data, corrected_segments: updatedSegments };
621
- setData(newData);
622
- saveData(newData, initialData);
623
- }}
624
- onPlaySegment={handlePlaySegment}
625
- currentTime={currentAudioTime}
626
- setModalSpacebarHandler={handleSetModalSpacebarHandler}
627
- />
628
- )}
629
1025
  </Box>
630
1026
  )
631
1027
  }