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
@@ -1,15 +1,16 @@
1
+ import { useState, useEffect, useCallback, useMemo, memo } from 'react'
1
2
  import {
2
3
  AnchorSequence,
3
4
  CorrectionData,
4
5
  GapSequence,
5
6
  HighlightInfo,
6
7
  InteractionMode,
7
- LyricsSegment
8
+ LyricsSegment,
9
+ ReferenceSource,
10
+ WordCorrection
8
11
  } from '../types'
9
12
  import { Box, Button, Grid, useMediaQuery, useTheme } from '@mui/material'
10
- import { useCallback, useState, useEffect } from 'react'
11
13
  import { ApiClient } from '../api'
12
- import DetailsModal from './DetailsModal'
13
14
  import ReferenceView from './ReferenceView'
14
15
  import TranscriptionView from './TranscriptionView'
15
16
  import { WordClickInfo, FlashType } from './shared/types'
@@ -19,12 +20,17 @@ import {
19
20
  addSegmentBefore,
20
21
  splitSegment,
21
22
  deleteSegment,
22
- updateSegment
23
+ updateSegment,
24
+ mergeSegment,
25
+ findAndReplace
23
26
  } from './shared/utils/segmentOperations'
24
27
  import { loadSavedData, saveData, clearSavedData } from './shared/utils/localStorage'
25
- import { setupKeyboardHandlers, setModalHandler } from './shared/utils/keyboardHandlers'
28
+ import { setupKeyboardHandlers, setModalHandler, getModalState } from './shared/utils/keyboardHandlers'
26
29
  import Header from './Header'
27
- import { findWordById, getWordsFromIds } from './shared/utils/wordUtils'
30
+ import { getWordsFromIds } from './shared/utils/wordUtils'
31
+ import AddLyricsModal from './AddLyricsModal'
32
+ import { RestoreFromTrash, OndemandVideo } from '@mui/icons-material'
33
+ import FindReplaceModal from './FindReplaceModal'
28
34
 
29
35
  // Add type for window augmentation at the top of the file
30
36
  declare global {
@@ -34,6 +40,7 @@ declare global {
34
40
  }
35
41
  }
36
42
 
43
+ const debugLog = false;
37
44
  export interface LyricsAnalyzerProps {
38
45
  data: CorrectionData
39
46
  onFileLoad: () => void
@@ -59,6 +66,160 @@ export type ModalContent = {
59
66
  }
60
67
  }
61
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
+
62
223
  export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly, audioHash }: LyricsAnalyzerProps) {
63
224
  const [modalContent, setModalContent] = useState<ModalContent | null>(null)
64
225
  const [flashingType, setFlashingType] = useState<FlashType>(null)
@@ -73,35 +234,45 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
73
234
  const [isReviewComplete, setIsReviewComplete] = useState(false)
74
235
  const [data, setData] = useState(initialData)
75
236
  const [originalData] = useState(() => JSON.parse(JSON.stringify(initialData)))
76
- const [interactionMode, setInteractionMode] = useState<InteractionMode>('details')
237
+ const [interactionMode, setInteractionMode] = useState<InteractionMode>('edit')
77
238
  const [isShiftPressed, setIsShiftPressed] = useState(false)
78
- const [isCtrlPressed, setIsCtrlPressed] = useState(false)
79
239
  const [editModalSegment, setEditModalSegment] = useState<{
80
240
  segment: LyricsSegment
81
241
  index: number
82
242
  originalSegment: LyricsSegment
83
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)
84
249
  const [isReviewModalOpen, setIsReviewModalOpen] = useState(false)
85
250
  const [currentAudioTime, setCurrentAudioTime] = useState(0)
86
251
  const [isUpdatingHandlers, setIsUpdatingHandlers] = useState(false)
87
252
  const [flashingHandler, setFlashingHandler] = useState<string | null>(null)
253
+ const [isAddingLyrics, setIsAddingLyrics] = useState(false)
254
+ const [isAddLyricsModalOpen, setIsAddLyricsModalOpen] = useState(false)
255
+ const [isAnyModalOpen, setIsAnyModalOpen] = useState(false)
256
+ const [isFindReplaceModalOpen, setIsFindReplaceModalOpen] = useState(false)
88
257
  const theme = useTheme()
89
258
  const isMobile = useMediaQuery(theme.breakpoints.down('md'))
90
259
 
91
260
  // Update debug logging to use new ID-based structure
92
261
  useEffect(() => {
93
- console.log('LyricsAnalyzer Initial Data:', {
94
- hasData: !!initialData,
95
- segmentsCount: initialData?.corrected_segments?.length ?? 0,
96
- anchorsCount: initialData?.anchor_sequences?.length ?? 0,
97
- gapsCount: initialData?.gap_sequences?.length ?? 0,
98
- firstAnchor: initialData?.anchor_sequences?.[0] && {
99
- transcribedWordIds: initialData.anchor_sequences[0].transcribed_word_ids,
100
- referenceWordIds: initialData.anchor_sequences[0].reference_word_ids
101
- },
102
- firstSegment: initialData?.corrected_segments?.[0],
103
- referenceSources: Object.keys(initialData?.reference_lyrics ?? {})
104
- });
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
+ }
105
276
  }, [initialData]);
