lyrics-transcriber 0.40.0__py3-none-any.whl → 0.42.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 (79) hide show
  1. lyrics_transcriber/cli/cli_main.py +7 -0
  2. lyrics_transcriber/core/config.py +1 -0
  3. lyrics_transcriber/core/controller.py +30 -52
  4. lyrics_transcriber/correction/anchor_sequence.py +325 -150
  5. lyrics_transcriber/correction/corrector.py +224 -107
  6. lyrics_transcriber/correction/handlers/base.py +28 -10
  7. lyrics_transcriber/correction/handlers/extend_anchor.py +47 -24
  8. lyrics_transcriber/correction/handlers/levenshtein.py +75 -33
  9. lyrics_transcriber/correction/handlers/llm.py +290 -0
  10. lyrics_transcriber/correction/handlers/no_space_punct_match.py +81 -36
  11. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +46 -26
  12. lyrics_transcriber/correction/handlers/repeat.py +28 -11
  13. lyrics_transcriber/correction/handlers/sound_alike.py +68 -32
  14. lyrics_transcriber/correction/handlers/syllables_match.py +80 -30
  15. lyrics_transcriber/correction/handlers/word_count_match.py +36 -19
  16. lyrics_transcriber/correction/handlers/word_operations.py +68 -22
  17. lyrics_transcriber/correction/text_utils.py +3 -7
  18. lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
  19. lyrics_transcriber/frontend/.yarn/releases/yarn-4.6.0.cjs +934 -0
  20. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  21. lyrics_transcriber/frontend/dist/assets/{index-DKnNJHRK.js → index-coH8y7gV.js} +16284 -9032
  22. lyrics_transcriber/frontend/dist/assets/index-coH8y7gV.js.map +1 -0
  23. lyrics_transcriber/frontend/dist/index.html +1 -1
  24. lyrics_transcriber/frontend/package.json +6 -2
  25. lyrics_transcriber/frontend/src/App.tsx +18 -2
  26. lyrics_transcriber/frontend/src/api.ts +103 -6
  27. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +7 -6
  28. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +86 -59
  29. lyrics_transcriber/frontend/src/components/EditModal.tsx +93 -43
  30. lyrics_transcriber/frontend/src/components/FileUpload.tsx +2 -2
  31. lyrics_transcriber/frontend/src/components/Header.tsx +251 -0
  32. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +303 -265
  33. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +117 -0
  34. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +125 -40
  35. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +129 -115
  36. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +59 -78
  37. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +40 -16
  38. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +4 -10
  39. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +137 -68
  40. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +1 -1
  41. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +85 -115
  42. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  43. lyrics_transcriber/frontend/src/components/shared/types.ts +15 -7
  44. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +35 -0
  45. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  46. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +7 -7
  47. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +121 -0
  48. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  49. lyrics_transcriber/frontend/src/types.js +2 -0
  50. lyrics_transcriber/frontend/src/types.ts +70 -49
  51. lyrics_transcriber/frontend/src/validation.ts +132 -0
  52. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  53. lyrics_transcriber/frontend/yarn.lock +3752 -0
  54. lyrics_transcriber/lyrics/base_lyrics_provider.py +75 -12
  55. lyrics_transcriber/lyrics/file_provider.py +6 -5
  56. lyrics_transcriber/lyrics/genius.py +5 -2
  57. lyrics_transcriber/lyrics/spotify.py +58 -21
  58. lyrics_transcriber/output/ass/config.py +16 -5
  59. lyrics_transcriber/output/cdg.py +8 -8
  60. lyrics_transcriber/output/generator.py +29 -14
  61. lyrics_transcriber/output/plain_text.py +15 -10
  62. lyrics_transcriber/output/segment_resizer.py +16 -3
  63. lyrics_transcriber/output/subtitles.py +56 -2
  64. lyrics_transcriber/output/video.py +107 -1
  65. lyrics_transcriber/review/__init__.py +0 -1
  66. lyrics_transcriber/review/server.py +337 -164
  67. lyrics_transcriber/transcribers/audioshake.py +3 -0
  68. lyrics_transcriber/transcribers/base_transcriber.py +11 -3
  69. lyrics_transcriber/transcribers/whisper.py +11 -1
  70. lyrics_transcriber/types.py +151 -105
  71. lyrics_transcriber/utils/word_utils.py +27 -0
  72. {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/METADATA +3 -1
  73. {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/RECORD +76 -63
  74. {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/WHEEL +1 -1
  75. lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js.map +0 -1
  76. lyrics_transcriber/frontend/package-lock.json +0 -4260
  77. lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +0 -202
  78. {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/LICENSE +0 -0
  79. {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/entry_points.txt +0 -0
@@ -6,22 +6,25 @@ import {
6
6
  InteractionMode,
7
7
  LyricsSegment
8
8
  } from '../types'
9
- import LockIcon from '@mui/icons-material/Lock'
10
- import UploadFileIcon from '@mui/icons-material/UploadFile'
11
- import { Box, Button, Grid, Typography, useMediaQuery, useTheme } from '@mui/material'
9
+ import { Box, Button, Grid, useMediaQuery, useTheme } from '@mui/material'
12
10
  import { useCallback, useState, useEffect } from 'react'
13
11
  import { ApiClient } from '../api'
14
- import CorrectionMetrics from './CorrectionMetrics'
15
12
  import DetailsModal from './DetailsModal'
16
- import ModeSelector from './ModeSelector'
17
13
  import ReferenceView from './ReferenceView'
18
14
  import TranscriptionView from './TranscriptionView'
19
15
  import { WordClickInfo, FlashType } from './shared/types'
20
16
  import EditModal from './EditModal'
21
17
  import ReviewChangesModal from './ReviewChangesModal'
22
- import AudioPlayer from './AudioPlayer'
23
- import { nanoid } from 'nanoid'
24
- import { initializeDataWithIds, normalizeDataForSubmission } from './shared/utils/initializeDataWithIds'
18
+ import {
19
+ addSegmentBefore,
20
+ splitSegment,
21
+ deleteSegment,
22
+ updateSegment
23
+ } from './shared/utils/segmentOperations'
24
+ import { loadSavedData, saveData, clearSavedData } from './shared/utils/localStorage'
25
+ import { setupKeyboardHandlers } from './shared/utils/keyboardHandlers'
26
+ import Header from './Header'
27
+ import { findWordById, getWordsFromIds } from './shared/utils/wordUtils'
25
28
 
26
29
  // Add type for window augmentation at the top of the file
27
30
  declare global {
@@ -31,12 +34,13 @@ declare global {
31
34
  }
32
35
  }
33
36
 
34
- interface LyricsAnalyzerProps {
37
+ export interface LyricsAnalyzerProps {
35
38
  data: CorrectionData
36
39
  onFileLoad: () => void
37
40
  onShowMetadata: () => void
38
41
  apiClient: ApiClient | null
39
42
  isReadOnly: boolean
43
+ audioHash: string
40
44
  }
41
45
 
42
46
  export type ModalContent = {
@@ -44,26 +48,30 @@ export type ModalContent = {
44
48
  data: AnchorSequence & {
45
49
  wordId: string
46
50
  word?: string
51
+ anchor_sequences: AnchorSequence[]
47
52
  }
48
53
  } | {
49
54
  type: 'gap'
50
55
  data: GapSequence & {
51
56
  wordId: string
52
57
  word: string
58
+ anchor_sequences: AnchorSequence[]
53
59
  }
54
60
  }
55
61
 
56
- export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly }: LyricsAnalyzerProps) {
62
+ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly, audioHash }: LyricsAnalyzerProps) {
57
63
  const [modalContent, setModalContent] = useState<ModalContent | null>(null)
58
64
  const [flashingType, setFlashingType] = useState<FlashType>(null)
59
65
  const [highlightInfo, setHighlightInfo] = useState<HighlightInfo | null>(null)
60
66
  const [currentSource, setCurrentSource] = useState<string>(() => {
61
- const availableSources = Object.keys(initialData.reference_texts)
67
+ if (!initialData?.reference_lyrics) {
68
+ return ''
69
+ }
70
+ const availableSources = Object.keys(initialData.reference_lyrics)
62
71
  return availableSources.length > 0 ? availableSources[0] : ''
63
72
  })
64
73
  const [isReviewComplete, setIsReviewComplete] = useState(false)
65
- const [data, setData] = useState(() => initializeDataWithIds(initialData))
66
- // Create deep copy of initial data for comparison later
74
+ const [data, setData] = useState(initialData)
67
75
  const [originalData] = useState(() => JSON.parse(JSON.stringify(initialData)))
68
76
  const [interactionMode, setInteractionMode] = useState<InteractionMode>('details')
69
77
  const [isShiftPressed, setIsShiftPressed] = useState(false)
@@ -75,106 +83,48 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
75
83
  } | null>(null)
76
84
  const [isReviewModalOpen, setIsReviewModalOpen] = useState(false)
77
85
  const [currentAudioTime, setCurrentAudioTime] = useState(0)
86
+ const [isUpdatingHandlers, setIsUpdatingHandlers] = useState(false)
87
+ const [flashingHandler, setFlashingHandler] = useState<string | null>(null)
78
88
  const theme = useTheme()
79
89
  const isMobile = useMediaQuery(theme.breakpoints.down('md'))
80
90
 
81
- // Simple hash function for generating storage keys
82
- const generateStorageKey = (text: string): string => {
83
- let hash = 0;
84
- for (let i = 0; i < text.length; i++) {
85
- const char = text.charCodeAt(i);
86
- hash = ((hash << 5) - hash) + char;
87
- hash = hash & hash; // Convert to 32-bit integer
88
- }
89
- return `song_${hash}`;
90
- }
91
-
92
- // Add local storage handling
91
+ // Update debug logging to use new ID-based structure
93
92
  useEffect(() => {
94
- const storageKey = generateStorageKey(initialData.transcribed_text);
95
- const savedDataStr = localStorage.getItem('lyrics_analyzer_data');
96
- const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {};
97
-
98
- if (savedDataObj[storageKey]) {
99
- try {
100
- const parsed = savedDataObj[storageKey];
101
- if (parsed.transcribed_text === initialData.transcribed_text) {
102
- const stripIds = (obj: CorrectionData): LyricsSegment[] => {
103
- const clone = JSON.parse(JSON.stringify(obj));
104
- return clone.corrected_segments.map((segment: LyricsSegment) => {
105
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
106
- const { id: _id, ...strippedSegment } = segment;
107
- return {
108
- ...strippedSegment,
109
- words: segment.words.map(word => {
110
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
111
- const { id: _wordId, ...strippedWord } = word;
112
- return strippedWord;
113
- })
114
- };
115
- });
116
- };
117
-
118
- const strippedSaved = stripIds(parsed);
119
- const strippedInitial = stripIds(initialData);
120
-
121
- const hasChanges = JSON.stringify(strippedSaved) !== JSON.stringify(strippedInitial);
122
-
123
- if (hasChanges && window.confirm('Found saved progress for this song. Would you like to restore it?')) {
124
- setData(parsed);
125
- } else if (!hasChanges) {
126
- delete savedDataObj[storageKey];
127
- localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj));
128
- }
129
- }
130
- } catch (error) {
131
- console.error('Failed to parse saved data:', error);
132
- delete savedDataObj[storageKey];
133
- localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj));
134
- }
135
- }
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
+ });
136
105
  }, [initialData]);
