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.
- lyrics_transcriber/cli/cli_main.py +7 -0
- lyrics_transcriber/core/config.py +1 -0
- lyrics_transcriber/core/controller.py +30 -52
- lyrics_transcriber/correction/anchor_sequence.py +325 -150
- lyrics_transcriber/correction/corrector.py +224 -107
- lyrics_transcriber/correction/handlers/base.py +28 -10
- lyrics_transcriber/correction/handlers/extend_anchor.py +47 -24
- lyrics_transcriber/correction/handlers/levenshtein.py +75 -33
- lyrics_transcriber/correction/handlers/llm.py +290 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +81 -36
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +46 -26
- lyrics_transcriber/correction/handlers/repeat.py +28 -11
- lyrics_transcriber/correction/handlers/sound_alike.py +68 -32
- lyrics_transcriber/correction/handlers/syllables_match.py +80 -30
- lyrics_transcriber/correction/handlers/word_count_match.py +36 -19
- lyrics_transcriber/correction/handlers/word_operations.py +68 -22
- lyrics_transcriber/correction/text_utils.py +3 -7
- lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
- lyrics_transcriber/frontend/.yarn/releases/yarn-4.6.0.cjs +934 -0
- lyrics_transcriber/frontend/.yarnrc.yml +3 -0
- lyrics_transcriber/frontend/dist/assets/{index-DKnNJHRK.js → index-coH8y7gV.js} +16284 -9032
- lyrics_transcriber/frontend/dist/assets/index-coH8y7gV.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/package.json +6 -2
- lyrics_transcriber/frontend/src/App.tsx +18 -2
- lyrics_transcriber/frontend/src/api.ts +103 -6
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +7 -6
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +86 -59
- lyrics_transcriber/frontend/src/components/EditModal.tsx +93 -43
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +2 -2
- lyrics_transcriber/frontend/src/components/Header.tsx +251 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +303 -265
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +117 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +125 -40
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +129 -115
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +59 -78
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +40 -16
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +4 -10
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +137 -68
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +1 -1
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +85 -115
- lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
- lyrics_transcriber/frontend/src/components/shared/types.ts +15 -7
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +35 -0
- lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +7 -7
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +121 -0
- lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
- lyrics_transcriber/frontend/src/types.js +2 -0
- lyrics_transcriber/frontend/src/types.ts +70 -49
- lyrics_transcriber/frontend/src/validation.ts +132 -0
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/yarn.lock +3752 -0
- lyrics_transcriber/lyrics/base_lyrics_provider.py +75 -12
- lyrics_transcriber/lyrics/file_provider.py +6 -5
- lyrics_transcriber/lyrics/genius.py +5 -2
- lyrics_transcriber/lyrics/spotify.py +58 -21
- lyrics_transcriber/output/ass/config.py +16 -5
- lyrics_transcriber/output/cdg.py +8 -8
- lyrics_transcriber/output/generator.py +29 -14
- lyrics_transcriber/output/plain_text.py +15 -10
- lyrics_transcriber/output/segment_resizer.py +16 -3
- lyrics_transcriber/output/subtitles.py +56 -2
- lyrics_transcriber/output/video.py +107 -1
- lyrics_transcriber/review/__init__.py +0 -1
- lyrics_transcriber/review/server.py +337 -164
- lyrics_transcriber/transcribers/audioshake.py +3 -0
- lyrics_transcriber/transcribers/base_transcriber.py +11 -3
- lyrics_transcriber/transcribers/whisper.py +11 -1
- lyrics_transcriber/types.py +151 -105
- lyrics_transcriber/utils/word_utils.py +27 -0
- {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/METADATA +3 -1
- {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/RECORD +76 -63
- {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/WHEEL +1 -1
- lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js.map +0 -1
- lyrics_transcriber/frontend/package-lock.json +0 -4260
- lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +0 -202
- {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/LICENSE +0 -0
- {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
|
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
|
23
|
-
|
24
|
-
|
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
|
-
|
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(
|
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
|
-
//
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
//
|
107
|
+
// Load saved data
|
139
108
|
useEffect(() => {
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
-
}, [
|
113
|
+
}, [initialData])
|
149
114
|
|
150
|
-
//
|
115
|
+
// Save data
|
151
116
|
useEffect(() => {
|
152
|
-
|
153
|
-
|
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
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
-
|
213
|
-
|
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 (
|
218
|
-
const
|
287
|
+
if (segmentIndex !== -1) {
|
288
|
+
const segment = data.corrected_segments[segmentIndex];
|
219
289
|
setEditModalSegment({
|
220
290
|
segment,
|
221
291
|
index: segmentIndex,
|
222
|
-
originalSegment:
|
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
|
-
|
297
|
+
const word = findWordById(data.corrected_segments, info.word_id);
|
298
|
+
setModalContent({
|
229
299
|
type: 'anchor',
|
230
|
-
|
231
|
-
|
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
|
-
|
308
|
+
const word = findWordById(data.corrected_segments, info.word_id);
|
309
|
+
setModalContent({
|
235
310
|
type: 'gap',
|
236
|
-
|
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
|
-
}, [
|
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 =
|
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
|
-
|
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
|
-
|
327
|
-
|
328
|
-
|
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
|
-
|
331
|
-
|
332
|
-
|
374
|
+
const handleAddSegment = useCallback((beforeIndex: number) => {
|
375
|
+
const newData = addSegmentBefore(data, beforeIndex)
|
376
|
+
setData(newData)
|
377
|
+
}, [data])
|
333
378
|
|
334
|
-
|
335
|
-
|
336
|
-
|
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
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
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
|
-
}, [
|
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
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
{
|
368
|
-
|
369
|
-
|
370
|
-
|
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
|
-
|
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 && (
|