106
277
 
107
278
  // Load saved data
@@ -121,29 +292,57 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
121
292
 
122
293
  // Keyboard handlers
123
294
  useEffect(() => {
124
- console.log('Setting up keyboard handlers in LyricsAnalyzer')
295
+ const { currentModalHandler } = getModalState()
296
+
297
+ if (debugLog) {
298
+ console.log('LyricsAnalyzer - Setting up keyboard effect', {
299
+ isAnyModalOpen,
300
+ hasSpacebarHandler: !!currentModalHandler
301
+ })
302
+ }
125
303
 
126
304
  const { handleKeyDown, handleKeyUp } = setupKeyboardHandlers({
127
305
  setIsShiftPressed,
128
- setIsCtrlPressed
129
306
  })
130
307
 
131
- console.log('Adding keyboard event listeners')
308
+ // Always add keyboard listeners
309
+ if (debugLog) {
310
+ console.log('LyricsAnalyzer - Adding keyboard event listeners')
311
+ }
132
312
  window.addEventListener('keydown', handleKeyDown)
133
313
  window.addEventListener('keyup', handleKeyUp)
134
314
 
315
+ // Reset modifier states when a modal opens
316
+ if (isAnyModalOpen) {
317
+ setIsShiftPressed(false)
318
+ }
319
+
320
+ // Cleanup function
135
321
  return () => {
136
- console.log('Removing keyboard event listeners')
322
+ if (debugLog) {
323
+ console.log('LyricsAnalyzer - Cleanup effect running')
324
+ }
137
325
  window.removeEventListener('keydown', handleKeyDown)
138
326
  window.removeEventListener('keyup', handleKeyUp)
139
327
  document.body.style.userSelect = ''
140
328
  }
141
- }, [setIsShiftPressed, setIsCtrlPressed])
329
+ }, [setIsShiftPressed, isAnyModalOpen])
330
+
331
+ // Update modal state tracking
332
+ useEffect(() => {
333
+ const modalOpen = Boolean(
334
+ modalContent ||
335
+ editModalSegment ||
336
+ isReviewModalOpen ||
337
+ isAddLyricsModalOpen ||
338
+ isFindReplaceModalOpen ||
339
+ isEditAllModalOpen
340
+ )
341
+ setIsAnyModalOpen(modalOpen)
342
+ }, [modalContent, editModalSegment, isReviewModalOpen, isAddLyricsModalOpen, isFindReplaceModalOpen, isEditAllModalOpen])
142
343
 
143
344
  // Calculate effective mode based on modifier key states
144
- const effectiveMode = isShiftPressed ? 'highlight' :
145
- isCtrlPressed ? 'edit' :
146
- interactionMode
345
+ const effectiveMode = isShiftPressed ? 'highlight' : interactionMode
147
346
 
148
347
  const handleFlash = useCallback((type: FlashType, info?: HighlightInfo) => {
149
348
  setFlashingType(null)
@@ -164,7 +363,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
164
363
  }, [])