137
106
 
138
- // Save to local storage whenever data changes
107
+ // Load saved data
139
108
  useEffect(() => {
140
- if (!isReadOnly) {
141
- const storageKey = generateStorageKey(initialData.transcribed_text);
142
- const savedDataStr = localStorage.getItem('lyrics_analyzer_data');
143
- const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {};
144
-
145
- savedDataObj[storageKey] = data;
146
- localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj));
109
+ const savedData = loadSavedData(initialData)
110
+ if (savedData && window.confirm('Found saved progress for this song. Would you like to restore it?')) {
111
+ setData(savedData)
147
112
  }
148
- }, [data, isReadOnly, initialData.transcribed_text]);
113
+ }, [initialData])
149
114
 
150
- // Update keyboard event handler
115
+ // Save data
151
116
  useEffect(() => {
152
- const handleKeyDown = (e: KeyboardEvent) => {
153
- if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
154
- return
155
- }
156
-
157
- if (e.key === 'Shift') {
158
- setIsShiftPressed(true)
159
- document.body.style.userSelect = 'none'
160
- } else if (e.key === 'Meta') {
161
- setIsCtrlPressed(true)
162
- } else if (e.key === ' ' || e.code === 'Space') {
163
- e.preventDefault()
164
- if (window.toggleAudioPlayback) {
165
- window.toggleAudioPlayback()
166
- }
167
- }
117
+ if (!isReadOnly) {
118
+ saveData(data, initialData)
168
119
  }
120
+ }, [data, isReadOnly, initialData])
169
121
 
