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