lyrics-transcriber 0.41.0__py3-none-any.whl → 0.43.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/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-D0Gr3Ep7.js} +16509 -9038
- lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.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 +14 -6
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +86 -59
- lyrics_transcriber/frontend/src/components/EditModal.tsx +281 -63
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +2 -2
- lyrics_transcriber/frontend/src/components/Header.tsx +249 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +320 -266
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +120 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +174 -52
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +158 -114
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +59 -78
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +39 -16
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +4 -10
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +134 -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 +67 -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/global.d.ts +9 -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 +1 -1
- lyrics_transcriber/output/generator.py +22 -8
- lyrics_transcriber/output/plain_text.py +15 -10
- lyrics_transcriber/output/segment_resizer.py +16 -3
- lyrics_transcriber/output/subtitles.py +27 -1
- 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.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/METADATA +3 -1
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/RECORD +75 -61
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.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.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.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, setModalHandler } 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,115 +83,62 @@ 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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
+
// Update debug logging to use new ID-based structure
|
92
|
+
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
|
+
});
|
105
|
+
}, [initialData]);
|
91
106
|
|
92
|
-
//
|
107
|
+
// Load saved data
|
93
108
|
useEffect(() => {
|
94
|
-
const
|
95
|
-
|
96
|
-
|
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
|
-
}
|
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)
|
135
112
|
}
|
136
|
-
}, [initialData])
|
113
|
+
}, [initialData])
|
137
114
|
|
138
|
-
// Save
|
115
|
+
// Save data
|
139
116
|
useEffect(() => {
|
140
117
|
if (!isReadOnly) {
|
141
|
-
|
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));
|
118
|
+
saveData(data, initialData)
|
147
119
|
}
|
148
|
-
}, [data, isReadOnly, initialData
|
120
|
+
}, [data, isReadOnly, initialData])
|
149
121
|
|
150
|
-
//
|
122
|
+
// Keyboard handlers
|
151
123
|
useEffect(() => {
|
152
|
-
|
153
|
-
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
154
|
-
return
|
155
|
-
}
|
124
|
+
console.log('Setting up keyboard handlers in LyricsAnalyzer')
|
156
125
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
setIsCtrlPressed(true)
|
162
|
-
} else if (e.key === ' ' || e.code === 'Space') {
|
163
|
-
e.preventDefault()
|
164
|
-
if (window.toggleAudioPlayback) {
|
165
|
-
window.toggleAudioPlayback()
|
166
|
-
}
|
167
|
-
}
|
168
|
-
}
|
169
|
-
|
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
|
-
}
|
126
|
+
const { handleKeyDown, handleKeyUp } = setupKeyboardHandlers({
|
127
|
+
setIsShiftPressed,
|
128
|
+
setIsCtrlPressed
|
129
|
+
})
|
178
130
|
|
131
|
+
console.log('Adding keyboard event listeners')
|
179
132
|
window.addEventListener('keydown', handleKeyDown)
|
180
133
|
window.addEventListener('keyup', handleKeyUp)
|
134
|
+
|
181
135
|
return () => {
|
136
|
+
console.log('Removing keyboard event listeners')
|
182
137
|
window.removeEventListener('keydown', handleKeyDown)
|
183
138
|
window.removeEventListener('keyup', handleKeyUp)
|
184
139
|
document.body.style.userSelect = ''
|
185
140
|
}
|
186
|
-
}, [])
|
141
|
+
}, [setIsShiftPressed, setIsCtrlPressed])
|
187
142
|
|
188
143
|
// Calculate effective mode based on modifier key states
|
189
144
|
const effectiveMode = isShiftPressed ? 'highlight' :
|
@@ -209,85 +164,175 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
209
164
|
}, [])
|
210
165
|
|
211
166
|
const handleWordClick = useCallback((info: WordClickInfo) => {
|
212
|
-
|
213
|
-
|
167
|
+
console.log('LyricsAnalyzer handleWordClick:', { info });
|
168
|
+
|
169
|
+
if (effectiveMode === 'highlight') {
|
170
|
+
// Find if this word is part of a correction
|
171
|
+
const correction = data.corrections?.find(c =>
|
172
|
+
c.corrected_word_id === info.word_id ||
|
173
|
+
c.word_id === info.word_id
|
174
|
+
);
|
175
|
+
|
176
|
+
if (correction) {
|
177
|
+
setHighlightInfo({
|
178
|
+
type: 'correction',
|
179
|
+
transcribed_words: [], // Required by type but not used for corrections
|
180
|
+
correction: correction
|
181
|
+
});
|
182
|
+
setFlashingType('word');
|
183
|
+
return;
|
184
|
+
}
|
185
|
+
|
186
|
+
// Find if this word is part of an anchor sequence
|
187
|
+
const anchor = data.anchor_sequences?.find(a =>
|
188
|
+
a.transcribed_word_ids.includes(info.word_id) ||
|
189
|
+
Object.values(a.reference_word_ids).some(ids =>
|
190
|
+
ids.includes(info.word_id)
|
191
|
+
)
|
192
|
+
);
|
193
|
+
|
194
|
+
if (anchor) {
|
195
|
+
// Create a temporary segment containing all words
|
196
|
+
const allWords = data.corrected_segments.flatMap(s => s.words)
|
197
|
+
const tempSegment: LyricsSegment = {
|
198
|
+
id: 'temp',
|
199
|
+
words: allWords,
|
200
|
+
text: allWords.map(w => w.text).join(' '),
|
201
|
+
start_time: allWords[0]?.start_time ?? null,
|
202
|
+
end_time: allWords[allWords.length - 1]?.end_time ?? null
|
203
|
+
}
|
204
|
+
|
205
|
+
const transcribedWords = getWordsFromIds(
|
206
|
+
[tempSegment],
|
207
|
+
anchor.transcribed_word_ids
|
208
|
+
);
|
209
|
+
|
210
|
+
const referenceWords = Object.fromEntries(
|
211
|
+
Object.entries(anchor.reference_word_ids).map(([source, ids]) => {
|
212
|
+
const sourceWords = data.reference_lyrics[source].segments.flatMap(s => s.words)
|
213
|
+
const tempSourceSegment: LyricsSegment = {
|
214
|
+
id: `temp-${source}`,
|
215
|
+
words: sourceWords,
|
216
|
+
text: sourceWords.map(w => w.text).join(' '),
|
217
|
+
start_time: sourceWords[0]?.start_time ?? null,
|
218
|
+
end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
|
219
|
+
}
|
220
|
+
return [
|
221
|
+
source,
|
222
|
+
getWordsFromIds([tempSourceSegment], ids)
|
223
|
+
]
|
224
|
+
})
|
225
|
+
);
|
226
|
+
|
227
|
+
setHighlightInfo({
|
228
|
+
type: 'anchor',
|
229
|
+
sequence: anchor,
|
230
|
+
transcribed_words: transcribedWords,
|
231
|
+
reference_words: referenceWords
|
232
|
+
});
|
233
|
+
setFlashingType('word');
|
234
|
+
return;
|
235
|
+
}
|
236
|
+
|
237
|
+
// Find if this word is part of a gap sequence
|
238
|
+
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
|
+
)
|
243
|
+
);
|
244
|
+
|
245
|
+
if (gap) {
|
246
|
+
const allWords = data.corrected_segments.flatMap(s => s.words)
|
247
|
+
const tempSegment: LyricsSegment = {
|
248
|
+
id: 'temp',
|
249
|
+
words: allWords,
|
250
|
+
text: allWords.map(w => w.text).join(' '),
|
251
|
+
start_time: allWords[0]?.start_time ?? null,
|
252
|
+
end_time: allWords[allWords.length - 1]?.end_time ?? null
|
253
|
+
}
|
254
|
+
|
255
|
+
const transcribedWords = getWordsFromIds(
|
256
|
+
[tempSegment],
|
257
|
+
gap.transcribed_word_ids
|
258
|
+
);
|
259
|
+
|
260
|
+
const referenceWords = Object.fromEntries(
|
261
|
+
Object.entries(gap.reference_word_ids).map(([source, ids]) => {
|
262
|
+
const sourceWords = data.reference_lyrics[source].segments.flatMap(s => s.words)
|
263
|
+
const tempSourceSegment: LyricsSegment = {
|
264
|
+
id: `temp-${source}`,
|
265
|
+
words: sourceWords,
|
266
|
+
text: sourceWords.map(w => w.text).join(' '),
|
267
|
+
start_time: sourceWords[0]?.start_time ?? null,
|
268
|
+
end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
|
269
|
+
}
|
270
|
+
return [
|
271
|
+
source,
|
272
|
+
getWordsFromIds([tempSourceSegment], ids)
|
273
|
+
]
|
274
|
+
})
|
275
|
+
);
|
276
|
+
|
277
|
+
setHighlightInfo({
|
278
|
+
type: 'gap',
|
279
|
+
sequence: gap,
|
280
|
+
transcribed_words: transcribedWords,
|
281
|
+
reference_words: referenceWords
|
282
|
+
});
|
283
|
+
setFlashingType('word');
|
284
|
+
return;
|
285
|
+
}
|
286
|
+
} else if (effectiveMode === 'edit') {
|
287
|
+
// Find the segment containing this word
|
288
|
+
const segmentIndex = data.corrected_segments.findIndex(segment =>
|
214
289
|
segment.words.some(word => word.id === info.word_id)
|
215
|
-
)
|
290
|
+
);
|
216
291
|
|
217
|
-
if (
|
218
|
-
const
|
292
|
+
if (segmentIndex !== -1) {
|
293
|
+
const segment = data.corrected_segments[segmentIndex];
|
219
294
|
setEditModalSegment({
|
220
295
|
segment,
|
221
296
|
index: segmentIndex,
|
222
|
-
originalSegment:
|
223
|
-
})
|
297
|
+
originalSegment: JSON.parse(JSON.stringify(segment))
|
298
|
+
});
|
224
299
|
}
|
225
|
-
} else {
|
226
|
-
// Update flash handling for anchors/gaps
|
300
|
+
} else if (effectiveMode === 'details') {
|
227
301
|
if (info.type === 'anchor' && info.anchor) {
|
228
|
-
|
302
|
+
const word = findWordById(data.corrected_segments, info.word_id);
|
303
|
+
setModalContent({
|
229
304
|
type: 'anchor',
|
230
|
-
|
231
|
-
|
232
|
-
|
305
|
+
data: {
|
306
|
+
...info.anchor,
|
307
|
+
wordId: info.word_id,
|
308
|
+
word: word?.text,
|
309
|
+
anchor_sequences: data.anchor_sequences
|
310
|
+
}
|
311
|
+
});
|
233
312
|
} else if (info.type === 'gap' && info.gap) {
|
234
|
-
|
313
|
+
const word = findWordById(data.corrected_segments, info.word_id);
|
314
|
+
setModalContent({
|
235
315
|
type: 'gap',
|
236
|
-
|
237
|
-
|
316
|
+
data: {
|
317
|
+
...info.gap,
|
318
|
+
wordId: info.word_id,
|
319
|
+
word: word?.text || '',
|
320
|
+
anchor_sequences: data.anchor_sequences
|
321
|
+
}
|
322
|
+
});
|
238
323
|
}
|
239
324
|
}
|
240
|
-
}, [
|
325
|
+
}, [data, effectiveMode, setModalContent]);
|
241
326
|
|
242
327
|
const handleUpdateSegment = useCallback((updatedSegment: LyricsSegment) => {
|
243
328
|
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
|
-
|
329
|
+
const newData = updateSegment(data, editModalSegment.index, updatedSegment)
|
260
330
|
setData(newData)
|
261
331
|
setEditModalSegment(null)
|
262
332
|
}, [data, editModalSegment])
|
263
333
|
|
264
334
|
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
|
-
|
335
|
+
const newData = deleteSegment(data, segmentIndex)
|
291
336
|
setData(newData)
|
292
337
|
}, [data])
|
293
338
|
|
@@ -300,8 +345,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
300
345
|
|
301
346
|
try {
|
302
347
|
console.log('Submitting changes to server')
|
303
|
-
|
304
|
-
await apiClient.submitCorrections(dataToSubmit)
|
348
|
+
await apiClient.submitCorrections(data)
|
305
349
|
|
306
350
|
setIsReviewComplete(true)
|
307
351
|
setIsReviewModalOpen(false)
|
@@ -323,114 +367,111 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
323
367
|
|
324
368
|
const handleResetCorrections = useCallback(() => {
|
325
369
|
if (window.confirm('Are you sure you want to reset all corrections? This cannot be undone.')) {
|
326
|
-
|
327
|
-
|
328
|
-
|
370
|
+
clearSavedData(initialData)
|
371
|
+
setData(JSON.parse(JSON.stringify(initialData)))
|
372
|
+
setModalContent(null)
|
373
|
+
setFlashingType(null)
|
374
|
+
setHighlightInfo(null)
|
375
|
+
setInteractionMode('details')
|
376
|
+
}
|
377
|
+
}, [initialData])
|
329
378
|
|
330
|
-
|
331
|
-
|
332
|
-
|
379
|
+
const handleAddSegment = useCallback((beforeIndex: number) => {
|
380
|
+
const newData = addSegmentBefore(data, beforeIndex)
|
381
|
+
setData(newData)
|
382
|
+
}, [data])
|
333
383
|
|
334
|
-
|
335
|
-
|
336
|
-
|
384
|
+
const handleSplitSegment = useCallback((segmentIndex: number, afterWordIndex: number) => {
|
385
|
+
const newData = splitSegment(data, segmentIndex, afterWordIndex)
|
386
|
+
if (newData) {
|
387
|
+
setData(newData)
|
388
|
+
setEditModalSegment(null)
|
389
|
+
}
|
390
|
+
}, [data])
|
337
391
|
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
392
|
+
const handleHandlerToggle = useCallback(async (handler: string, enabled: boolean) => {
|
393
|
+
if (!apiClient) return
|
394
|
+
|
395
|
+
try {
|
396
|
+
setIsUpdatingHandlers(true);
|
397
|
+
|
398
|
+
// Get current enabled handlers
|
399
|
+
const currentEnabled = new Set(data.metadata.enabled_handlers || [])
|
400
|
+
|
401
|
+
// Update the set based on the toggle
|
402
|
+
if (enabled) {
|
403
|
+
currentEnabled.add(handler)
|
404
|
+
} else {
|
405
|
+
currentEnabled.delete(handler)
|
406
|
+
}
|
407
|
+
|
408
|
+
// Call API to update handlers
|
409
|
+
const newData = await apiClient.updateHandlers(Array.from(currentEnabled))
|
410
|
+
|
411
|
+
// Update local state with new correction data
|
412
|
+
setData(newData)
|
413
|
+
|
414
|
+
// Clear any existing modals or highlights
|
415
|
+
setModalContent(null)
|
416
|
+
setFlashingType(null)
|
417
|
+
setHighlightInfo(null)
|
418
|
+
|
419
|
+
// Flash the updated corrections
|
420
|
+
handleFlash('corrected')
|
421
|
+
} catch (error) {
|
422
|
+
console.error('Failed to update handlers:', error)
|
423
|
+
alert('Failed to update correction handlers. Please try again.')
|
424
|
+
} finally {
|
425
|
+
setIsUpdatingHandlers(false);
|
343
426
|
}
|
344
|
-
}, [
|
427
|
+
}, [apiClient, data.metadata.enabled_handlers, handleFlash])
|
428
|
+
|
429
|
+
const handleHandlerClick = useCallback((handler: string) => {
|
430
|
+
console.log('Handler clicked:', handler);
|
431
|
+
setFlashingHandler(handler);
|
432
|
+
setFlashingType('handler');
|
433
|
+
console.log('Set flashingHandler to:', handler);
|
434
|
+
console.log('Set flashingType to: handler');
|
435
|
+
|
436
|
+
// Clear the flash after a short delay
|
437
|
+
setTimeout(() => {
|
438
|
+
console.log('Clearing flash state');
|
439
|
+
setFlashingHandler(null);
|
440
|
+
setFlashingType(null);
|
441
|
+
}, 1500);
|
442
|
+
}, []);
|
443
|
+
|
444
|
+
// Wrap setModalSpacebarHandler in useCallback
|
445
|
+
const handleSetModalSpacebarHandler = useCallback((handler: (() => (e: KeyboardEvent) => void) | undefined) => {
|
446
|
+
// Update the global modal handler
|
447
|
+
setModalHandler(handler ? handler() : undefined, !!handler)
|
448
|
+
}, [])
|
345
449
|
|
346
450
|
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>
|
451
|
+
<Box sx={{
|
452
|
+
p: 3,
|
453
|
+
pb: 6,
|
454
|
+
maxWidth: '100%',
|
455
|
+
overflowX: 'hidden'
|
456
|
+
}}>
|
457
|
+
<Header
|
458
|
+
isReadOnly={isReadOnly}
|
459
|
+
onFileLoad={onFileLoad}
|
460
|
+
data={data}
|
461
|
+
onMetricClick={{
|
462
|
+
anchor: () => handleFlash('anchor'),
|
463
|
+
corrected: () => handleFlash('corrected'),
|
464
|
+
uncorrected: () => handleFlash('uncorrected')
|
465
|
+
}}
|
466
|
+
effectiveMode={effectiveMode}
|
467
|
+
onModeChange={setInteractionMode}
|
468
|
+
apiClient={apiClient}
|
469
|
+
audioHash={audioHash}
|
470
|
+
onTimeUpdate={setCurrentAudioTime}
|
471
|
+
onHandlerToggle={handleHandlerToggle}
|
472
|
+
isUpdatingHandlers={isUpdatingHandlers}
|
473
|
+
onHandlerClick={handleHandlerClick}
|
474
|
+
/>
|
434
475
|
|
435
476
|
<Grid container spacing={2} direction={isMobile ? 'column' : 'row'}>
|
436
477
|
<Grid item xs={12} md={6}>
|
@@ -440,14 +481,16 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
440
481
|
onElementClick={setModalContent}
|
441
482
|
onWordClick={handleWordClick}
|
442
483
|
flashingType={flashingType}
|
484
|
+
flashingHandler={flashingHandler}
|
443
485
|
highlightInfo={highlightInfo}
|
444
486
|
onPlaySegment={handlePlaySegment}
|
445
487
|
currentTime={currentAudioTime}
|
488
|
+
anchors={data.anchor_sequences}
|
446
489
|
/>
|
447
490
|
</Grid>
|
448
491
|
<Grid item xs={12} md={6}>
|
449
492
|
<ReferenceView
|
450
|
-
|
493
|
+
referenceSources={data.reference_lyrics}
|
451
494
|
anchors={data.anchor_sequences}
|
452
495
|
gaps={data.gap_sequences}
|
453
496
|
mode={effectiveMode}
|
@@ -458,6 +501,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
458
501
|
currentSource={currentSource}
|
459
502
|
onSourceChange={setCurrentSource}
|
460
503
|
corrected_segments={data.corrected_segments}
|
504
|
+
corrections={data.corrections}
|
461
505
|
/>
|
462
506
|
</Grid>
|
463
507
|
</Grid>
|
@@ -466,18 +510,26 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
466
510
|
open={modalContent !== null}
|
467
511
|
content={modalContent}
|
468
512
|
onClose={() => setModalContent(null)}
|
513
|
+
allCorrections={data.corrections}
|
514
|
+
referenceLyrics={data.reference_lyrics}
|
469
515
|
/>
|
470
516
|
|
471
517
|
<EditModal
|
472
518
|
open={Boolean(editModalSegment)}
|
473
|
-
onClose={() =>
|
519
|
+
onClose={() => {
|
520
|
+
setEditModalSegment(null)
|
521
|
+
handleSetModalSpacebarHandler(undefined)
|
522
|
+
}}
|
474
523
|
segment={editModalSegment?.segment ?? null}
|
475
524
|
segmentIndex={editModalSegment?.index ?? null}
|
476
525
|
originalSegment={editModalSegment?.originalSegment ?? null}
|
477
526
|
onSave={handleUpdateSegment}
|
478
527
|
onDelete={handleDeleteSegment}
|
528
|
+
onAddSegment={handleAddSegment}
|
529
|
+
onSplitSegment={handleSplitSegment}
|
479
530
|
onPlaySegment={handlePlaySegment}
|
480
531
|
currentTime={currentAudioTime}
|
532
|
+
setModalSpacebarHandler={handleSetModalSpacebarHandler}
|
481
533
|
/>
|
482
534
|
|
483
535
|
<ReviewChangesModal
|
@@ -486,6 +538,8 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
486
538
|
originalData={originalData}
|
487
539
|
updatedData={data}
|
488
540
|
onSubmit={handleSubmitToServer}
|
541
|
+
apiClient={apiClient}
|
542
|
+
setModalSpacebarHandler={handleSetModalSpacebarHandler}
|
489
543
|
/>
|
490
544
|
|
491
545
|
{!isReadOnly && apiClient && (
|