170
- const handleKeyUp = (e: KeyboardEvent) => {
171
- if (e.key === 'Shift') {
172
- setIsShiftPressed(false)
173
- document.body.style.userSelect = ''
174
- } else if (e.key === 'Meta') {
175
- setIsCtrlPressed(false)
176
- }
177
- }
122
+ // Keyboard handlers
123
+ useEffect(() => {
124
+ const { handleKeyDown, handleKeyUp } = setupKeyboardHandlers({
125
+ setIsShiftPressed,
126
+ setIsCtrlPressed
127
+ })
178
128
 
179
129
  window.addEventListener('keydown', handleKeyDown)
180
130
  window.addEventListener('keyup', handleKeyUp)
@@ -209,85 +159,175 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
209
159
  }, [])
210
160
 
211
161
  const handleWordClick = useCallback((info: WordClickInfo) => {
212
- if (effectiveMode === 'edit') {
213
- const segment = data.corrected_segments.find(segment =>
162
+ console.log('LyricsAnalyzer handleWordClick:', { info });
163
+
164
+ if (effectiveMode === 'highlight') {
165
+ // Find if this word is part of a correction
166
+ const correction = data.corrections?.find(c =>
167
+ c.corrected_word_id === info.word_id ||
168
+ c.word_id === info.word_id
169
+ );
170
+
171
+ if (correction) {
172
+ setHighlightInfo({
173
+ type: 'correction',
174
+ transcribed_words: [], // Required by type but not used for corrections
175
+ correction: correction
176
+ });
177
+ setFlashingType('word');
178
+ return;
179
+ }
180
+
181
+ // Find if this word is part of an anchor sequence
182
+ const anchor = data.anchor_sequences?.find(a =>
183
+ a.transcribed_word_ids.includes(info.word_id) ||
184
+ Object.values(a.reference_word_ids).some(ids =>
185
+ ids.includes(info.word_id)
186
+ )
187
+ );
188
+
189
+ if (anchor) {
190
+ // Create a temporary segment containing all words
191
+ const allWords = data.corrected_segments.flatMap(s => s.words)
192
+ const tempSegment: LyricsSegment = {
193
+ id: 'temp',
194
+ words: allWords,
195
+ text: allWords.map(w => w.text).join(' '),
196
+ start_time: allWords[0]?.start_time ?? null,
197
+ end_time: allWords[allWords.length - 1]?.end_time ?? null
198
+ }
199
+
200
+ const transcribedWords = getWordsFromIds(
201
+ [tempSegment],
202
+ anchor.transcribed_word_ids
203
+ );
204
+
205
+ const referenceWords = Object.fromEntries(
206
+ Object.entries(anchor.reference_word_ids).map(([source, ids]) => {
207
+ const sourceWords = data.reference_lyrics[source].segments.flatMap(s => s.words)
208
+ const tempSourceSegment: LyricsSegment = {
209
+ id: `temp-${source}`,
210
+ words: sourceWords,
211
+ text: sourceWords.map(w => w.text).join(' '),
212
+ start_time: sourceWords[0]?.start_time ?? null,
213
+ end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
214
+ }
215
+ return [
216
+ source,
217
+ getWordsFromIds([tempSourceSegment], ids)
218
+ ]
219
+ })
220
+ );
221
+
222
+ setHighlightInfo({
223
+ type: 'anchor',
224
+ sequence: anchor,
225
+ transcribed_words: transcribedWords,
226
+ reference_words: referenceWords
227
+ });
228
+ setFlashingType('word');
229
+ return;
230
+ }
231
+
232
+ // Find if this word is part of a gap sequence
233
+ const gap = data.gap_sequences?.find(g =>
234
+ g.transcribed_word_ids.includes(info.word_id) ||
235
+ Object.values(g.reference_word_ids).some(ids =>
236
+ ids.includes(info.word_id)
237
+ )
238
+ );
239
+
240
+ if (gap) {
241
+ const allWords = data.corrected_segments.flatMap(s => s.words)
242
+ const tempSegment: LyricsSegment = {
243
+ id: 'temp',
244
+ words: allWords,
245
+ text: allWords.map(w => w.text).join(' '),
246
+ start_time: allWords[0]?.start_time ?? null,
247
+ end_time: allWords[allWords.length - 1]?.end_time ?? null
248
+ }
249
+
250
+ const transcribedWords = getWordsFromIds(
251
+ [tempSegment],
252
+ gap.transcribed_word_ids
253
+ );
254
+
255
+ const referenceWords = Object.fromEntries(
256
+ Object.entries(gap.reference_word_ids).map(([source, ids]) => {
257
+ const sourceWords = data.reference_lyrics[source].segments.flatMap(s => s.words)
258
+ const tempSourceSegment: LyricsSegment = {
259
+ id: `temp-${source}`,
260
+ words: sourceWords,
261
+ text: sourceWords.map(w => w.text).join(' '),
262
+ start_time: sourceWords[0]?.start_time ?? null,
263
+ end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
264
+ }
265
+ return [
266
+ source,
267
+ getWordsFromIds([tempSourceSegment], ids)
268
+ ]
269
+ })
270
+ );
271
+
272
+ setHighlightInfo({
273
+ type: 'gap',
274
+ sequence: gap,
275
+ transcribed_words: transcribedWords,
276
+ reference_words: referenceWords
277
+ });
278
+ setFlashingType('word');
279
+ return;
280
+ }
281
+ } else if (effectiveMode === 'edit') {
282
+ // Find the segment containing this word
283
+ const segmentIndex = data.corrected_segments.findIndex(segment =>
214
284
  segment.words.some(word => word.id === info.word_id)
215
- )
285
+ );
216
286
 
217
- if (segment) {
218
- const segmentIndex = data.corrected_segments.indexOf(segment)
287
+ if (segmentIndex !== -1) {
288
+ const segment = data.corrected_segments[segmentIndex];
219
289
  setEditModalSegment({
220
290
  segment,
221
291
  index: segmentIndex,
222
- originalSegment: originalData.corrected_segments[segmentIndex]
223
- })
292
+ originalSegment: JSON.parse(JSON.stringify(segment))
293
+ });
224
294
  }
225
- } else {
226
- // Update flash handling for anchors/gaps
295
+ } else if (effectiveMode === 'details') {
227
296
  if (info.type === 'anchor' && info.anchor) {
228
- handleFlash('word', {
297
+ const word = findWordById(data.corrected_segments, info.word_id);
298
+ setModalContent({
229
299
  type: 'anchor',
230
- word_ids: info.anchor.word_ids,
231
- reference_word_ids: info.anchor.reference_word_ids
232
- })
300
+ data: {
301
+ ...info.anchor,
302
+ wordId: info.word_id,
303
+ word: word?.text,
304
+ anchor_sequences: data.anchor_sequences
305
+ }
306
+ });
233
307
  } else if (info.type === 'gap' && info.gap) {
234
- handleFlash('word', {
308
+ const word = findWordById(data.corrected_segments, info.word_id);
309
+ setModalContent({
235
310
  type: 'gap',
236
- word_ids: info.gap.word_ids
237
- })
311
+ data: {
312
+ ...info.gap,
313
+ wordId: info.word_id,
314
+ word: word?.text || '',
315
+ anchor_sequences: data.anchor_sequences
316
+ }
317
+ });
238
318
  }
239
319
  }
240
- }, [effectiveMode, data.corrected_segments, handleFlash, originalData.corrected_segments])
320
+ }, [data, effectiveMode, setModalContent]);
241
321
 