165
364
 
166
365
  const handleWordClick = useCallback((info: WordClickInfo) => {
167
- console.log('LyricsAnalyzer handleWordClick:', { info });
366
+ if (debugLog) {
367
+ console.log('LyricsAnalyzer handleWordClick:', { info });
368
+ }
168
369
 
169
370
  if (effectiveMode === 'highlight') {
170
371
  // Find if this word is part of a correction
@@ -217,10 +418,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
217
418
  start_time: sourceWords[0]?.start_time ?? null,
218
419
  end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
219
420
  }
220
- return [
221
- source,
222
- getWordsFromIds([tempSourceSegment], ids)
223
- ]
421
+ return [source, getWordsFromIds([tempSourceSegment], ids)]
224
422
  })
225
423
  );
226
424
 
@@ -236,13 +434,11 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
236
434
 
237
435
  // Find if this word is part of a gap sequence
238
436
  const gap = data.gap_sequences?.find(g =>
239
- g.transcribed_word_ids.includes(info.word_id) ||
240
- Object.values(g.reference_word_ids).some(ids =>
241
- ids.includes(info.word_id)
242
- )
437
+ g.transcribed_word_ids.includes(info.word_id)
243
438
  );
244
439
 
245
440
  if (gap) {
441
+ // Create a temporary segment containing all words
246
442
  const allWords = data.corrected_segments.flatMap(s => s.words)
247
443
  const tempSegment: LyricsSegment = {
248
444
  id: 'temp',
@@ -267,10 +463,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
267
463
  start_time: sourceWords[0]?.start_time ?? null,
268
464
  end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
269
465
  }
270
- return [
271
- source,
272
- getWordsFromIds([tempSourceSegment], ids)
273
- ]
466
+ return [source, getWordsFromIds([tempSourceSegment], ids)]
274
467
  })
275
468
  );
276
469
 
@@ -297,30 +490,6 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
297
490
  originalSegment: JSON.parse(JSON.stringify(segment))
298
491
  });
299
492
  }
300
- } else if (effectiveMode === 'details') {
301
- if (info.type === 'anchor' && info.anchor) {
302
- const word = findWordById(data.corrected_segments, info.word_id);
303
- setModalContent({
304
- type: 'anchor',
305
- data: {
306
- ...info.anchor,
307
- wordId: info.word_id,
308
- word: word?.text,
309
- anchor_sequences: data.anchor_sequences
310
- }
311
- });
312
- } else if (info.type === 'gap' && info.gap) {
313
- const word = findWordById(data.corrected_segments, info.word_id);
314
- setModalContent({
315
- type: 'gap',
316
- data: {
317
- ...info.gap,
318
- wordId: info.word_id,
319
- word: word?.text || '',
320
- anchor_sequences: data.anchor_sequences
321
- }
322
- });
323
- }
324
493
  }
325
494
  }, [data, effectiveMode, setModalContent]);
326
495
 
@@ -344,7 +513,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
344
513
  if (!apiClient) return
345
514
 
346
515
  try {
347
- console.log('Submitting changes to server')
516
+ if (debugLog) {
517
+ console.log('Submitting changes to server')
518
+ }
348
519
  await apiClient.submitCorrections(data)
349
520
 
350
521
  setIsReviewComplete(true)
@@ -372,7 +543,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
372
543
  setModalContent(null)
373
544
  setFlashingType(null)
374
545
  setHighlightInfo(null)
375
- setInteractionMode('details')
546
+ setInteractionMode('edit')
376
547
  }
377
548
  }, [initialData])
378
549
 
@@ -389,6 +560,12 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
389
560
  }
390
561
  }, [data])
391
562
 
563
+ const handleMergeSegment = useCallback((segmentIndex: number, mergeWithNext: boolean) => {
564
+ const newData = mergeSegment(data, segmentIndex, mergeWithNext)
565
+ setData(newData)
566
+ setEditModalSegment(null)
567
+ }, [data])
568
+
392
569
  const handleHandlerToggle = useCallback(async (handler: string, enabled: boolean) => {
393
570
  if (!apiClient) return
394
571
 
@@ -427,15 +604,20 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
427
604
  }, [apiClient, data.metadata.enabled_handlers, handleFlash])
