lyrics-transcriber 0.43.1__py3-none-any.whl → 0.45.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lyrics_transcriber/core/controller.py +58 -24
- lyrics_transcriber/correction/anchor_sequence.py +22 -8
- lyrics_transcriber/correction/corrector.py +47 -3
- lyrics_transcriber/correction/handlers/llm.py +15 -12
- lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
- lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
- lyrics_transcriber/frontend/dist/assets/{index-D0Gr3Ep7.js → index-ZCT0s9MG.js} +10174 -6197
- lyrics_transcriber/frontend/dist/assets/index-ZCT0s9MG.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/App.tsx +5 -5
- lyrics_transcriber/frontend/src/api.ts +37 -0
- lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +14 -10
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +62 -56
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
- lyrics_transcriber/frontend/src/components/EditModal.tsx +467 -399
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +373 -0
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +308 -0
- lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
- lyrics_transcriber/frontend/src/components/Header.tsx +141 -101
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +569 -107
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +22 -13
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +1 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +29 -12
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +21 -4
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +29 -15
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +36 -18
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +89 -41
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +9 -2
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +27 -3
- lyrics_transcriber/frontend/src/components/shared/types.ts +17 -2
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +90 -19
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +192 -0
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +267 -0
- lyrics_transcriber/frontend/src/main.tsx +7 -1
- lyrics_transcriber/frontend/src/theme.ts +177 -0
- lyrics_transcriber/frontend/src/types.ts +1 -1
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/lyrics/base_lyrics_provider.py +2 -2
- lyrics_transcriber/lyrics/user_input_provider.py +44 -0
- lyrics_transcriber/output/generator.py +40 -12
- lyrics_transcriber/review/server.py +238 -8
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/METADATA +3 -2
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/RECORD +48 -40
- lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +0 -1
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +0 -252
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +0 -110
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/WHEEL +0 -0
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/entry_points.txt +0 -0
@@ -1,15 +1,16 @@
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo, memo } from 'react'
|
1
2
|
import {
|
2
3
|
AnchorSequence,
|
3
4
|
CorrectionData,
|
4
5
|
GapSequence,
|
5
6
|
HighlightInfo,
|
6
7
|
InteractionMode,
|
7
|
-
LyricsSegment
|
8
|
+
LyricsSegment,
|
9
|
+
ReferenceSource,
|
10
|
+
WordCorrection
|
8
11
|
} from '../types'
|
9
12
|
import { Box, Button, Grid, useMediaQuery, useTheme } from '@mui/material'
|
10
|
-
import { useCallback, useState, useEffect } from 'react'
|
11
13
|
import { ApiClient } from '../api'
|
12
|
-
import DetailsModal from './DetailsModal'
|
13
14
|
import ReferenceView from './ReferenceView'
|
14
15
|
import TranscriptionView from './TranscriptionView'
|
15
16
|
import { WordClickInfo, FlashType } from './shared/types'
|
@@ -19,12 +20,17 @@ import {
|
|
19
20
|
addSegmentBefore,
|
20
21
|
splitSegment,
|
21
22
|
deleteSegment,
|
22
|
-
updateSegment
|
23
|
+
updateSegment,
|
24
|
+
mergeSegment,
|
25
|
+
findAndReplace
|
23
26
|
} from './shared/utils/segmentOperations'
|
24
27
|
import { loadSavedData, saveData, clearSavedData } from './shared/utils/localStorage'
|
25
|
-
import { setupKeyboardHandlers, setModalHandler } from './shared/utils/keyboardHandlers'
|
28
|
+
import { setupKeyboardHandlers, setModalHandler, getModalState } from './shared/utils/keyboardHandlers'
|
26
29
|
import Header from './Header'
|
27
|
-
import {
|
30
|
+
import { getWordsFromIds } from './shared/utils/wordUtils'
|
31
|
+
import AddLyricsModal from './AddLyricsModal'
|
32
|
+
import { RestoreFromTrash, OndemandVideo } from '@mui/icons-material'
|
33
|
+
import FindReplaceModal from './FindReplaceModal'
|
28
34
|
|
29
35
|
// Add type for window augmentation at the top of the file
|
30
36
|
declare global {
|
@@ -34,6 +40,7 @@ declare global {
|
|
34
40
|
}
|
35
41
|
}
|
36
42
|
|
43
|
+
const debugLog = false;
|
37
44
|
export interface LyricsAnalyzerProps {
|
38
45
|
data: CorrectionData
|
39
46
|
onFileLoad: () => void
|
@@ -59,6 +66,160 @@ export type ModalContent = {
|
|
59
66
|
}
|
60
67
|
}
|
61
68
|
|
69
|
+
// Define types for the memoized components
|
70
|
+
interface MemoizedTranscriptionViewProps {
|
71
|
+
data: CorrectionData
|
72
|
+
mode: InteractionMode
|
73
|
+
onElementClick: (content: ModalContent) => void
|
74
|
+
onWordClick: (info: WordClickInfo) => void
|
75
|
+
flashingType: FlashType
|
76
|
+
flashingHandler: string | null
|
77
|
+
highlightInfo: HighlightInfo | null
|
78
|
+
onPlaySegment?: (time: number) => void
|
79
|
+
currentTime: number
|
80
|
+
anchors: AnchorSequence[]
|
81
|
+
disableHighlighting: boolean
|
82
|
+
}
|
83
|
+
|
84
|
+
// Create a memoized TranscriptionView component
|
85
|
+
const MemoizedTranscriptionView = memo(function MemoizedTranscriptionView({
|
86
|
+
data,
|
87
|
+
mode,
|
88
|
+
onElementClick,
|
89
|
+
onWordClick,
|
90
|
+
flashingType,
|
91
|
+
flashingHandler,
|
92
|
+
highlightInfo,
|
93
|
+
onPlaySegment,
|
94
|
+
currentTime,
|
95
|
+
anchors,
|
96
|
+
disableHighlighting
|
97
|
+
}: MemoizedTranscriptionViewProps) {
|
98
|
+
return (
|
99
|
+
<TranscriptionView
|
100
|
+
data={data}
|
101
|
+
mode={mode}
|
102
|
+
onElementClick={onElementClick}
|
103
|
+
onWordClick={onWordClick}
|
104
|
+
flashingType={flashingType}
|
105
|
+
flashingHandler={flashingHandler}
|
106
|
+
highlightInfo={highlightInfo}
|
107
|
+
onPlaySegment={onPlaySegment}
|
108
|
+
currentTime={disableHighlighting ? undefined : currentTime}
|
109
|
+
anchors={anchors}
|
110
|
+
/>
|
111
|
+
);
|
112
|
+
});
|
113
|
+
|
114
|
+
interface MemoizedReferenceViewProps {
|
115
|
+
referenceSources: Record<string, ReferenceSource>
|
116
|
+
anchors: AnchorSequence[]
|
117
|
+
gaps: GapSequence[]
|
118
|
+
mode: InteractionMode
|
119
|
+
onElementClick: (content: ModalContent) => void
|
120
|
+
onWordClick: (info: WordClickInfo) => void
|
121
|
+
flashingType: FlashType
|
122
|
+
highlightInfo: HighlightInfo | null
|
123
|
+
currentSource: string
|
124
|
+
onSourceChange: (source: string) => void
|
125
|
+
corrected_segments: LyricsSegment[]
|
126
|
+
corrections: WordCorrection[]
|
127
|
+
}
|
128
|
+
|
129
|
+
// Create a memoized ReferenceView component
|
130
|
+
const MemoizedReferenceView = memo(function MemoizedReferenceView({
|
131
|
+
referenceSources,
|
132
|
+
anchors,
|
133
|
+
gaps,
|
134
|
+
mode,
|
135
|
+
onElementClick,
|
136
|
+
onWordClick,
|
137
|
+
flashingType,
|
138
|
+
highlightInfo,
|
139
|
+
currentSource,
|
140
|
+
onSourceChange,
|
141
|
+
corrected_segments,
|
142
|
+
corrections
|
143
|
+
}: MemoizedReferenceViewProps) {
|
144
|
+
return (
|
145
|
+
<ReferenceView
|
146
|
+
referenceSources={referenceSources}
|
147
|
+
anchors={anchors}
|
148
|
+
gaps={gaps}
|
149
|
+
mode={mode}
|
150
|
+
onElementClick={onElementClick}
|
151
|
+
onWordClick={onWordClick}
|
152
|
+
flashingType={flashingType}
|
153
|
+
highlightInfo={highlightInfo}
|
154
|
+
currentSource={currentSource}
|
155
|
+
onSourceChange={onSourceChange}
|
156
|
+
corrected_segments={corrected_segments}
|
157
|
+
corrections={corrections}
|
158
|
+
/>
|
159
|
+
);
|
160
|
+
});
|
161
|
+
|
162
|
+
interface MemoizedHeaderProps {
|
163
|
+
isReadOnly: boolean
|
164
|
+
onFileLoad: () => void
|
165
|
+
data: CorrectionData
|
166
|
+
onMetricClick: {
|
167
|
+
anchor: () => void
|
168
|
+
corrected: () => void
|
169
|
+
uncorrected: () => void
|
170
|
+
}
|
171
|
+
effectiveMode: InteractionMode
|
172
|
+
onModeChange: (mode: InteractionMode) => void
|
173
|
+
apiClient: ApiClient | null
|
174
|
+
audioHash: string
|
175
|
+
onTimeUpdate: (time: number) => void
|
176
|
+
onHandlerToggle: (handler: string, enabled: boolean) => void
|
177
|
+
isUpdatingHandlers: boolean
|
178
|
+
onHandlerClick?: (handler: string) => void
|
179
|
+
onAddLyrics?: () => void
|
180
|
+
onFindReplace?: () => void
|
181
|
+
onEditAll?: () => void
|
182
|
+
}
|
183
|
+
|
184
|
+
// Create a memoized Header component
|
185
|
+
const MemoizedHeader = memo(function MemoizedHeader({
|
186
|
+
isReadOnly,
|
187
|
+
onFileLoad,
|
188
|
+
data,
|
189
|
+
onMetricClick,
|
190
|
+
effectiveMode,
|
191
|
+
onModeChange,
|
192
|
+
apiClient,
|
193
|
+
audioHash,
|
194
|
+
onTimeUpdate,
|
195
|
+
onHandlerToggle,
|
196
|
+
isUpdatingHandlers,
|
197
|
+
onHandlerClick,
|
198
|
+
onAddLyrics,
|
199
|
+
onFindReplace,
|
200
|
+
onEditAll
|
201
|
+
}: MemoizedHeaderProps) {
|
202
|
+
return (
|
203
|
+
<Header
|
204
|
+
isReadOnly={isReadOnly}
|
205
|
+
onFileLoad={onFileLoad}
|
206
|
+
data={data}
|
207
|
+
onMetricClick={onMetricClick}
|
208
|
+
effectiveMode={effectiveMode}
|
209
|
+
onModeChange={onModeChange}
|
210
|
+
apiClient={apiClient}
|
211
|
+
audioHash={audioHash}
|
212
|
+
onTimeUpdate={onTimeUpdate}
|
213
|
+
onHandlerToggle={onHandlerToggle}
|
214
|
+
isUpdatingHandlers={isUpdatingHandlers}
|
215
|
+
onHandlerClick={onHandlerClick}
|
216
|
+
onAddLyrics={onAddLyrics}
|
217
|
+
onFindReplace={onFindReplace}
|
218
|
+
onEditAll={onEditAll}
|
219
|
+
/>
|
220
|
+
);
|
221
|
+
});
|
222
|
+
|
62
223
|
export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly, audioHash }: LyricsAnalyzerProps) {
|
63
224
|
const [modalContent, setModalContent] = useState<ModalContent | null>(null)
|
64
225
|
const [flashingType, setFlashingType] = useState<FlashType>(null)
|
@@ -73,35 +234,45 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
73
234
|
const [isReviewComplete, setIsReviewComplete] = useState(false)
|
74
235
|
const [data, setData] = useState(initialData)
|
75
236
|
const [originalData] = useState(() => JSON.parse(JSON.stringify(initialData)))
|
76
|
-
const [interactionMode, setInteractionMode] = useState<InteractionMode>('
|
237
|
+
const [interactionMode, setInteractionMode] = useState<InteractionMode>('edit')
|
77
238
|
const [isShiftPressed, setIsShiftPressed] = useState(false)
|
78
|
-
const [isCtrlPressed, setIsCtrlPressed] = useState(false)
|
79
239
|
const [editModalSegment, setEditModalSegment] = useState<{
|
80
240
|
segment: LyricsSegment
|
81
241
|
index: number
|
82
242
|
originalSegment: LyricsSegment
|
83
243
|
} | null>(null)
|
244
|
+
const [isEditAllModalOpen, setIsEditAllModalOpen] = useState(false)
|
245
|
+
const [globalEditSegment, setGlobalEditSegment] = useState<LyricsSegment | null>(null)
|
246
|
+
const [originalGlobalSegment, setOriginalGlobalSegment] = useState<LyricsSegment | null>(null)
|
247
|
+
const [originalTranscribedGlobalSegment, setOriginalTranscribedGlobalSegment] = useState<LyricsSegment | null>(null)
|
248
|
+
const [isLoadingGlobalEdit, setIsLoadingGlobalEdit] = useState(false)
|
84
249
|
const [isReviewModalOpen, setIsReviewModalOpen] = useState(false)
|
85
250
|
const [currentAudioTime, setCurrentAudioTime] = useState(0)
|
86
251
|
const [isUpdatingHandlers, setIsUpdatingHandlers] = useState(false)
|
87
252
|
const [flashingHandler, setFlashingHandler] = useState<string | null>(null)
|
253
|
+
const [isAddingLyrics, setIsAddingLyrics] = useState(false)
|
254
|
+
const [isAddLyricsModalOpen, setIsAddLyricsModalOpen] = useState(false)
|
255
|
+
const [isAnyModalOpen, setIsAnyModalOpen] = useState(false)
|
256
|
+
const [isFindReplaceModalOpen, setIsFindReplaceModalOpen] = useState(false)
|
88
257
|
const theme = useTheme()
|
89
258
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
90
259
|
|
91
260
|
// Update debug logging to use new ID-based structure
|
92
261
|
useEffect(() => {
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
262
|
+
if (debugLog) {
|
263
|
+
console.log('LyricsAnalyzer Initial Data:', {
|
264
|
+
hasData: !!initialData,
|
265
|
+
segmentsCount: initialData?.corrected_segments?.length ?? 0,
|
266
|
+
anchorsCount: initialData?.anchor_sequences?.length ?? 0,
|
267
|
+
gapsCount: initialData?.gap_sequences?.length ?? 0,
|
268
|
+
firstAnchor: initialData?.anchor_sequences?.[0] && {
|
269
|
+
transcribedWordIds: initialData.anchor_sequences[0].transcribed_word_ids,
|
270
|
+
referenceWordIds: initialData.anchor_sequences[0].reference_word_ids
|
271
|
+
},
|
272
|
+
firstSegment: initialData?.corrected_segments?.[0],
|
273
|
+
referenceSources: Object.keys(initialData?.reference_lyrics ?? {})
|
274
|
+
});
|
275
|
+
}
|
105
276
|
}, [initialData]);
|
106
277
|
|
107
278
|
// Load saved data
|
@@ -121,29 +292,57 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
121
292
|
|
122
293
|
// Keyboard handlers
|
123
294
|
useEffect(() => {
|
124
|
-
|
295
|
+
const { currentModalHandler } = getModalState()
|
296
|
+
|
297
|
+
if (debugLog) {
|
298
|
+
console.log('LyricsAnalyzer - Setting up keyboard effect', {
|
299
|
+
isAnyModalOpen,
|
300
|
+
hasSpacebarHandler: !!currentModalHandler
|
301
|
+
})
|
302
|
+
}
|
125
303
|
|
126
304
|
const { handleKeyDown, handleKeyUp } = setupKeyboardHandlers({
|
127
305
|
setIsShiftPressed,
|
128
|
-
setIsCtrlPressed
|
129
306
|
})
|
130
307
|
|
131
|
-
|
308
|
+
// Always add keyboard listeners
|
309
|
+
if (debugLog) {
|
310
|
+
console.log('LyricsAnalyzer - Adding keyboard event listeners')
|
311
|
+
}
|
132
312
|
window.addEventListener('keydown', handleKeyDown)
|
133
313
|
window.addEventListener('keyup', handleKeyUp)
|
134
314
|
|
315
|
+
// Reset modifier states when a modal opens
|
316
|
+
if (isAnyModalOpen) {
|
317
|
+
setIsShiftPressed(false)
|
318
|
+
}
|
319
|
+
|
320
|
+
// Cleanup function
|
135
321
|
return () => {
|
136
|
-
|
322
|
+
if (debugLog) {
|
323
|
+
console.log('LyricsAnalyzer - Cleanup effect running')
|
324
|
+
}
|
137
325
|
window.removeEventListener('keydown', handleKeyDown)
|
138
326
|
window.removeEventListener('keyup', handleKeyUp)
|
139
327
|
document.body.style.userSelect = ''
|
140
328
|
}
|
141
|
-
}, [setIsShiftPressed,
|
329
|
+
}, [setIsShiftPressed, isAnyModalOpen])
|
330
|
+
|
331
|
+
// Update modal state tracking
|
332
|
+
useEffect(() => {
|
333
|
+
const modalOpen = Boolean(
|
334
|
+
modalContent ||
|
335
|
+
editModalSegment ||
|
336
|
+
isReviewModalOpen ||
|
337
|
+
isAddLyricsModalOpen ||
|
338
|
+
isFindReplaceModalOpen ||
|
339
|
+
isEditAllModalOpen
|
340
|
+
)
|
341
|
+
setIsAnyModalOpen(modalOpen)
|
342
|
+
}, [modalContent, editModalSegment, isReviewModalOpen, isAddLyricsModalOpen, isFindReplaceModalOpen, isEditAllModalOpen])
|
142
343
|
|
143
344
|
// Calculate effective mode based on modifier key states
|
144
|
-
const effectiveMode = isShiftPressed ? 'highlight' :
|
145
|
-
isCtrlPressed ? 'edit' :
|
146
|
-
interactionMode
|
345
|
+
const effectiveMode = isShiftPressed ? 'highlight' : interactionMode
|
147
346
|
|
148
347
|
const handleFlash = useCallback((type: FlashType, info?: HighlightInfo) => {
|
149
348
|
setFlashingType(null)
|
@@ -164,7 +363,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
164
363
|
}, [])
|
165
364
|
|
166
365
|
const handleWordClick = useCallback((info: WordClickInfo) => {
|
167
|
-
|
366
|
+
if (debugLog) {
|
367
|
+
console.log('LyricsAnalyzer handleWordClick:', { info });
|
368
|
+
}
|
168
369
|
|
169
370
|
if (effectiveMode === 'highlight') {
|
170
371
|
// Find if this word is part of a correction
|
@@ -217,10 +418,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
217
418
|
start_time: sourceWords[0]?.start_time ?? null,
|
218
419
|
end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
|
219
420
|
}
|
220
|
-
return [
|
221
|
-
source,
|
222
|
-
getWordsFromIds([tempSourceSegment], ids)
|
223
|
-
]
|
421
|
+
return [source, getWordsFromIds([tempSourceSegment], ids)]
|
224
422
|
})
|
225
423
|
);
|
226
424
|
|
@@ -236,13 +434,11 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
236
434
|
|
237
435
|
// Find if this word is part of a gap sequence
|
238
436
|
const gap = data.gap_sequences?.find(g =>
|
239
|
-
g.transcribed_word_ids.includes(info.word_id)
|
240
|
-
Object.values(g.reference_word_ids).some(ids =>
|
241
|
-
ids.includes(info.word_id)
|
242
|
-
)
|
437
|
+
g.transcribed_word_ids.includes(info.word_id)
|
243
438
|
);
|
244
439
|
|
245
440
|
if (gap) {
|
441
|
+
// Create a temporary segment containing all words
|
246
442
|
const allWords = data.corrected_segments.flatMap(s => s.words)
|
247
443
|
const tempSegment: LyricsSegment = {
|
248
444
|
id: 'temp',
|
@@ -267,10 +463,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
267
463
|
start_time: sourceWords[0]?.start_time ?? null,
|
268
464
|
end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
|
269
465
|
}
|
270
|
-
return [
|
271
|
-
source,
|
272
|
-
getWordsFromIds([tempSourceSegment], ids)
|
273
|
-
]
|
466
|
+
return [source, getWordsFromIds([tempSourceSegment], ids)]
|
274
467
|
})
|
275
468
|
);
|
276
469
|
|
@@ -297,30 +490,6 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
297
490
|
originalSegment: JSON.parse(JSON.stringify(segment))
|
298
491
|
});
|
299
492
|
}
|
300
|
-
} else if (effectiveMode === 'details') {
|
301
|
-
if (info.type === 'anchor' && info.anchor) {
|
302
|
-
const word = findWordById(data.corrected_segments, info.word_id);
|
303
|
-
setModalContent({
|
304
|
-
type: 'anchor',
|
305
|
-
data: {
|
306
|
-
...info.anchor,
|
307
|
-
wordId: info.word_id,
|
308
|
-
word: word?.text,
|
309
|
-
anchor_sequences: data.anchor_sequences
|
310
|
-
}
|
311
|
-
});
|
312
|
-
} else if (info.type === 'gap' && info.gap) {
|
313
|
-
const word = findWordById(data.corrected_segments, info.word_id);
|
314
|
-
setModalContent({
|
315
|
-
type: 'gap',
|
316
|
-
data: {
|
317
|
-
...info.gap,
|
318
|
-
wordId: info.word_id,
|
319
|
-
word: word?.text || '',
|
320
|
-
anchor_sequences: data.anchor_sequences
|
321
|
-
}
|
322
|
-
});
|
323
|
-
}
|
324
493
|
}
|
325
494
|
}, [data, effectiveMode, setModalContent]);
|
326
495
|
|
@@ -344,7 +513,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
344
513
|
if (!apiClient) return
|
345
514
|
|
346
515
|
try {
|
347
|
-
|
516
|
+
if (debugLog) {
|
517
|
+
console.log('Submitting changes to server')
|
518
|
+
}
|
348
519
|
await apiClient.submitCorrections(data)
|
349
520
|
|
350
521
|
setIsReviewComplete(true)
|
@@ -372,7 +543,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
372
543
|
setModalContent(null)
|
373
544
|
setFlashingType(null)
|
374
545
|
setHighlightInfo(null)
|
375
|
-
setInteractionMode('
|
546
|
+
setInteractionMode('edit')
|
376
547
|
}
|
377
548
|
}, [initialData])
|
378
549
|
|
@@ -389,6 +560,12 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
389
560
|
}
|
390
561
|
}, [data])
|
391
562
|
|
563
|
+
const handleMergeSegment = useCallback((segmentIndex: number, mergeWithNext: boolean) => {
|
564
|
+
const newData = mergeSegment(data, segmentIndex, mergeWithNext)
|
565
|
+
setData(newData)
|
566
|
+
setEditModalSegment(null)
|
567
|
+
}, [data])
|
568
|
+
|
392
569
|
const handleHandlerToggle = useCallback(async (handler: string, enabled: boolean) => {
|
393
570
|
if (!apiClient) return
|
394
571
|
|
@@ -427,15 +604,20 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
427
604
|
}, [apiClient, data.metadata.enabled_handlers, handleFlash])
|
428
605
|
|
429
606
|
const handleHandlerClick = useCallback((handler: string) => {
|
430
|
-
|
607
|
+
if (debugLog) {
|
608
|
+
console.log('Handler clicked:', handler);
|
609
|
+
}
|
431
610
|
setFlashingHandler(handler);
|
432
611
|
setFlashingType('handler');
|
433
|
-
|
434
|
-
|
435
|
-
|
612
|
+
if (debugLog) {
|
613
|
+
console.log('Set flashingHandler to:', handler);
|
614
|
+
console.log('Set flashingType to: handler');
|
615
|
+
}
|
436
616
|
// Clear the flash after a short delay
|
437
617
|
setTimeout(() => {
|
438
|
-
|
618
|
+
if (debugLog) {
|
619
|
+
console.log('Clearing flash state');
|
620
|
+
}
|
439
621
|
setFlashingHandler(null);
|
440
622
|
setFlashingType(null);
|
441
623
|
}, 1500);
|
@@ -443,26 +625,260 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
443
625
|
|
444
626
|
// Wrap setModalSpacebarHandler in useCallback
|
445
627
|
const handleSetModalSpacebarHandler = useCallback((handler: (() => (e: KeyboardEvent) => void) | undefined) => {
|
628
|
+
if (debugLog) {
|
629
|
+
console.log('LyricsAnalyzer - Setting modal handler:', {
|
630
|
+
hasHandler: !!handler
|
631
|
+
})
|
632
|
+
}
|
446
633
|
// Update the global modal handler
|
447
634
|
setModalHandler(handler ? handler() : undefined, !!handler)
|
448
635
|
}, [])
|
449
636
|
|
637
|
+
// Add new handler for adding lyrics
|
638
|
+
const handleAddLyrics = useCallback(async (source: string, lyrics: string) => {
|
639
|
+
if (!apiClient) return
|
640
|
+
|
641
|
+
try {
|
642
|
+
setIsAddingLyrics(true)
|
643
|
+
const newData = await apiClient.addLyrics(source, lyrics)
|
644
|
+
setData(newData)
|
645
|
+
} finally {
|
646
|
+
setIsAddingLyrics(false)
|
647
|
+
}
|
648
|
+
}, [apiClient])
|
649
|
+
|
650
|
+
const handleFindReplace = (findText: string, replaceText: string, options: { caseSensitive: boolean, useRegex: boolean, fullTextMode: boolean }) => {
|
651
|
+
const newData = findAndReplace(data, findText, replaceText, options)
|
652
|
+
setData(newData)
|
653
|
+
}
|
654
|
+
|
655
|
+
// Add handler for Edit All functionality
|
656
|
+
const handleEditAll = useCallback(() => {
|
657
|
+
console.log('EditAll - Starting process');
|
658
|
+
|
659
|
+
// Create empty placeholder segments to prevent the modal from closing
|
660
|
+
const placeholderSegment: LyricsSegment = {
|
661
|
+
id: 'loading-placeholder',
|
662
|
+
words: [],
|
663
|
+
text: '',
|
664
|
+
start_time: 0,
|
665
|
+
end_time: 1
|
666
|
+
};
|
667
|
+
|
668
|
+
// Set placeholder segments first
|
669
|
+
setGlobalEditSegment(placeholderSegment);
|
670
|
+
setOriginalGlobalSegment(placeholderSegment);
|
671
|
+
|
672
|
+
// Show loading state
|
673
|
+
setIsLoadingGlobalEdit(true);
|
674
|
+
console.log('EditAll - Set loading state to true');
|
675
|
+
|
676
|
+
// Open the modal with placeholder data
|
677
|
+
setIsEditAllModalOpen(true);
|
678
|
+
console.log('EditAll - Set modal open to true');
|
679
|
+
|
680
|
+
// Use requestAnimationFrame to ensure the modal with loading state is rendered
|
681
|
+
// before doing the expensive operation
|
682
|
+
requestAnimationFrame(() => {
|
683
|
+
console.log('EditAll - Inside requestAnimationFrame');
|
684
|
+
|
685
|
+
// Use setTimeout to allow the modal to render before doing the expensive operation
|
686
|
+
setTimeout(() => {
|
687
|
+
console.log('EditAll - Inside setTimeout, starting data processing');
|
688
|
+
|
689
|
+
try {
|
690
|
+
console.time('EditAll - Data processing');
|
691
|
+
|
692
|
+
// Create a combined segment with all words from all segments
|
693
|
+
const allWords = data.corrected_segments.flatMap(segment => segment.words)
|
694
|
+
console.log(`EditAll - Collected ${allWords.length} words from all segments`);
|
695
|
+
|
696
|
+
// Sort words by start time to maintain chronological order
|
697
|
+
const sortedWords = [...allWords].sort((a, b) => {
|
698
|
+
const aTime = a.start_time ?? 0
|
699
|
+
const bTime = b.start_time ?? 0
|
700
|
+
return aTime - bTime
|
701
|
+
})
|
702
|
+
console.log('EditAll - Sorted words by start time');
|
703
|
+
|
704
|
+
// Create a global segment containing all words
|
705
|
+
const globalSegment: LyricsSegment = {
|
706
|
+
id: 'global-edit',
|
707
|
+
words: sortedWords,
|
708
|
+
text: sortedWords.map(w => w.text).join(' '),
|
709
|
+
start_time: sortedWords[0]?.start_time ?? null,
|
710
|
+
end_time: sortedWords[sortedWords.length - 1]?.end_time ?? null
|
711
|
+
}
|
712
|
+
console.log('EditAll - Created global segment');
|
713
|
+
|
714
|
+
// Store the original global segment for reset functionality
|
715
|
+
setGlobalEditSegment(globalSegment)
|
716
|
+
console.log('EditAll - Set global edit segment');
|
717
|
+
|
718
|
+
setOriginalGlobalSegment(JSON.parse(JSON.stringify(globalSegment)))
|
719
|
+
console.log('EditAll - Set original global segment');
|
720
|
+
|
721
|
+
// Create the original transcribed global segment for Un-Correct functionality
|
722
|
+
if (originalData.original_segments) {
|
723
|
+
console.log('EditAll - Processing original segments for Un-Correct functionality');
|
724
|
+
|
725
|
+
// Get all words from original segments
|
726
|
+
const originalWords = originalData.original_segments.flatMap((segment: LyricsSegment) => segment.words)
|
727
|
+
console.log(`EditAll - Collected ${originalWords.length} words from original segments`);
|
728
|
+
|
729
|
+
// Sort words by start time
|
730
|
+
const sortedOriginalWords = [...originalWords].sort((a, b) => {
|
731
|
+
const aTime = a.start_time ?? 0
|
732
|
+
const bTime = b.start_time ?? 0
|
733
|
+
return aTime - bTime
|
734
|
+
})
|
735
|
+
console.log('EditAll - Sorted original words by start time');
|
736
|
+
|
737
|
+
// Create the original transcribed global segment
|
738
|
+
const originalTranscribedGlobal: LyricsSegment = {
|
739
|
+
id: 'original-transcribed-global',
|
740
|
+
words: sortedOriginalWords,
|
741
|
+
text: sortedOriginalWords.map(w => w.text).join(' '),
|
742
|
+
start_time: sortedOriginalWords[0]?.start_time ?? null,
|
743
|
+
end_time: sortedOriginalWords[sortedOriginalWords.length - 1]?.end_time ?? null
|
744
|
+
}
|
745
|
+
console.log('EditAll - Created original transcribed global segment');
|
746
|
+
|
747
|
+
setOriginalTranscribedGlobalSegment(originalTranscribedGlobal)
|
748
|
+
console.log('EditAll - Set original transcribed global segment');
|
749
|
+
} else {
|
750
|
+
setOriginalTranscribedGlobalSegment(null)
|
751
|
+
console.log('EditAll - No original segments found, set original transcribed global segment to null');
|
752
|
+
}
|
753
|
+
|
754
|
+
console.timeEnd('EditAll - Data processing');
|
755
|
+
} catch (error) {
|
756
|
+
console.error('Error preparing global edit data:', error);
|
757
|
+
} finally {
|
758
|
+
// Clear loading state
|
759
|
+
console.log('EditAll - Finished processing, setting loading state to false');
|
760
|
+
setIsLoadingGlobalEdit(false);
|
761
|
+
}
|
762
|
+
}, 100); // Small delay to allow the modal to render
|
763
|
+
});
|
764
|
+
}, [data.corrected_segments, originalData.original_segments])
|
765
|
+
|
766
|
+
// Handle saving the global edit
|
767
|
+
const handleSaveGlobalEdit = useCallback((updatedSegment: LyricsSegment) => {
|
768
|
+
console.log('Global Edit - Saving with new approach:', {
|
769
|
+
updatedSegmentId: updatedSegment.id,
|
770
|
+
wordCount: updatedSegment.words.length,
|
771
|
+
originalSegmentCount: data.corrected_segments.length,
|
772
|
+
originalTotalWordCount: data.corrected_segments.reduce((count, segment) => count + segment.words.length, 0)
|
773
|
+
})
|
774
|
+
|
775
|
+
// Get the updated words from the global segment
|
776
|
+
const updatedWords = updatedSegment.words
|
777
|
+
|
778
|
+
// Create a new array of segments with the same structure as the original
|
779
|
+
const updatedSegments = []
|
780
|
+
let wordIndex = 0
|
781
|
+
|
782
|
+
// Distribute words to segments based on the original segment sizes
|
783
|
+
for (const segment of data.corrected_segments) {
|
784
|
+
const originalWordCount = segment.words.length
|
785
|
+
|
786
|
+
// Get the words for this segment from the updated global segment
|
787
|
+
const segmentWords = []
|
788
|
+
const endIndex = Math.min(wordIndex + originalWordCount, updatedWords.length)
|
789
|
+
|
790
|
+
for (let i = wordIndex; i < endIndex; i++) {
|
791
|
+
segmentWords.push(updatedWords[i])
|
792
|
+
}
|
793
|
+
|
794
|
+
// Update the word index for the next segment
|
795
|
+
wordIndex = endIndex
|
796
|
+
|
797
|
+
// If we have words for this segment, create an updated segment
|
798
|
+
if (segmentWords.length > 0) {
|
799
|
+
// Recalculate segment start and end times
|
800
|
+
const validStartTimes = segmentWords.map(w => w.start_time).filter((t): t is number => t !== null)
|
801
|
+
const validEndTimes = segmentWords.map(w => w.end_time).filter((t): t is number => t !== null)
|
802
|
+
|
803
|
+
const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null
|
804
|
+
const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null
|
805
|
+
|
806
|
+
// Create the updated segment
|
807
|
+
updatedSegments.push({
|
808
|
+
...segment,
|
809
|
+
words: segmentWords,
|
810
|
+
text: segmentWords.map(w => w.text).join(' '),
|
811
|
+
start_time: segmentStartTime,
|
812
|
+
end_time: segmentEndTime
|
813
|
+
})
|
814
|
+
}
|
815
|
+
}
|
816
|
+
|
817
|
+
// If there are any remaining words, add them to the last segment
|
818
|
+
if (wordIndex < updatedWords.length) {
|
819
|
+
const remainingWords = updatedWords.slice(wordIndex)
|
820
|
+
const lastSegment = updatedSegments[updatedSegments.length - 1]
|
821
|
+
|
822
|
+
// Combine the remaining words with the last segment
|
823
|
+
const combinedWords = [...lastSegment.words, ...remainingWords]
|
824
|
+
|
825
|
+
// Recalculate segment start and end times
|
826
|
+
const validStartTimes = combinedWords.map(w => w.start_time).filter((t): t is number => t !== null)
|
827
|
+
const validEndTimes = combinedWords.map(w => w.end_time).filter((t): t is number => t !== null)
|
828
|
+
|
829
|
+
const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null
|
830
|
+
const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null
|
831
|
+
|
832
|
+
// Update the last segment
|
833
|
+
updatedSegments[updatedSegments.length - 1] = {
|
834
|
+
...lastSegment,
|
835
|
+
words: combinedWords,
|
836
|
+
text: combinedWords.map(w => w.text).join(' '),
|
837
|
+
start_time: segmentStartTime,
|
838
|
+
end_time: segmentEndTime
|
839
|
+
}
|
840
|
+
}
|
841
|
+
|
842
|
+
console.log('Global Edit - Updated Segments with new approach:', {
|
843
|
+
segmentCount: updatedSegments.length,
|
844
|
+
firstSegmentWordCount: updatedSegments[0]?.words.length,
|
845
|
+
totalWordCount: updatedSegments.reduce((count, segment) => count + segment.words.length, 0),
|
846
|
+
originalTotalWordCount: data.corrected_segments.reduce((count, segment) => count + segment.words.length, 0)
|
847
|
+
})
|
848
|
+
|
849
|
+
// Update the data with the new segments
|
850
|
+
setData({
|
851
|
+
...data,
|
852
|
+
corrected_segments: updatedSegments
|
853
|
+
})
|
854
|
+
|
855
|
+
// Close the modal
|
856
|
+
setIsEditAllModalOpen(false)
|
857
|
+
setGlobalEditSegment(null)
|
858
|
+
}, [data])
|
859
|
+
|
860
|
+
// Memoize the metric click handlers
|
861
|
+
const metricClickHandlers = useMemo(() => ({
|
862
|
+
anchor: () => handleFlash('anchor'),
|
863
|
+
corrected: () => handleFlash('corrected'),
|
864
|
+
uncorrected: () => handleFlash('uncorrected')
|
865
|
+
}), [handleFlash]);
|
866
|
+
|
867
|
+
// Determine if any modal is open to disable highlighting
|
868
|
+
const isAnyModalOpenMemo = useMemo(() => isAnyModalOpen, [isAnyModalOpen]);
|
869
|
+
|
450
870
|
return (
|
451
871
|
<Box sx={{
|
452
|
-
p:
|
453
|
-
pb:
|
872
|
+
p: 1,
|
873
|
+
pb: 3,
|
454
874
|
maxWidth: '100%',
|
455
875
|
overflowX: 'hidden'
|
456
876
|
}}>
|
457
|
-
<
|
877
|
+
<MemoizedHeader
|
458
878
|
isReadOnly={isReadOnly}
|
459
879
|
onFileLoad={onFileLoad}
|
460
880
|
data={data}
|
461
|
-
onMetricClick={
|
462
|
-
anchor: () => handleFlash('anchor'),
|
463
|
-
corrected: () => handleFlash('corrected'),
|
464
|
-
uncorrected: () => handleFlash('uncorrected')
|
465
|
-
}}
|
881
|
+
onMetricClick={metricClickHandlers}
|
466
882
|
effectiveMode={effectiveMode}
|
467
883
|
onModeChange={setInteractionMode}
|
468
884
|
apiClient={apiClient}
|
@@ -471,11 +887,14 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
471
887
|
onHandlerToggle={handleHandlerToggle}
|
472
888
|
isUpdatingHandlers={isUpdatingHandlers}
|
473
889
|
onHandlerClick={handleHandlerClick}
|
890
|
+
onAddLyrics={() => setIsAddLyricsModalOpen(true)}
|
891
|
+
onFindReplace={() => setIsFindReplaceModalOpen(true)}
|
892
|
+
onEditAll={handleEditAll}
|
474
893
|
/>
|
475
894
|
|
476
|
-
<Grid container
|
895
|
+
<Grid container direction={isMobile ? 'column' : 'row'}>
|
477
896
|
<Grid item xs={12} md={6}>
|
478
|
-
<
|
897
|
+
<MemoizedTranscriptionView
|
479
898
|
data={data}
|
480
899
|
mode={effectiveMode}
|
481
900
|
onElementClick={setModalContent}
|
@@ -486,10 +905,37 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
486
905
|
onPlaySegment={handlePlaySegment}
|
487
906
|
currentTime={currentAudioTime}
|
488
907
|
anchors={data.anchor_sequences}
|
908
|
+
disableHighlighting={isAnyModalOpenMemo}
|
489
909
|
/>
|
910
|
+
{!isReadOnly && apiClient && (
|
911
|
+
<Box sx={{
|
912
|
+
mt: 2,
|
913
|
+
mb: 3,
|
914
|
+
display: 'flex',
|
915
|
+
justifyContent: 'space-between',
|
916
|
+
width: '100%'
|
917
|
+
}}>
|
918
|
+
<Button
|
919
|
+
variant="outlined"
|
920
|
+
color="warning"
|
921
|
+
onClick={handleResetCorrections}
|
922
|
+
startIcon={<RestoreFromTrash />}
|
923
|
+
>
|
924
|
+
Reset Corrections
|
925
|
+
</Button>
|
926
|
+
<Button
|
927
|
+
variant="contained"
|
928
|
+
onClick={handleFinishReview}
|
929
|
+
disabled={isReviewComplete}
|
930
|
+
endIcon={<OndemandVideo />}
|
931
|
+
>
|
932
|
+
{isReviewComplete ? 'Review Complete' : 'Preview Video'}
|
933
|
+
</Button>
|
934
|
+
</Box>
|
935
|
+
)}
|
490
936
|
</Grid>
|
491
937
|
<Grid item xs={12} md={6}>
|
492
|
-
<
|
938
|
+
<MemoizedReferenceView
|
493
939
|
referenceSources={data.reference_lyrics}
|
494
940
|
anchors={data.anchor_sequences}
|
495
941
|
gaps={data.gap_sequences}
|
@@ -506,12 +952,25 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
506
952
|
</Grid>
|
507
953
|
</Grid>
|
508
954
|
|
509
|
-
<
|
510
|
-
open={
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
955
|
+
<EditModal
|
956
|
+
open={isEditAllModalOpen}
|
957
|
+
onClose={() => {
|
958
|
+
setIsEditAllModalOpen(false)
|
959
|
+
setGlobalEditSegment(null)
|
960
|
+
setOriginalGlobalSegment(null)
|
961
|
+
setOriginalTranscribedGlobalSegment(null)
|
962
|
+
handleSetModalSpacebarHandler(undefined)
|
963
|
+
}}
|
964
|
+
segment={globalEditSegment}
|
965
|
+
segmentIndex={null}
|
966
|
+
originalSegment={originalGlobalSegment}
|
967
|
+
onSave={handleSaveGlobalEdit}
|
968
|
+
onPlaySegment={handlePlaySegment}
|
969
|
+
currentTime={currentAudioTime}
|
970
|
+
setModalSpacebarHandler={handleSetModalSpacebarHandler}
|
971
|
+
originalTranscribedSegment={originalTranscribedGlobalSegment}
|
972
|
+
isGlobal={true}
|
973
|
+
isLoading={isLoadingGlobalEdit}
|
515
974
|
/>
|
516
975
|
|
517
976
|
<EditModal
|
@@ -527,9 +986,17 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
527
986
|
onDelete={handleDeleteSegment}
|
528
987
|
onAddSegment={handleAddSegment}
|
529
988
|
onSplitSegment={handleSplitSegment}
|
989
|
+
onMergeSegment={handleMergeSegment}
|
530
990
|
onPlaySegment={handlePlaySegment}
|
531
991
|
currentTime={currentAudioTime}
|
532
992
|
setModalSpacebarHandler={handleSetModalSpacebarHandler}
|
993
|
+
originalTranscribedSegment={
|
994
|
+
editModalSegment?.segment && editModalSegment?.index !== null
|
995
|
+
? originalData.original_segments.find(
|
996
|
+
(s: LyricsSegment) => s.id === editModalSegment.segment.id
|
997
|
+
) || null
|
998
|
+
: null
|
999
|
+
}
|
533
1000
|
/>
|
534
1001
|
|
535
1002
|
<ReviewChangesModal
|
@@ -542,24 +1009,19 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
542
1009
|
setModalSpacebarHandler={handleSetModalSpacebarHandler}
|
543
1010
|
/>
|
544
1011
|
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
>
|
559
|
-
Reset Corrections
|
560
|
-
</Button>
|
561
|
-
</Box>
|
562
|
-
)}
|
1012
|
+
<AddLyricsModal
|
1013
|
+
open={isAddLyricsModalOpen}
|
1014
|
+
onClose={() => setIsAddLyricsModalOpen(false)}
|
1015
|
+
onSubmit={handleAddLyrics}
|
1016
|
+
isSubmitting={isAddingLyrics}
|
1017
|
+
/>
|
1018
|
+
|
1019
|
+
<FindReplaceModal
|
1020
|
+
open={isFindReplaceModalOpen}
|
1021
|
+
onClose={() => setIsFindReplaceModalOpen(false)}
|
1022
|
+
onReplace={handleFindReplace}
|
1023
|
+
data={data}
|
1024
|
+
/>
|
563
1025
|
</Box>
|
564
1026
|
)
|
565
1027
|
}
|