242
322
  const handleUpdateSegment = useCallback((updatedSegment: LyricsSegment) => {
243
323
  if (!editModalSegment) return
244
-
245
- const newData = { ...data }
246
-
247
- // Ensure new words have IDs
248
- updatedSegment.words = updatedSegment.words.map(word => ({
249
- ...word,
250
- id: word.id || nanoid()
251
- }))
252
-
253
- newData.corrected_segments[editModalSegment.index] = updatedSegment
254
-
255
- // Update corrected_text
256
- newData.corrected_text = newData.corrected_segments
257
- .map(segment => segment.text)
258
- .join('\n')
259
-
324
+ const newData = updateSegment(data, editModalSegment.index, updatedSegment)
260
325
  setData(newData)
261
326
  setEditModalSegment(null)
262
327
  }, [data, editModalSegment])
263
328
 
264
329
  const handleDeleteSegment = useCallback((segmentIndex: number) => {
265
- const newData = { ...data }
266
- const deletedSegment = newData.corrected_segments[segmentIndex]
267
-
268
- // Remove segment
269
- newData.corrected_segments = newData.corrected_segments.filter((_, index) => index !== segmentIndex)
270
-
271
- // Update anchor and gap sequences to remove references to deleted words
272
- newData.anchor_sequences = newData.anchor_sequences.map(anchor => ({
273
- ...anchor,
274
- word_ids: anchor.word_ids.filter(id =>
275
- !deletedSegment.words.some(word => word.id === id)
276
- )
277
- }))
278
-
279
- newData.gap_sequences = newData.gap_sequences.map(gap => ({
280
- ...gap,
281
- word_ids: gap.word_ids.filter(id =>
282
- !deletedSegment.words.some(word => word.id === id)
283
- )
284
- }))
285
-
286
- // Update corrected_text
287
- newData.corrected_text = newData.corrected_segments
288
- .map(segment => segment.text)
289
- .join('\n')
290
-
330
+ const newData = deleteSegment(data, segmentIndex)
291
331
  setData(newData)
292
332
  }, [data])