428
605
 
429
606
  const handleHandlerClick = useCallback((handler: string) => {
430
- console.log('Handler clicked:', handler);
607
+ if (debugLog) {
608
+ console.log('Handler clicked:', handler);
609
+ }
431
610
  setFlashingHandler(handler);
432
611
  setFlashingType('handler');
433
- console.log('Set flashingHandler to:', handler);
434
- console.log('Set flashingType to: handler');
435
-
612
+ if (debugLog) {
613
+ console.log('Set flashingHandler to:', handler);
614
+ console.log('Set flashingType to: handler');
615
+ }
436
616
  // Clear the flash after a short delay
437
617
  setTimeout(() => {
438
- console.log('Clearing flash state');
618
+ if (debugLog) {
619
+ console.log('Clearing flash state');
620
+ }
439
621
  setFlashingHandler(null);
440
622
  setFlashingType(null);
441
623
  }, 1500);
@@ -443,26 +625,260 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
443
625
 
444
626
  // Wrap setModalSpacebarHandler in useCallback
445
627
  const handleSetModalSpacebarHandler = useCallback((handler: (() => (e: KeyboardEvent) => void) | undefined) => {
628
+ if (debugLog) {
629
+ console.log('LyricsAnalyzer - Setting modal handler:', {
630
+ hasHandler: !!handler
631
+ })
632
+ }
446
633
  // Update the global modal handler
447
634
  setModalHandler(handler ? handler() : undefined, !!handler)
448
635
  }, [])
449
636
 
637
+ // Add new handler for adding lyrics
638
+ const handleAddLyrics = useCallback(async (source: string, lyrics: string) => {
639
+ if (!apiClient) return
640
+
641
+ try {
642
+ setIsAddingLyrics(true)
643
+ const newData = await apiClient.addLyrics(source, lyrics)
644
+ setData(newData)
645
+ } finally {
646
+ setIsAddingLyrics(false)
647
+ }
648
+ }, [apiClient])
649
+
650
+ const handleFindReplace = (findText: string, replaceText: string, options: { caseSensitive: boolean, useRegex: boolean, fullTextMode: boolean }) => {
651
+ const newData = findAndReplace(data, findText, replaceText, options)
652
+ setData(newData)
653
+ }
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
+
450
870
  return (
451
871
  <Box sx={{
452
- p: 3,
453
- pb: 6,
872
+ p: 1,
873
+ pb: 3,
454
874
  maxWidth: '100%',
455
875
  overflowX: 'hidden'
456
876
  }}>
457
- <Header
877
+ <MemoizedHeader
458
878
  isReadOnly={isReadOnly}
459
879
  onFileLoad={onFileLoad}
460
880
  data={data}
461
- onMetricClick={{
462
- anchor: () => handleFlash('anchor'),
463
- corrected: () => handleFlash('corrected'),
464
- uncorrected: () => handleFlash('uncorrected')
465
- }}
881
+ onMetricClick={metricClickHandlers}
466
882
  effectiveMode={effectiveMode}
467
883
  onModeChange={setInteractionMode}
468
884
  apiClient={apiClient}
@@ -471,11 +887,14 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
471
887
  onHandlerToggle={handleHandlerToggle}
472
888
  isUpdatingHandlers={isUpdatingHandlers}
473
889
  onHandlerClick={handleHandlerClick}
890
+ onAddLyrics={() => setIsAddLyricsModalOpen(true)}
891
+ onFindReplace={() => setIsFindReplaceModalOpen(true)}
892
+ onEditAll={handleEditAll}
474
893
  />
475
894
 
476
- <Grid container spacing={2} direction={isMobile ? 'column' : 'row'}>
895
+ <Grid container direction={isMobile ? 'column' : 'row'}>
477
896
  <Grid item xs={12} md={6}>
478
- <TranscriptionView
897
+ <MemoizedTranscriptionView
479
898
  data={data}
480
899
  mode={effectiveMode}
481
900
  onElementClick={setModalContent}
@@ -486,10 +905,37 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
486
905
  onPlaySegment={handlePlaySegment}
487
906
  currentTime={currentAudioTime}
488
907
  anchors={data.anchor_sequences}
908
+ disableHighlighting={isAnyModalOpenMemo}
489
909
  />
910
+ {!isReadOnly && apiClient && (
911
+ <Box sx={{
912
+ mt: 2,
913
+ mb: 3,
914
+ display: 'flex',
915
+ justifyContent: 'space-between',
916
+ width: '100%'
917
+ }}>
918
+ <Button
919
+ variant="outlined"
920
+ color="warning"
921
+ onClick={handleResetCorrections}
922
+ startIcon={<RestoreFromTrash />}
923
+ >
924
+ Reset Corrections
925
+ </Button>
926
+ <Button
927
+ variant="contained"
928
+ onClick={handleFinishReview}
929
+ disabled={isReviewComplete}
930
+ endIcon={<OndemandVideo />}
931
+ >
932
+ {isReviewComplete ? 'Review Complete' : 'Preview Video'}
933
+ </Button>
934
+ </Box>
935
+ )}
490
936
  </Grid>
491
937
  <Grid item xs={12} md={6}>
492
- <ReferenceView
938
+ <MemoizedReferenceView
493
939
  referenceSources={data.reference_lyrics}
494
940
  anchors={data.anchor_sequences}
495
941
  gaps={data.gap_sequences}
@@ -506,12 +952,25 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
506
952
  </Grid>
507
953
  </Grid>
508
954
 
509
- <DetailsModal
510
- open={modalContent !== null}
511
- content={modalContent}
512
- onClose={() => setModalContent(null)}
513
- allCorrections={data.corrections}
514
- referenceLyrics={data.reference_lyrics}
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}
515
974
  />
516
975
 
517
976
  <EditModal
@@ -527,9 +986,17 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
527
986
  onDelete={handleDeleteSegment}
528
987
  onAddSegment={handleAddSegment}
529
988
  onSplitSegment={handleSplitSegment}
989
+ onMergeSegment={handleMergeSegment}
530
990
  onPlaySegment={handlePlaySegment}
531
991
  currentTime={currentAudioTime}
532
992
  setModalSpacebarHandler={handleSetModalSpacebarHandler}
993
+ originalTranscribedSegment={
994
+ editModalSegment?.segment && editModalSegment?.index !== null
995
+ ? originalData.original_segments.find(
996
+ (s: LyricsSegment) => s.id === editModalSegment.segment.id
997
+ ) || null
998
+ : null
999
+ }
533
1000
  />
534
1001
 
535
1002
  <ReviewChangesModal
@@ -542,24 +1009,19 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
542
1009
  setModalSpacebarHandler={handleSetModalSpacebarHandler}
543
1010
  />
544
1011
 
545
- {!isReadOnly && apiClient && (
546
- <Box sx={{ mt: 2, mb: 3, display: 'flex', gap: 2 }}>
547
- <Button
548
- variant="contained"
549
- onClick={handleFinishReview}
550
- disabled={isReviewComplete}
551
- >
552
- {isReviewComplete ? 'Review Complete' : 'Finish Review'}
553
- </Button>
554
- <Button
555
- variant="outlined"
556
- color="warning"
557
- onClick={handleResetCorrections}
558
- >
559
- Reset Corrections
560
- </Button>
561
- </Box>
562
- )}
1012
+ <AddLyricsModal
1013
+ open={isAddLyricsModalOpen}
1014
+ onClose={() => setIsAddLyricsModalOpen(false)}
1015
+ onSubmit={handleAddLyrics}
1016
+ isSubmitting={isAddingLyrics}
1017
+ />
1018
+
1019
+ <FindReplaceModal
1020
+ open={isFindReplaceModalOpen}
1021
+ onClose={() => setIsFindReplaceModalOpen(false)}
1022
+ onReplace={handleFindReplace}
1023
+ data={data}
1024
+ />
563
1025
  </Box>
564
1026
  )
565
1027
  }