lyrics-transcriber 0.48.0__py3-none-any.whl → 0.49.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/frontend/dist/assets/{index-BvRLUQmZ.js → index-BpvPgWoc.js} +159 -30
- lyrics_transcriber/frontend/dist/assets/index-BpvPgWoc.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/components/Header.tsx +55 -5
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +226 -47
- {lyrics_transcriber-0.48.0.dist-info → lyrics_transcriber-0.49.0.dist-info}/METADATA +1 -1
- {lyrics_transcriber-0.48.0.dist-info → lyrics_transcriber-0.49.0.dist-info}/RECORD +10 -10
- {lyrics_transcriber-0.48.0.dist-info → lyrics_transcriber-0.49.0.dist-info}/WHEEL +1 -1
- lyrics_transcriber/frontend/dist/assets/index-BvRLUQmZ.js.map +0 -1
- {lyrics_transcriber-0.48.0.dist-info → lyrics_transcriber-0.49.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.48.0.dist-info → lyrics_transcriber-0.49.0.dist-info}/entry_points.txt +0 -0
@@ -5,7 +5,7 @@
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7
7
|
<title>Lyrics Transcriber Analyzer</title>
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
8
|
+
<script type="module" crossorigin src="/assets/index-BpvPgWoc.js"></script>
|
9
9
|
</head>
|
10
10
|
<body>
|
11
11
|
<div id="root"></div>
|
@@ -1,8 +1,10 @@
|
|
1
|
-
import { Box, Button, Typography, useMediaQuery, useTheme, Switch, FormControlLabel, Tooltip, Paper } from '@mui/material'
|
1
|
+
import { Box, Button, Typography, useMediaQuery, useTheme, Switch, FormControlLabel, Tooltip, Paper, IconButton } from '@mui/material'
|
2
2
|
import LockIcon from '@mui/icons-material/Lock'
|
3
3
|
import UploadFileIcon from '@mui/icons-material/UploadFile'
|
4
4
|
import FindReplaceIcon from '@mui/icons-material/FindReplace'
|
5
5
|
import EditIcon from '@mui/icons-material/Edit'
|
6
|
+
import UndoIcon from '@mui/icons-material/Undo'
|
7
|
+
import RedoIcon from '@mui/icons-material/Redo'
|
6
8
|
import { CorrectionData, InteractionMode } from '../types'
|
7
9
|
import CorrectionMetrics from './CorrectionMetrics'
|
8
10
|
import ModeSelector from './ModeSelector'
|
@@ -29,6 +31,10 @@ interface HeaderProps {
|
|
29
31
|
onHandlerClick?: (handler: string) => void
|
30
32
|
onFindReplace?: () => void
|
31
33
|
onEditAll?: () => void
|
34
|
+
onUndo: () => void
|
35
|
+
onRedo: () => void
|
36
|
+
canUndo: boolean
|
37
|
+
canRedo: boolean
|
32
38
|
}
|
33
39
|
|
34
40
|
export default function Header({
|
@@ -46,6 +52,10 @@ export default function Header({
|
|
46
52
|
onHandlerClick,
|
47
53
|
onFindReplace,
|
48
54
|
onEditAll,
|
55
|
+
onUndo,
|
56
|
+
onRedo,
|
57
|
+
canUndo,
|
58
|
+
canRedo,
|
49
59
|
}: HeaderProps) {
|
50
60
|
const theme = useTheme()
|
51
61
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
@@ -102,8 +112,8 @@ export default function Header({
|
|
102
112
|
</Box>
|
103
113
|
)}
|
104
114
|
|
105
|
-
<Box sx={{
|
106
|
-
display: 'flex',
|
115
|
+
<Box sx={{
|
116
|
+
display: 'flex',
|
107
117
|
flexDirection: isMobile ? 'column' : 'row',
|
108
118
|
gap: 1,
|
109
119
|
justifyContent: 'space-between',
|
@@ -220,8 +230,8 @@ export default function Header({
|
|
220
230
|
</Box>
|
221
231
|
|
222
232
|
<Paper sx={{ p: 0.8, mb: 1 }}>
|
223
|
-
<Box sx={{
|
224
|
-
display: 'flex',
|
233
|
+
<Box sx={{
|
234
|
+
display: 'flex',
|
225
235
|
flexDirection: isMobile ? 'column' : 'row',
|
226
236
|
gap: 1,
|
227
237
|
alignItems: isMobile ? 'flex-start' : 'center',
|
@@ -239,6 +249,46 @@ export default function Header({
|
|
239
249
|
effectiveMode={effectiveMode}
|
240
250
|
onChange={onModeChange}
|
241
251
|
/>
|
252
|
+
{!isReadOnly && (
|
253
|
+
<Box sx={{ display: 'flex', height: '32px' }}>
|
254
|
+
<Tooltip title="Undo (Cmd/Ctrl+Z)">
|
255
|
+
<span>
|
256
|
+
<IconButton
|
257
|
+
size="small"
|
258
|
+
onClick={onUndo}
|
259
|
+
disabled={!canUndo}
|
260
|
+
sx={{
|
261
|
+
border: `1px solid ${theme.palette.divider}`,
|
262
|
+
borderRadius: '4px',
|
263
|
+
mx: 0.25,
|
264
|
+
height: '32px',
|
265
|
+
width: '32px'
|
266
|
+
}}
|
267
|
+
>
|
268
|
+
<UndoIcon fontSize="small" />
|
269
|
+
</IconButton>
|
270
|
+
</span>
|
271
|
+
</Tooltip>
|
272
|
+
<Tooltip title="Redo (Cmd/Ctrl+Shift+Z)">
|
273
|
+
<span>
|
274
|
+
<IconButton
|
275
|
+
size="small"
|
276
|
+
onClick={onRedo}
|
277
|
+
disabled={!canRedo}
|
278
|
+
sx={{
|
279
|
+
border: `1px solid ${theme.palette.divider}`,
|
280
|
+
borderRadius: '4px',
|
281
|
+
mx: 0.25,
|
282
|
+
height: '32px',
|
283
|
+
width: '32px'
|
284
|
+
}}
|
285
|
+
>
|
286
|
+
<RedoIcon fontSize="small" />
|
287
|
+
</IconButton>
|
288
|
+
</span>
|
289
|
+
</Tooltip>
|
290
|
+
</Box>
|
291
|
+
)}
|
242
292
|
{!isReadOnly && (
|
243
293
|
<Button
|
244
294
|
variant="outlined"
|
@@ -20,7 +20,6 @@ import {
|
|
20
20
|
addSegmentBefore,
|
21
21
|
splitSegment,
|
22
22
|
deleteSegment,
|
23
|
-
updateSegment,
|
24
23
|
mergeSegment,
|
25
24
|
findAndReplace,
|
26
25
|
deleteWord
|
@@ -186,6 +185,10 @@ interface MemoizedHeaderProps {
|
|
186
185
|
onAddLyrics?: () => void
|
187
186
|
onFindReplace?: () => void
|
188
187
|
onEditAll?: () => void
|
188
|
+
onUndo: () => void
|
189
|
+
onRedo: () => void
|
190
|
+
canUndo: boolean
|
191
|
+
canRedo: boolean
|
189
192
|
}
|
190
193
|
|
191
194
|
// Create a memoized Header component
|
@@ -203,7 +206,11 @@ const MemoizedHeader = memo(function MemoizedHeader({
|
|
203
206
|
isUpdatingHandlers,
|
204
207
|
onHandlerClick,
|
205
208
|
onFindReplace,
|
206
|
-
onEditAll
|
209
|
+
onEditAll,
|
210
|
+
onUndo,
|
211
|
+
onRedo,
|
212
|
+
canUndo,
|
213
|
+
canRedo
|
207
214
|
}: MemoizedHeaderProps) {
|
208
215
|
return (
|
209
216
|
<Header
|
@@ -221,6 +228,10 @@ const MemoizedHeader = memo(function MemoizedHeader({
|
|
221
228
|
onHandlerClick={onHandlerClick}
|
222
229
|
onFindReplace={onFindReplace}
|
223
230
|
onEditAll={onEditAll}
|
231
|
+
onUndo={onUndo}
|
232
|
+
onRedo={onRedo}
|
233
|
+
canUndo={canUndo}
|
234
|
+
canRedo={canRedo}
|
224
235
|
/>
|
225
236
|
);
|
226
237
|
});
|
@@ -237,7 +248,6 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
237
248
|
return availableSources.length > 0 ? availableSources[0] : ''
|
238
249
|
})
|
239
250
|
const [isReviewComplete, setIsReviewComplete] = useState(false)
|
240
|
-
const [data, setData] = useState(initialData)
|
241
251
|
const [originalData] = useState(() => JSON.parse(JSON.stringify(initialData)))
|
242
252
|
const [interactionMode, setInteractionMode] = useState<InteractionMode>('edit')
|
243
253
|
const [isShiftPressed, setIsShiftPressed] = useState(false)
|
@@ -263,6 +273,35 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
263
273
|
const theme = useTheme()
|
264
274
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
265
275
|
|
276
|
+
// State history for Undo/Redo
|
277
|
+
const [history, setHistory] = useState<CorrectionData[]>([initialData])
|
278
|
+
const [historyIndex, setHistoryIndex] = useState(0)
|
279
|
+
|
280
|
+
// Derived state: the current data based on history index
|
281
|
+
const data = history[historyIndex];
|
282
|
+
|
283
|
+
// Function to update data and manage history
|
284
|
+
const updateDataWithHistory = useCallback((newData: CorrectionData, actionDescription?: string) => {
|
285
|
+
if (debugLog) {
|
286
|
+
console.log(`[DEBUG] updateDataWithHistory: Action - ${actionDescription || 'Unknown'}. Current index: ${historyIndex}, History length: ${history.length}`);
|
287
|
+
}
|
288
|
+
const newHistory = history.slice(0, historyIndex + 1)
|
289
|
+
const deepCopiedNewData = JSON.parse(JSON.stringify(newData));
|
290
|
+
|
291
|
+
newHistory.push(deepCopiedNewData)
|
292
|
+
setHistory(newHistory)
|
293
|
+
setHistoryIndex(newHistory.length - 1)
|
294
|
+
if (debugLog) {
|
295
|
+
console.log(`[DEBUG] updateDataWithHistory: History updated. New index: ${newHistory.length - 1}, New length: ${newHistory.length}`);
|
296
|
+
}
|
297
|
+
}, [history, historyIndex])
|
298
|
+
|
299
|
+
// Reset history when initial data changes (e.g., new file loaded)
|
300
|
+
useEffect(() => {
|
301
|
+
setHistory([initialData])
|
302
|
+
setHistoryIndex(0)
|
303
|
+
}, [initialData])
|
304
|
+
|
266
305
|
// Update debug logging to use new ID-based structure
|
267
306
|
useEffect(() => {
|
268
307
|
if (debugLog) {
|
@@ -285,16 +324,18 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
285
324
|
useEffect(() => {
|
286
325
|
const savedData = loadSavedData(initialData)
|
287
326
|
if (savedData && window.confirm('Found saved progress for this song. Would you like to restore it?')) {
|
288
|
-
|
327
|
+
// Replace history with saved data as the initial state
|
328
|
+
setHistory([savedData])
|
329
|
+
setHistoryIndex(0)
|
289
330
|
}
|
290
|
-
}, [initialData])
|
331
|
+
}, [initialData]) // Keep dependency only on initialData
|
291
332
|
|
292
|
-
// Save data
|
333
|
+
// Save data - This should save the *current* state, not affect history
|
293
334
|
useEffect(() => {
|
294
335
|
if (!isReadOnly) {
|
295
|
-
saveData(data, initialData)
|
336
|
+
saveData(data, initialData) // Use 'data' derived from history and the initialData prop
|
296
337
|
}
|
297
|
-
}, [data, isReadOnly, initialData])
|
338
|
+
}, [data, isReadOnly, initialData]) // Correct dependencies
|
298
339
|
|
299
340
|
// Keyboard handlers
|
300
341
|
useEffect(() => {
|
@@ -380,8 +421,8 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
380
421
|
if (effectiveMode === 'delete_word') {
|
381
422
|
// Use the shared deleteWord utility function
|
382
423
|
const newData = deleteWord(data, info.word_id);
|
383
|
-
|
384
|
-
|
424
|
+
updateDataWithHistory(newData, 'delete word'); // Update history
|
425
|
+
|
385
426
|
// Flash to indicate the word was deleted
|
386
427
|
handleFlash('word');
|
387
428
|
return;
|
@@ -511,19 +552,47 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
511
552
|
});
|
512
553
|
}
|
513
554
|
}
|
514
|
-
}, [data, effectiveMode, setModalContent, handleFlash, deleteWord]);
|
555
|
+
}, [data, effectiveMode, setModalContent, handleFlash, deleteWord, updateDataWithHistory]);
|
515
556
|
|
516
557
|
const handleUpdateSegment = useCallback((updatedSegment: LyricsSegment) => {
|
517
558
|
if (!editModalSegment) return
|
518
|
-
|
519
|
-
|
559
|
+
|
560
|
+
if (debugLog) {
|
561
|
+
console.log('[DEBUG] handleUpdateSegment: Updating history from modal save', {
|
562
|
+
segmentIndex: editModalSegment.index,
|
563
|
+
currentHistoryIndex: historyIndex,
|
564
|
+
currentHistoryLength: history.length,
|
565
|
+
currentSegmentText: history[historyIndex]?.corrected_segments[editModalSegment.index]?.text,
|
566
|
+
updatedSegmentText: updatedSegment.text
|
567
|
+
});
|
568
|
+
}
|
569
|
+
|
570
|
+
// --- Ensure Immutability Here ---
|
571
|
+
const currentData = history[historyIndex];
|
572
|
+
const newSegments = currentData.corrected_segments.map((segment, i) =>
|
573
|
+
i === editModalSegment.index ? updatedSegment : segment
|
574
|
+
);
|
575
|
+
const newDataImmutable: CorrectionData = {
|
576
|
+
...currentData,
|
577
|
+
corrected_segments: newSegments,
|
578
|
+
};
|
579
|
+
// --- End Immutability Ensure ---
|
580
|
+
|
581
|
+
updateDataWithHistory(newDataImmutable, 'update segment');
|
582
|
+
|
583
|
+
if (debugLog) {
|
584
|
+
console.log('[DEBUG] handleUpdateSegment: History updated (async)', {
|
585
|
+
newHistoryIndex: historyIndex + 1,
|
586
|
+
newHistoryLength: history.length - historyIndex === 1 ? history.length + 1 : historyIndex + 2
|
587
|
+
});
|
588
|
+
}
|
520
589
|
setEditModalSegment(null)
|
521
|
-
}, [
|
590
|
+
}, [history, historyIndex, editModalSegment, updateDataWithHistory])
|
522
591
|
|
523
592
|
const handleDeleteSegment = useCallback((segmentIndex: number) => {
|
524
593
|
const newData = deleteSegment(data, segmentIndex)
|
525
|
-
|
526
|
-
}, [data])
|
594
|
+
updateDataWithHistory(newData, 'delete segment')
|
595
|
+
}, [data, updateDataWithHistory])
|
527
596
|
|
528
597
|
const handleFinishReview = useCallback(() => {
|
529
598
|
setIsReviewModalOpen(true)
|
@@ -559,7 +628,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
559
628
|
const handleResetCorrections = useCallback(() => {
|
560
629
|
if (window.confirm('Are you sure you want to reset all corrections? This cannot be undone.')) {
|
561
630
|
clearSavedData(initialData)
|
562
|
-
|
631
|
+
// Reset history to the original initial data
|
632
|
+
setHistory([JSON.parse(JSON.stringify(initialData))])
|
633
|
+
setHistoryIndex(0)
|
563
634
|
setModalContent(null)
|
564
635
|
setFlashingType(null)
|
565
636
|
setHighlightInfo(null)
|
@@ -569,22 +640,22 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
569
640
|
|
570
641
|
const handleAddSegment = useCallback((beforeIndex: number) => {
|
571
642
|
const newData = addSegmentBefore(data, beforeIndex)
|
572
|
-
|
573
|
-
}, [data])
|
643
|
+
updateDataWithHistory(newData, 'add segment')
|
644
|
+
}, [data, updateDataWithHistory])
|
574
645
|
|
575
646
|
const handleSplitSegment = useCallback((segmentIndex: number, afterWordIndex: number) => {
|
576
647
|
const newData = splitSegment(data, segmentIndex, afterWordIndex)
|
577
648
|
if (newData) {
|
578
|
-
|
649
|
+
updateDataWithHistory(newData, 'split segment')
|
579
650
|
setEditModalSegment(null)
|
580
651
|
}
|
581
|
-
}, [data])
|
652
|
+
}, [data, updateDataWithHistory])
|
582
653
|
|
583
654
|
const handleMergeSegment = useCallback((segmentIndex: number, mergeWithNext: boolean) => {
|
584
655
|
const newData = mergeSegment(data, segmentIndex, mergeWithNext)
|
585
|
-
|
656
|
+
updateDataWithHistory(newData, 'merge segment')
|
586
657
|
setEditModalSegment(null)
|
587
|
-
}, [data])
|
658
|
+
}, [data, updateDataWithHistory])
|
588
659
|
|
589
660
|
const handleHandlerToggle = useCallback(async (handler: string, enabled: boolean) => {
|
590
661
|
if (!apiClient) return
|
@@ -606,7 +677,8 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
606
677
|
const newData = await apiClient.updateHandlers(Array.from(currentEnabled))
|
607
678
|
|
608
679
|
// Update local state with new correction data
|
609
|
-
|
680
|
+
// This API call returns the *entire* new state, so treat it as a single history step
|
681
|
+
updateDataWithHistory(newData, `toggle handler ${handler}`); // Update history
|
610
682
|
|
611
683
|
// Clear any existing modals or highlights
|
612
684
|
setModalContent(null)
|
@@ -621,7 +693,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
621
693
|
} finally {
|
622
694
|
setIsUpdatingHandlers(false);
|
623
695
|
}
|
624
|
-
}, [apiClient, data.metadata.enabled_handlers, handleFlash])
|
696
|
+
}, [apiClient, data.metadata.enabled_handlers, handleFlash, updateDataWithHistory])
|
625
697
|
|
626
698
|
const handleHandlerClick = useCallback((handler: string) => {
|
627
699
|
if (debugLog) {
|
@@ -661,21 +733,22 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
661
733
|
try {
|
662
734
|
setIsAddingLyrics(true)
|
663
735
|
const newData = await apiClient.addLyrics(source, lyrics)
|
664
|
-
|
736
|
+
// This API call returns the *entire* new state
|
737
|
+
updateDataWithHistory(newData, 'add lyrics'); // Update history
|
665
738
|
} finally {
|
666
739
|
setIsAddingLyrics(false)
|
667
740
|
}
|
668
|
-
}, [apiClient])
|
741
|
+
}, [apiClient, updateDataWithHistory])
|
669
742
|
|
670
743
|
const handleFindReplace = (findText: string, replaceText: string, options: { caseSensitive: boolean, useRegex: boolean, fullTextMode: boolean }) => {
|
671
744
|
const newData = findAndReplace(data, findText, replaceText, options)
|
672
|
-
|
745
|
+
updateDataWithHistory(newData, 'find/replace'); // Update history
|
673
746
|
}
|
674
747
|
|
675
748
|
// Add handler for Edit All functionality
|
676
749
|
const handleEditAll = useCallback(() => {
|
677
750
|
console.log('EditAll - Starting process');
|
678
|
-
|
751
|
+
|
679
752
|
// Create empty placeholder segments to prevent the modal from closing
|
680
753
|
const placeholderSegment: LyricsSegment = {
|
681
754
|
id: 'loading-placeholder',
|
@@ -684,31 +757,31 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
684
757
|
start_time: 0,
|
685
758
|
end_time: 1
|
686
759
|
};
|
687
|
-
|
760
|
+
|
688
761
|
// Set placeholder segments first
|
689
762
|
setGlobalEditSegment(placeholderSegment);
|
690
763
|
setOriginalGlobalSegment(placeholderSegment);
|
691
|
-
|
764
|
+
|
692
765
|
// Show loading state
|
693
766
|
setIsLoadingGlobalEdit(true);
|
694
767
|
console.log('EditAll - Set loading state to true');
|
695
|
-
|
768
|
+
|
696
769
|
// Open the modal with placeholder data
|
697
770
|
setIsEditAllModalOpen(true);
|
698
771
|
console.log('EditAll - Set modal open to true');
|
699
|
-
|
772
|
+
|
700
773
|
// Use requestAnimationFrame to ensure the modal with loading state is rendered
|
701
774
|
// before doing the expensive operation
|
702
775
|
requestAnimationFrame(() => {
|
703
776
|
console.log('EditAll - Inside requestAnimationFrame');
|
704
|
-
|
777
|
+
|
705
778
|
// Use setTimeout to allow the modal to render before doing the expensive operation
|
706
779
|
setTimeout(() => {
|
707
780
|
console.log('EditAll - Inside setTimeout, starting data processing');
|
708
|
-
|
781
|
+
|
709
782
|
try {
|
710
783
|
console.time('EditAll - Data processing');
|
711
|
-
|
784
|
+
|
712
785
|
// Create a combined segment with all words from all segments
|
713
786
|
const allWords = data.corrected_segments.flatMap(segment => segment.words)
|
714
787
|
console.log(`EditAll - Collected ${allWords.length} words from all segments`);
|
@@ -734,18 +807,18 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
734
807
|
// Store the original global segment for reset functionality
|
735
808
|
setGlobalEditSegment(globalSegment)
|
736
809
|
console.log('EditAll - Set global edit segment');
|
737
|
-
|
810
|
+
|
738
811
|
setOriginalGlobalSegment(JSON.parse(JSON.stringify(globalSegment)))
|
739
812
|
console.log('EditAll - Set original global segment');
|
740
|
-
|
813
|
+
|
741
814
|
// Create the original transcribed global segment for Un-Correct functionality
|
742
815
|
if (originalData.original_segments) {
|
743
816
|
console.log('EditAll - Processing original segments for Un-Correct functionality');
|
744
|
-
|
817
|
+
|
745
818
|
// Get all words from original segments
|
746
819
|
const originalWords = originalData.original_segments.flatMap((segment: LyricsSegment) => segment.words)
|
747
820
|
console.log(`EditAll - Collected ${originalWords.length} words from original segments`);
|
748
|
-
|
821
|
+
|
749
822
|
// Sort words by start time
|
750
823
|
const sortedOriginalWords = [...originalWords].sort((a, b) => {
|
751
824
|
const aTime = a.start_time ?? 0
|
@@ -753,7 +826,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
753
826
|
return aTime - bTime
|
754
827
|
})
|
755
828
|
console.log('EditAll - Sorted original words by start time');
|
756
|
-
|
829
|
+
|
757
830
|
// Create the original transcribed global segment
|
758
831
|
const originalTranscribedGlobal: LyricsSegment = {
|
759
832
|
id: 'original-transcribed-global',
|
@@ -763,14 +836,14 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
763
836
|
end_time: sortedOriginalWords[sortedOriginalWords.length - 1]?.end_time ?? null
|
764
837
|
}
|
765
838
|
console.log('EditAll - Created original transcribed global segment');
|
766
|
-
|
839
|
+
|
767
840
|
setOriginalTranscribedGlobalSegment(originalTranscribedGlobal)
|
768
841
|
console.log('EditAll - Set original transcribed global segment');
|
769
842
|
} else {
|
770
843
|
setOriginalTranscribedGlobalSegment(null)
|
771
844
|
console.log('EditAll - No original segments found, set original transcribed global segment to null');
|
772
845
|
}
|
773
|
-
|
846
|
+
|
774
847
|
console.timeEnd('EditAll - Data processing');
|
775
848
|
} catch (error) {
|
776
849
|
console.error('Error preparing global edit data:', error);
|
@@ -867,15 +940,49 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
867
940
|
})
|
868
941
|
|
869
942
|
// Update the data with the new segments
|
870
|
-
|
943
|
+
const newData = {
|
871
944
|
...data,
|
872
945
|
corrected_segments: updatedSegments
|
873
|
-
}
|
946
|
+
};
|
947
|
+
updateDataWithHistory(newData, 'edit all'); // Update history
|
874
948
|
|
875
949
|
// Close the modal
|
876
950
|
setIsEditAllModalOpen(false)
|
877
951
|
setGlobalEditSegment(null)
|
878
|
-
}, [data])
|
952
|
+
}, [data, updateDataWithHistory])
|
953
|
+
|
954
|
+
// Undo/Redo handlers
|
955
|
+
const handleUndo = useCallback(() => {
|
956
|
+
if (historyIndex > 0) {
|
957
|
+
const newIndex = historyIndex - 1;
|
958
|
+
if (debugLog) {
|
959
|
+
console.log(`[DEBUG] Undo: moving from index ${historyIndex} to ${newIndex}. History length: ${history.length}`);
|
960
|
+
}
|
961
|
+
setHistoryIndex(newIndex);
|
962
|
+
} else {
|
963
|
+
if (debugLog) {
|
964
|
+
console.log(`[DEBUG] Undo: already at the beginning (index ${historyIndex})`);
|
965
|
+
}
|
966
|
+
}
|
967
|
+
}, [historyIndex, history])
|
968
|
+
|
969
|
+
const handleRedo = useCallback(() => {
|
970
|
+
if (historyIndex < history.length - 1) {
|
971
|
+
const newIndex = historyIndex + 1;
|
972
|
+
if (debugLog) {
|
973
|
+
console.log(`[DEBUG] Redo: moving from index ${historyIndex} to ${newIndex}. History length: ${history.length}`);
|
974
|
+
}
|
975
|
+
setHistoryIndex(newIndex);
|
976
|
+
} else {
|
977
|
+
if (debugLog) {
|
978
|
+
console.log(`[DEBUG] Redo: already at the end (index ${historyIndex}, history length ${history.length})`);
|
979
|
+
}
|
980
|
+
}
|
981
|
+
}, [historyIndex, history])
|
982
|
+
|
983
|
+
// Determine if Undo/Redo is possible
|
984
|
+
const canUndo = historyIndex > 0
|
985
|
+
const canRedo = historyIndex < history.length - 1
|
879
986
|
|
880
987
|
// Memoize the metric click handlers
|
881
988
|
const metricClickHandlers = useMemo(() => ({
|
@@ -887,6 +994,72 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
887
994
|
// Determine if any modal is open to disable highlighting
|
888
995
|
const isAnyModalOpenMemo = useMemo(() => isAnyModalOpen, [isAnyModalOpen]);
|
889
996
|
|
997
|
+
// Update keyboard handlers to include Undo/Redo shortcuts (Cmd/Ctrl + Z, Cmd/Ctrl + Shift + Z)
|
998
|
+
useEffect(() => {
|
999
|
+
const { currentModalHandler } = getModalState()
|
1000
|
+
|
1001
|
+
if (debugLog) {
|
1002
|
+
console.log('LyricsAnalyzer - Setting up keyboard effect (incl. Undo/Redo)', {
|
1003
|
+
isAnyModalOpen,
|
1004
|
+
hasSpacebarHandler: !!currentModalHandler
|
1005
|
+
})
|
1006
|
+
}
|
1007
|
+
|
1008
|
+
const { handleKeyDown: baseHandleKeyDown, handleKeyUp, cleanup } = setupKeyboardHandlers({
|
1009
|
+
setIsShiftPressed,
|
1010
|
+
setIsCtrlPressed
|
1011
|
+
})
|
1012
|
+
|
1013
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
1014
|
+
// Prevent Undo/Redo if a modal is open or input/textarea has focus
|
1015
|
+
const targetElement = e.target as HTMLElement;
|
1016
|
+
const isInputFocused = targetElement.tagName === 'INPUT' || targetElement.tagName === 'TEXTAREA';
|
1017
|
+
|
1018
|
+
if (!isAnyModalOpen && !isInputFocused) {
|
1019
|
+
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
1020
|
+
const modifierKey = isMac ? e.metaKey : e.ctrlKey;
|
1021
|
+
|
1022
|
+
if (modifierKey && e.key.toLowerCase() === 'z') {
|
1023
|
+
e.preventDefault();
|
1024
|
+
if (e.shiftKey) {
|
1025
|
+
if (canRedo) handleRedo();
|
1026
|
+
} else {
|
1027
|
+
if (canUndo) handleUndo();
|
1028
|
+
}
|
1029
|
+
return; // Prevent base handler if we handled undo/redo
|
1030
|
+
}
|
1031
|
+
}
|
1032
|
+
|
1033
|
+
// Call original handler for other keys or when conditions not met
|
1034
|
+
baseHandleKeyDown(e);
|
1035
|
+
};
|
1036
|
+
|
1037
|
+
// Always add keyboard listeners
|
1038
|
+
if (debugLog) {
|
1039
|
+
console.log('LyricsAnalyzer - Adding keyboard event listeners (incl. Undo/Redo)')
|
1040
|
+
}
|
1041
|
+
window.addEventListener('keydown', handleKeyDown)
|
1042
|
+
window.addEventListener('keyup', handleKeyUp)
|
1043
|
+
|
1044
|
+
// Reset modifier states when a modal opens
|
1045
|
+
if (isAnyModalOpen) {
|
1046
|
+
setIsShiftPressed(false)
|
1047
|
+
setIsCtrlPressed(false)
|
1048
|
+
}
|
1049
|
+
|
1050
|
+
// Cleanup function
|
1051
|
+
return () => {
|
1052
|
+
if (debugLog) {
|
1053
|
+
console.log('LyricsAnalyzer - Cleanup effect running (incl. Undo/Redo)')
|
1054
|
+
}
|
1055
|
+
window.removeEventListener('keydown', handleKeyDown)
|
1056
|
+
window.removeEventListener('keyup', handleKeyUp)
|
1057
|
+
document.body.style.userSelect = ''
|
1058
|
+
// Call the cleanup function to remove window blur/focus listeners
|
1059
|
+
cleanup()
|
1060
|
+
}
|
1061
|
+
}, [setIsShiftPressed, setIsCtrlPressed, isAnyModalOpen, handleUndo, handleRedo, canUndo, canRedo]);
|
1062
|
+
|
890
1063
|
return (
|
891
1064
|
<Box sx={{
|
892
1065
|
p: 1,
|
@@ -910,6 +1083,10 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
910
1083
|
onAddLyrics={() => setIsAddLyricsModalOpen(true)}
|
911
1084
|
onFindReplace={() => setIsFindReplaceModalOpen(true)}
|
912
1085
|
onEditAll={handleEditAll}
|
1086
|
+
onUndo={handleUndo}
|
1087
|
+
onRedo={handleRedo}
|
1088
|
+
canUndo={canUndo}
|
1089
|
+
canRedo={canRedo}
|
913
1090
|
/>
|
914
1091
|
|
915
1092
|
<Grid container direction={isMobile ? 'column' : 'row'}>
|
@@ -927,7 +1104,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
927
1104
|
anchors={data.anchor_sequences}
|
928
1105
|
disableHighlighting={isAnyModalOpenMemo}
|
929
1106
|
onDataChange={(updatedData) => {
|
930
|
-
|
1107
|
+
// Direct data change from TranscriptionView (e.g., drag-and-drop)
|
1108
|
+
// needs to update history
|
1109
|
+
updateDataWithHistory(updatedData, 'direct data change');
|
931
1110
|
}}
|
932
1111
|
/>
|
933
1112
|
{!isReadOnly && apiClient && (
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: lyrics-transcriber
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.49.0
|
4
4
|
Summary: Automatically create synchronised lyrics files in ASS and MidiCo LRC formats with word-level timestamps, using Whisper and lyrics from Genius and Spotify
|
5
5
|
License: MIT
|
6
6
|
Author: Andrew Beveridge
|
@@ -26,9 +26,9 @@ lyrics_transcriber/frontend/.yarn/install-state.gz,sha256=kcgQ-S9HvdNHexkXQVt18L
|
|
26
26
|
lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs,sha256=KTYy2KCV2OpHhussV5jIPDdUSr7RftMRhqPsRUmgfAY,2765465
|
27
27
|
lyrics_transcriber/frontend/.yarnrc.yml,sha256=0hZQ1OTcPqTUNBqQeme4VFkIzrsabHNzLtc_M-wSgIM,66
|
28
28
|
lyrics_transcriber/frontend/README.md,sha256=-D6CAfKTT7Y0V3EjlZ2fMy7fyctFQ4x2TJ9vx6xtccM,1607
|
29
|
-
lyrics_transcriber/frontend/dist/assets/index-
|
30
|
-
lyrics_transcriber/frontend/dist/assets/index-
|
31
|
-
lyrics_transcriber/frontend/dist/index.html,sha256=
|
29
|
+
lyrics_transcriber/frontend/dist/assets/index-BpvPgWoc.js,sha256=KdL6UuSIiP9poxBxCPmBkh0b7kMWzvxX0oIOTk1UKbE,1235484
|
30
|
+
lyrics_transcriber/frontend/dist/assets/index-BpvPgWoc.js.map,sha256=VLKx5Pu8Z6CqaJUFha9V9IR5Py8lmarwW4IGq81mdQA,2632916
|
31
|
+
lyrics_transcriber/frontend/dist/index.html,sha256=0bU3wDz5WFd63vGKK1pTN5gI6BUzjAQYHiN_GOZmsug,400
|
32
32
|
lyrics_transcriber/frontend/dist/vite.svg,sha256=SnSK_UQ5GLsWWRyDTEAdrjPoeGGrXbrQgRw6O0qSFPs,1497
|
33
33
|
lyrics_transcriber/frontend/eslint.config.js,sha256=3ADH23ANA4NNBKFy6nCVk65e8bx1DrVd_FIaYNnhuqA,734
|
34
34
|
lyrics_transcriber/frontend/index.html,sha256=KfqJVONzpUyPIwV73nZRiCWlwLnFWeB3z0vzxDPNudU,376
|
@@ -45,8 +45,8 @@ lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx,sha256=74dKgU
|
|
45
45
|
lyrics_transcriber/frontend/src/components/EditWordList.tsx,sha256=atl-9Z-24U-KWojwo0apTy1Y9DbQGoVo2dFX4P-1Z9E,13681
|
46
46
|
lyrics_transcriber/frontend/src/components/FileUpload.tsx,sha256=fwn2rMWtMLPTZLREMb3ps4prSf9nzxGwnjmeC6KYsJA,2383
|
47
47
|
lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx,sha256=U7duKns4IqNXwbWFbQfdyaswnvkSRpfsU0UG__-Serc,20192
|
48
|
-
lyrics_transcriber/frontend/src/components/Header.tsx,sha256=
|
49
|
-
lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx,sha256=
|
48
|
+
lyrics_transcriber/frontend/src/components/Header.tsx,sha256=jQzWy1yMh7s4D7x5-LU763nh1FvDS9ViiFmc5KkRnlE,14176
|
49
|
+
lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx,sha256=lHzuY7pP94BS2qoUjToWvN1AXye5KC9CUUTaIBN27mI,49362
|
50
50
|
lyrics_transcriber/frontend/src/components/ModeSelector.tsx,sha256=HnBAK_gFgNBJLtMC_ESMVdUapDjmqmoLX8pQeyHfpOw,2651
|
51
51
|
lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx,sha256=-9_ljoaadHjAAvia_vhEUgCajPv2jFnRDVR6VWN_a0w,4166
|
52
52
|
lyrics_transcriber/frontend/src/components/ReferenceView.tsx,sha256=thpf2ojge9pvZojUnGaiQroeHfBmxhUesO7945RGSfk,9499
|
@@ -148,8 +148,8 @@ lyrics_transcriber/transcribers/base_transcriber.py,sha256=T3m4ZCwZ9Bpv6Jvb2hNcn
|
|
148
148
|
lyrics_transcriber/transcribers/whisper.py,sha256=YcCB1ic9H6zL1GS0jD0emu8-qlcH0QVEjjjYB4aLlIQ,13260
|
149
149
|
lyrics_transcriber/types.py,sha256=d73cDstrEI_tVgngDYYYFwjZNs6OVBuAB_QDkga7dWA,19841
|
150
150
|
lyrics_transcriber/utils/word_utils.py,sha256=-cMGpj9UV4F6IsoDKAV2i1aiqSO8eI91HMAm_igtVMk,958
|
151
|
-
lyrics_transcriber-0.
|
152
|
-
lyrics_transcriber-0.
|
153
|
-
lyrics_transcriber-0.
|
154
|
-
lyrics_transcriber-0.
|
155
|
-
lyrics_transcriber-0.
|
151
|
+
lyrics_transcriber-0.49.0.dist-info/LICENSE,sha256=BiPihPDxhxIPEx6yAxVfAljD5Bhm_XG2teCbPEj_m0Y,1069
|
152
|
+
lyrics_transcriber-0.49.0.dist-info/METADATA,sha256=Zo2iq0ReIMLXzxqqbgffCL6EvAGnUNER_Ezx8LXgREY,6017
|
153
|
+
lyrics_transcriber-0.49.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
154
|
+
lyrics_transcriber-0.49.0.dist-info/entry_points.txt,sha256=kcp-bSFkCACAEA0t166Kek0HpaJUXRo5SlF5tVrqNBU,216
|
155
|
+
lyrics_transcriber-0.49.0.dist-info/RECORD,,
|