293
333
 
@@ -300,8 +340,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
300
340
 
301
341
  try {
302
342
  console.log('Submitting changes to server')
303
- const dataToSubmit = normalizeDataForSubmission(data)
304
- await apiClient.submitCorrections(dataToSubmit)
343
+ await apiClient.submitCorrections(data)
305
344
 
306
345
  setIsReviewComplete(true)
307
346
  setIsReviewModalOpen(false)
@@ -323,114 +362,105 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
323
362
 
324
363
  const handleResetCorrections = useCallback(() => {
325
364
  if (window.confirm('Are you sure you want to reset all corrections? This cannot be undone.')) {
326
- const storageKey = generateStorageKey(initialData.transcribed_text);
327
- const savedDataStr = localStorage.getItem('lyrics_analyzer_data');
328
- const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {};
365
+ clearSavedData(initialData)
366
+ setData(JSON.parse(JSON.stringify(initialData)))
367
+ setModalContent(null)
368
+ setFlashingType(null)
369
+ setHighlightInfo(null)
370
+ setInteractionMode('details')
371
+ }
372
+ }, [initialData])
329
373
 
330
- // Remove only this song's data
331
- delete savedDataObj[storageKey];
332
- localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj));
374
+ const handleAddSegment = useCallback((beforeIndex: number) => {
375
+ const newData = addSegmentBefore(data, beforeIndex)
376
+ setData(newData)
377
+ }, [data])
333
378
 
334
- // Reset data to initial state with proper initialization
335
- const freshData = initializeDataWithIds(JSON.parse(JSON.stringify(initialData)));
336
- setData(freshData);
379
+ const handleSplitSegment = useCallback((segmentIndex: number, afterWordIndex: number) => {
380
+ const newData = splitSegment(data, segmentIndex, afterWordIndex)
381
+ if (newData) {
382
+ setData(newData)
383
+ setEditModalSegment(null)
384
+ }
385
+ }, [data])
337
386
 
338
- // Reset any UI state that might affect highlights
339
- setModalContent(null);
340
- setFlashingType(null);
341
- setHighlightInfo(null);
342
- setInteractionMode('details');
387
+ const handleHandlerToggle = useCallback(async (handler: string, enabled: boolean) => {
388
+ if (!apiClient) return
389
+
390
+ try {
391
+ setIsUpdatingHandlers(true);
392
+
393
+ // Get current enabled handlers
394
+ const currentEnabled = new Set(data.metadata.enabled_handlers || [])
395
+
396
+ // Update the set based on the toggle
397
+ if (enabled) {
398
+ currentEnabled.add(handler)
399
+ } else {
400
+ currentEnabled.delete(handler)
401
+ }
402
+
403
+ // Call API to update handlers
404
+ const newData = await apiClient.updateHandlers(Array.from(currentEnabled))
405
+
406
+ // Update local state with new correction data
407
+ setData(newData)
408
+
409
+ // Clear any existing modals or highlights
410
+ setModalContent(null)
411
+ setFlashingType(null)
412
+ setHighlightInfo(null)
413
+
414
+ // Flash the updated corrections
415
+ handleFlash('corrected')
416
+ } catch (error) {
417
+ console.error('Failed to update handlers:', error)
418
+ alert('Failed to update correction handlers. Please try again.')
419
+ } finally {
420
+ setIsUpdatingHandlers(false);
343
421
  }
344
- }, [initialData]);
422
+ }, [apiClient, data.metadata.enabled_handlers, handleFlash])
423
+
424
+ const handleHandlerClick = useCallback((handler: string) => {
425
+ console.log('Handler clicked:', handler);
426
+ setFlashingHandler(handler);
427
+ setFlashingType('handler');
428
+ console.log('Set flashingHandler to:', handler);
429
+ console.log('Set flashingType to: handler');
430
+
431
+ // Clear the flash after a short delay
432
+ setTimeout(() => {
433
+ console.log('Clearing flash state');
434
+ setFlashingHandler(null);
435
+ setFlashingType(null);
436
+ }, 1500);
437
+ }, []);
345
438
 
346
439
  return (
347
- <Box>
348
- {isReadOnly && (
349
- <Box sx={{ display: 'flex', alignItems: 'center', mb: 2, color: 'text.secondary' }}>
350
- <LockIcon sx={{ mr: 1 }} />
351
- <Typography variant="body2">
352
- View Only Mode
353
- </Typography>
354
- </Box>
355
- )}
356
- <Box sx={{
357
- display: 'flex',
358
- flexDirection: isMobile ? 'column' : 'row',
359
- gap: 2,
360
- justifyContent: 'space-between',
361
- alignItems: isMobile ? 'stretch' : 'center',
362
- mb: 3
363
- }}>
364
- <Typography variant="h4" sx={{ fontSize: isMobile ? '1.75rem' : '2.125rem' }}>
365
- Lyrics Correction Review
366
- </Typography>
367
- {isReadOnly && (
368
- <Button
369
- variant="outlined"
370
- startIcon={<UploadFileIcon />}
371
- onClick={onFileLoad}
372
- fullWidth={isMobile}
373
- >
374
- Load File
375
- </Button>
376
- )}
377
- </Box>
378
-
379
- <Box sx={{ mb: 3 }}>
380
- <CorrectionMetrics
381
- // Anchor metrics
382
- anchorCount={data.metadata.anchor_sequences_count}
383
- multiSourceAnchors={data.anchor_sequences?.filter(anchor =>
384
- // Add null checks
385
- anchor?.reference_word_ids &&
386
- Object.keys(anchor.reference_word_ids || {}).length > 1
387
- ).length ?? 0}
388
- anchorWordCount={data.anchor_sequences?.reduce((sum, anchor) =>
389
- sum + (anchor.length || 0), 0) ?? 0}
390
- // Gap metrics
391
- correctedGapCount={data.gap_sequences?.filter(gap =>
392
- gap.corrections?.length > 0).length ?? 0}
393
- uncorrectedGapCount={data.gap_sequences?.filter(gap =>
394
- !gap.corrections?.length).length ?? 0}
395
- uncorrectedGaps={data.gap_sequences
396
- ?.filter(gap => !gap.corrections?.length && gap.word_ids)
397
- .map(gap => ({
398
- position: gap.word_ids?.[0] ?? '',
399
- length: gap.length ?? 0
400
- })) ?? []}
401
- // Correction details
402
- replacedCount={data.gap_sequences?.reduce((count, gap) =>
403
- count + (gap.corrections?.filter(c => !c.is_deletion && !c.split_total).length ?? 0), 0) ?? 0}
404
- addedCount={data.gap_sequences?.reduce((count, gap) =>
405
- count + (gap.corrections?.filter(c => c.split_total).length ?? 0), 0) ?? 0}
406
- deletedCount={data.gap_sequences?.reduce((count, gap) =>
407
- count + (gap.corrections?.filter(c => c.is_deletion).length ?? 0), 0) ?? 0}
408
- onMetricClick={{
409
- anchor: () => handleFlash('anchor'),
410
- corrected: () => handleFlash('corrected'),
411
- uncorrected: () => handleFlash('uncorrected')
412
- }}
413
- totalWords={data.metadata.total_words}
414
- />
415
- </Box>
416
-
417
- <Box sx={{
418
- display: 'flex',
419
- flexDirection: isMobile ? 'column' : 'row',
420
- gap: 5,
421
- alignItems: 'flex-start',
422
- justifyContent: 'flex-start',
423
- mb: 3
424
- }}>
425
- <ModeSelector
426
- effectiveMode={effectiveMode}
427
- onChange={setInteractionMode}
428
- />
429
- <AudioPlayer
430
- apiClient={apiClient}
431
- onTimeUpdate={setCurrentAudioTime}
432
- />
433
- </Box>
440
+ <Box sx={{
441
+ p: 3,
442
+ pb: 6,
443
+ maxWidth: '100%',
444
+ overflowX: 'hidden'
445
+ }}>
446
+ <Header
447
+ isReadOnly={isReadOnly}
448
+ onFileLoad={onFileLoad}
449
+ data={data}
450
+ onMetricClick={{
451
+ anchor: () => handleFlash('anchor'),
452
+ corrected: () => handleFlash('corrected'),
453
+ uncorrected: () => handleFlash('uncorrected')
454
+ }}
455
+ effectiveMode={effectiveMode}
456
+ onModeChange={setInteractionMode}
457
+ apiClient={apiClient}
458
+ audioHash={audioHash}
459
+ onTimeUpdate={setCurrentAudioTime}
460
+ onHandlerToggle={handleHandlerToggle}
461
+ isUpdatingHandlers={isUpdatingHandlers}
462
+ onHandlerClick={handleHandlerClick}
463
+ />
434
464
 
435
465
  <Grid container spacing={2} direction={isMobile ? 'column' : 'row'}>
436
466
  <Grid item xs={12} md={6}>
@@ -440,14 +470,16 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
440
470
  onElementClick={setModalContent}
441
471
  onWordClick={handleWordClick}
442
472
  flashingType={flashingType}
473
+ flashingHandler={flashingHandler}
443
474
  highlightInfo={highlightInfo}
444
475
  onPlaySegment={handlePlaySegment}
445
476
  currentTime={currentAudioTime}
477
+ anchors={data.anchor_sequences}
446
478
  />
447
479
  </Grid>
448
480
  <Grid item xs={12} md={6}>
449
481
  <ReferenceView
450
- referenceTexts={data.reference_texts}
482
+ referenceSources={data.reference_lyrics}
451
483
  anchors={data.anchor_sequences}
452
484
  gaps={data.gap_sequences}
453
485
  mode={effectiveMode}
@@ -458,6 +490,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
458
490
  currentSource={currentSource}
459
491
  onSourceChange={setCurrentSource}
460
492
  corrected_segments={data.corrected_segments}
493
+ corrections={data.corrections}
461
494
  />
462
495
  </Grid>
463
496
  </Grid>
@@ -466,6 +499,8 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
466
499
  open={modalContent !== null}
467
500
  content={modalContent}
468
501
  onClose={() => setModalContent(null)}
502
+ allCorrections={data.corrections}
503
+ referenceLyrics={data.reference_lyrics}
469
504
  />
470
505
 
471
506
  <EditModal
@@ -476,6 +511,8 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
476
511
  originalSegment={editModalSegment?.originalSegment ?? null}
477
512
  onSave={handleUpdateSegment}
478
513
  onDelete={handleDeleteSegment}
514
+ onAddSegment={handleAddSegment}
515
+ onSplitSegment={handleSplitSegment}
479
516
  onPlaySegment={handlePlaySegment}
480
517
  currentTime={currentAudioTime}
481
518
  />
@@ -486,6 +523,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
486
523
  originalData={originalData}
487
524
  updatedData={data}
488
525
  onSubmit={handleSubmitToServer}
526
+ apiClient={apiClient}
489
527
  />
490
528
 
491
529
  {!isReadOnly && apiClient && (