lyrics-transcriber 0.42.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/frontend/dist/assets/{index-coH8y7gV.js → index-D0Gr3Ep7.js} +283 -64
- lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +7 -0
- lyrics_transcriber/frontend/src/components/EditModal.tsx +198 -30
- lyrics_transcriber/frontend/src/components/Header.tsx +0 -2
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +19 -3
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +4 -1
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +54 -17
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +32 -2
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +0 -1
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +0 -3
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +33 -1
- lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- {lyrics_transcriber-0.42.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/METADATA +1 -1
- {lyrics_transcriber-0.42.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/RECORD +20 -19
- lyrics_transcriber/frontend/dist/assets/index-coH8y7gV.js.map +0 -1
- {lyrics_transcriber-0.42.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.42.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/WHEEL +0 -0
- {lyrics_transcriber-0.42.0.dist-info → lyrics_transcriber-0.43.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-D0Gr3Ep7.js"></script>
|
9
9
|
</head>
|
10
10
|
<body>
|
11
11
|
<div id="root"></div>
|
@@ -33,16 +33,21 @@ export default function AudioPlayer({ apiClient, onTimeUpdate, audioHash }: Audi
|
|
33
33
|
}
|
34
34
|
|
35
35
|
audio.addEventListener('play', () => {
|
36
|
+
setIsPlaying(true)
|
37
|
+
window.isAudioPlaying = true
|
36
38
|
updateTime()
|
37
39
|
})
|
38
40
|
|
39
41
|
audio.addEventListener('pause', () => {
|
42
|
+
setIsPlaying(false)
|
43
|
+
window.isAudioPlaying = false
|
40
44
|
cancelAnimationFrame(animationFrameId)
|
41
45
|
})
|
42
46
|
|
43
47
|
audio.addEventListener('ended', () => {
|
44
48
|
cancelAnimationFrame(animationFrameId)
|
45
49
|
setIsPlaying(false)
|
50
|
+
window.isAudioPlaying = false
|
46
51
|
setCurrentTime(0)
|
47
52
|
})
|
48
53
|
|
@@ -55,6 +60,7 @@ export default function AudioPlayer({ apiClient, onTimeUpdate, audioHash }: Audi
|
|
55
60
|
audio.pause()
|
56
61
|
audio.src = ''
|
57
62
|
audioRef.current = null
|
63
|
+
window.isAudioPlaying = false
|
58
64
|
}
|
59
65
|
}, [apiClient, onTimeUpdate, audioHash])
|
60
66
|
|
@@ -107,6 +113,7 @@ export default function AudioPlayer({ apiClient, onTimeUpdate, audioHash }: Audi
|
|
107
113
|
useEffect(() => {
|
108
114
|
if (!apiClient) return
|
109
115
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
110
117
|
const win = window as any
|
111
118
|
win.seekAndPlayAudio = seekAndPlay
|
112
119
|
win.toggleAudioPlayback = togglePlayback
|
@@ -20,8 +20,10 @@ import RestoreIcon from '@mui/icons-material/RestoreFromTrash'
|
|
20
20
|
import MoreVertIcon from '@mui/icons-material/MoreVert'
|
21
21
|
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
|
22
22
|
import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
|
23
|
+
import CancelIcon from '@mui/icons-material/Cancel'
|
24
|
+
import StopIcon from '@mui/icons-material/Stop'
|
23
25
|
import { LyricsSegment, Word } from '../types'
|
24
|
-
import { useState, useEffect } from 'react'
|
26
|
+
import { useState, useEffect, useCallback } from 'react'
|
25
27
|
import TimelineEditor from './TimelineEditor'
|
26
28
|
import { nanoid } from 'nanoid'
|
27
29
|
|
@@ -37,6 +39,7 @@ interface EditModalProps {
|
|
37
39
|
onDelete?: (segmentIndex: number) => void
|
38
40
|
onAddSegment?: (segmentIndex: number) => void
|
39
41
|
onSplitSegment?: (segmentIndex: number, afterWordIndex: number) => void
|
42
|
+
setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
|
40
43
|
}
|
41
44
|
|
42
45
|
export default function EditModal({
|
@@ -51,26 +54,150 @@ export default function EditModal({
|
|
51
54
|
onDelete,
|
52
55
|
onAddSegment,
|
53
56
|
onSplitSegment,
|
57
|
+
setModalSpacebarHandler,
|
54
58
|
}: EditModalProps) {
|
59
|
+
// All useState hooks
|
55
60
|
const [editedSegment, setEditedSegment] = useState<LyricsSegment | null>(segment)
|
56
61
|
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null)
|
57
62
|
const [selectedWordIndex, setSelectedWordIndex] = useState<number | null>(null)
|
58
63
|
const [replacementText, setReplacementText] = useState('')
|
64
|
+
const [isManualSyncing, setIsManualSyncing] = useState(false)
|
65
|
+
const [syncWordIndex, setSyncWordIndex] = useState<number>(-1)
|
66
|
+
const [isPlaying, setIsPlaying] = useState(false)
|
59
67
|
|
60
|
-
//
|
68
|
+
// Define updateSegment first since other hooks depend on it
|
69
|
+
const updateSegment = useCallback((newWords: Word[]) => {
|
70
|
+
if (!editedSegment) return;
|
71
|
+
|
72
|
+
const validStartTimes = newWords.map(w => w.start_time).filter((t): t is number => t !== null)
|
73
|
+
const validEndTimes = newWords.map(w => w.end_time).filter((t): t is number => t !== null)
|
74
|
+
|
75
|
+
const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null
|
76
|
+
const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null
|
77
|
+
|
78
|
+
setEditedSegment({
|
79
|
+
...editedSegment,
|
80
|
+
words: newWords,
|
81
|
+
text: newWords.map(w => w.text).join(' '),
|
82
|
+
start_time: segmentStartTime,
|
83
|
+
end_time: segmentEndTime
|
84
|
+
})
|
85
|
+
}, [editedSegment])
|
86
|
+
|
87
|
+
// Other useCallback hooks
|
88
|
+
const cleanupManualSync = useCallback(() => {
|
89
|
+
setIsManualSyncing(false)
|
90
|
+
setSyncWordIndex(-1)
|
91
|
+
}, [])
|
92
|
+
|
93
|
+
const handleClose = useCallback(() => {
|
94
|
+
cleanupManualSync()
|
95
|
+
onClose()
|
96
|
+
}, [onClose, cleanupManualSync])
|
97
|
+
|
98
|
+
// All useEffect hooks
|
61
99
|
useEffect(() => {
|
62
100
|
setEditedSegment(segment)
|
63
101
|
}, [segment])
|
64
102
|
|
103
|
+
// Update the spacebar handler when modal state changes
|
104
|
+
useEffect(() => {
|
105
|
+
if (open) {
|
106
|
+
setModalSpacebarHandler(() => (e: KeyboardEvent) => {
|
107
|
+
e.preventDefault()
|
108
|
+
e.stopPropagation()
|
109
|
+
|
110
|
+
if (isManualSyncing && editedSegment) {
|
111
|
+
// Handle manual sync mode
|
112
|
+
if (syncWordIndex < editedSegment.words.length) {
|
113
|
+
const newWords = [...editedSegment.words]
|
114
|
+
const currentWord = newWords[syncWordIndex]
|
115
|
+
const prevWord = syncWordIndex > 0 ? newWords[syncWordIndex - 1] : null
|
116
|
+
|
117
|
+
currentWord.start_time = currentTime
|
118
|
+
|
119
|
+
if (prevWord) {
|
120
|
+
prevWord.end_time = currentTime - 0.01
|
121
|
+
}
|
122
|
+
|
123
|
+
if (syncWordIndex === editedSegment.words.length - 1) {
|
124
|
+
currentWord.end_time = editedSegment.end_time
|
125
|
+
setIsManualSyncing(false)
|
126
|
+
setSyncWordIndex(-1)
|
127
|
+
updateSegment(newWords)
|
128
|
+
} else {
|
129
|
+
setSyncWordIndex(syncWordIndex + 1)
|
130
|
+
updateSegment(newWords)
|
131
|
+
}
|
132
|
+
}
|
133
|
+
} else if (editedSegment && onPlaySegment) {
|
134
|
+
// Toggle segment playback when not in manual sync mode
|
135
|
+
const startTime = editedSegment.start_time ?? 0
|
136
|
+
const endTime = editedSegment.end_time ?? 0
|
137
|
+
|
138
|
+
if (currentTime >= startTime && currentTime <= endTime) {
|
139
|
+
if (window.toggleAudioPlayback) {
|
140
|
+
window.toggleAudioPlayback()
|
141
|
+
}
|
142
|
+
} else {
|
143
|
+
onPlaySegment(startTime)
|
144
|
+
}
|
145
|
+
}
|
146
|
+
})
|
147
|
+
} else {
|
148
|
+
setModalSpacebarHandler(undefined)
|
149
|
+
}
|
150
|
+
|
151
|
+
return () => {
|
152
|
+
setModalSpacebarHandler(undefined)
|
153
|
+
}
|
154
|
+
}, [
|
155
|
+
open,
|
156
|
+
isManualSyncing,
|
157
|
+
editedSegment,
|
158
|
+
syncWordIndex,
|
159
|
+
currentTime,
|
160
|
+
onPlaySegment,
|
161
|
+
updateSegment,
|
162
|
+
setModalSpacebarHandler
|
163
|
+
])
|
164
|
+
|
165
|
+
// Auto-stop sync if we go past the end time
|
166
|
+
useEffect(() => {
|
167
|
+
if (!editedSegment) return
|
168
|
+
|
169
|
+
const endTime = editedSegment.end_time ?? 0
|
170
|
+
|
171
|
+
if (window.isAudioPlaying && currentTime > endTime) {
|
172
|
+
console.log('Stopping playback: current time exceeded end time')
|
173
|
+
window.toggleAudioPlayback?.()
|
174
|
+
setIsManualSyncing(false)
|
175
|
+
setSyncWordIndex(-1)
|
176
|
+
}
|
177
|
+
|
178
|
+
}, [isManualSyncing, editedSegment, currentTime, setSyncWordIndex])
|
179
|
+
|
180
|
+
// Update isPlaying when currentTime changes
|
181
|
+
useEffect(() => {
|
182
|
+
if (editedSegment) {
|
183
|
+
const startTime = editedSegment.start_time ?? 0
|
184
|
+
const endTime = editedSegment.end_time ?? 0
|
185
|
+
const isWithinSegment = currentTime >= startTime && currentTime <= endTime
|
186
|
+
|
187
|
+
// Only consider it playing if it's within the segment AND audio is actually playing
|
188
|
+
setIsPlaying(isWithinSegment && window.isAudioPlaying === true)
|
189
|
+
}
|
190
|
+
}, [currentTime, editedSegment])
|
191
|
+
|
65
192
|
// Add a function to get safe time values
|
66
193
|
const getSafeTimeRange = (segment: LyricsSegment | null) => {
|
67
194
|
if (!segment) return { start: 0, end: 1 }; // Default 1-second range
|
68
|
-
|
69
195
|
const start = segment.start_time ?? 0;
|
70
196
|
const end = segment.end_time ?? (start + 1);
|
71
197
|
return { start, end };
|
72
198
|
}
|
73
199
|
|
200
|
+
// Early return after all hooks and function definitions
|
74
201
|
if (!segment || segmentIndex === null || !editedSegment || !originalSegment) return null
|
75
202
|
|
76
203
|
// Get safe time values for TimelineEditor
|
@@ -85,23 +212,6 @@ export default function EditModal({
|
|
85
212
|
updateSegment(newWords)
|
86
213
|
}
|
87
214
|
|
88
|
-
const updateSegment = (newWords: Word[]) => {
|
89
|
-
// Filter out null values before finding min/max
|
90
|
-
const validStartTimes = newWords.map(w => w.start_time).filter((t): t is number => t !== null)
|
91
|
-
const validEndTimes = newWords.map(w => w.end_time).filter((t): t is number => t !== null)
|
92
|
-
|
93
|
-
const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null
|
94
|
-
const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null
|
95
|
-
|
96
|
-
setEditedSegment({
|
97
|
-
...editedSegment,
|
98
|
-
words: newWords,
|
99
|
-
text: newWords.map(w => w.text).join(' '),
|
100
|
-
start_time: segmentStartTime,
|
101
|
-
end_time: segmentEndTime
|
102
|
-
})
|
103
|
-
}
|
104
|
-
|
105
215
|
const handleAddWord = (index?: number) => {
|
106
216
|
const newWords = [...editedSegment.words]
|
107
217
|
let newWord: Word
|
@@ -285,10 +395,42 @@ export default function EditModal({
|
|
285
395
|
}
|
286
396
|
}
|
287
397
|
|
398
|
+
// Add this new function to handle manual sync
|
399
|
+
const startManualSync = () => {
|
400
|
+
if (isManualSyncing) {
|
401
|
+
setIsManualSyncing(false)
|
402
|
+
setSyncWordIndex(-1)
|
403
|
+
return
|
404
|
+
}
|
405
|
+
|
406
|
+
if (!editedSegment || !onPlaySegment) return
|
407
|
+
|
408
|
+
setIsManualSyncing(true)
|
409
|
+
setSyncWordIndex(0)
|
410
|
+
// Start playing 3 seconds before segment start
|
411
|
+
const startTime = (editedSegment.start_time ?? 0) - 3
|
412
|
+
onPlaySegment(startTime)
|
413
|
+
}
|
414
|
+
|
415
|
+
// Handle play/stop button click
|
416
|
+
const handlePlayButtonClick = () => {
|
417
|
+
if (!segment?.start_time || !onPlaySegment) return
|
418
|
+
|
419
|
+
if (isPlaying) {
|
420
|
+
// Stop playback
|
421
|
+
if (window.toggleAudioPlayback) {
|
422
|
+
window.toggleAudioPlayback()
|
423
|
+
}
|
424
|
+
} else {
|
425
|
+
// Start playback
|
426
|
+
onPlaySegment(segment.start_time)
|
427
|
+
}
|
428
|
+
}
|
429
|
+
|
288
430
|
return (
|
289
431
|
<Dialog
|
290
432
|
open={open}
|
291
|
-
onClose={
|
433
|
+
onClose={handleClose}
|
292
434
|
maxWidth="md"
|
293
435
|
fullWidth
|
294
436
|
onKeyDown={handleKeyDown}
|
@@ -299,10 +441,14 @@ export default function EditModal({
|
|
299
441
|
{segment?.start_time !== null && onPlaySegment && (
|
300
442
|
<IconButton
|
301
443
|
size="small"
|
302
|
-
onClick={
|
444
|
+
onClick={handlePlayButtonClick}
|
303
445
|
sx={{ padding: '4px' }}
|
304
446
|
>
|
305
|
-
|
447
|
+
{isPlaying ? (
|
448
|
+
<StopIcon />
|
449
|
+
) : (
|
450
|
+
<PlayCircleOutlineIcon />
|
451
|
+
)}
|
306
452
|
</IconButton>
|
307
453
|
)}
|
308
454
|
</Box>
|
@@ -322,11 +468,30 @@ export default function EditModal({
|
|
322
468
|
/>
|
323
469
|
</Box>
|
324
470
|
|
325
|
-
<
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
471
|
+
<Box sx={{ mb: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
472
|
+
<Typography variant="body2" color="text.secondary">
|
473
|
+
Original Time Range: {originalSegment.start_time?.toFixed(2) ?? 'N/A'} - {originalSegment.end_time?.toFixed(2) ?? 'N/A'}
|
474
|
+
<br />
|
475
|
+
Current Time Range: {editedSegment.start_time?.toFixed(2) ?? 'N/A'} - {editedSegment.end_time?.toFixed(2) ?? 'N/A'}
|
476
|
+
</Typography>
|
477
|
+
|
478
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
479
|
+
<Button
|
480
|
+
variant={isManualSyncing ? "outlined" : "contained"}
|
481
|
+
onClick={startManualSync}
|
482
|
+
disabled={!onPlaySegment}
|
483
|
+
startIcon={isManualSyncing ? <CancelIcon /> : <PlayCircleOutlineIcon />}
|
484
|
+
color={isManualSyncing ? "error" : "primary"}
|
485
|
+
>
|
486
|
+
{isManualSyncing ? "Cancel Sync" : "Manual Sync"}
|
487
|
+
</Button>
|
488
|
+
{isManualSyncing && (
|
489
|
+
<Typography variant="body2">
|
490
|
+
Press spacebar for word {syncWordIndex + 1} of {editedSegment?.words.length}
|
491
|
+
</Typography>
|
492
|
+
)}
|
493
|
+
</Box>
|
494
|
+
</Box>
|
330
495
|
|
331
496
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 3 }}>
|
332
497
|
{editedSegment.words.map((word, index) => (
|
@@ -413,8 +578,11 @@ export default function EditModal({
|
|
413
578
|
Delete Segment
|
414
579
|
</Button>
|
415
580
|
</Box>
|
416
|
-
<Button onClick={
|
417
|
-
<Button onClick={
|
581
|
+
<Button onClick={handleClose}>Cancel</Button>
|
582
|
+
<Button onClick={() => {
|
583
|
+
cleanupManualSync()
|
584
|
+
onSave(editedSegment)
|
585
|
+
}}>
|
418
586
|
Save Changes
|
419
587
|
</Button>
|
420
588
|
</DialogActions>
|
@@ -86,8 +86,6 @@ export default function Header({
|
|
86
86
|
const addedCount = data.corrections.filter(c => c.split_total).length
|
87
87
|
const deletedCount = data.corrections.filter(c => c.is_deletion).length
|
88
88
|
|
89
|
-
console.log('Header: Render with isUpdatingHandlers =', isUpdatingHandlers)
|
90
|
-
|
91
89
|
return (
|
92
90
|
<>
|
93
91
|
{isReadOnly && (
|
@@ -22,7 +22,7 @@ import {
|
|
22
22
|
updateSegment
|
23
23
|
} from './shared/utils/segmentOperations'
|
24
24
|
import { loadSavedData, saveData, clearSavedData } from './shared/utils/localStorage'
|
25
|
-
import { setupKeyboardHandlers } from './shared/utils/keyboardHandlers'
|
25
|
+
import { setupKeyboardHandlers, setModalHandler } from './shared/utils/keyboardHandlers'
|
26
26
|
import Header from './Header'
|
27
27
|
import { findWordById, getWordsFromIds } from './shared/utils/wordUtils'
|
28
28
|
|
@@ -121,19 +121,24 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
121
121
|
|
122
122
|
// Keyboard handlers
|
123
123
|
useEffect(() => {
|
124
|
+
console.log('Setting up keyboard handlers in LyricsAnalyzer')
|
125
|
+
|
124
126
|
const { handleKeyDown, handleKeyUp } = setupKeyboardHandlers({
|
125
127
|
setIsShiftPressed,
|
126
128
|
setIsCtrlPressed
|
127
129
|
})
|
128
130
|
|
131
|
+
console.log('Adding keyboard event listeners')
|
129
132
|
window.addEventListener('keydown', handleKeyDown)
|
130
133
|
window.addEventListener('keyup', handleKeyUp)
|
134
|
+
|
131
135
|
return () => {
|
136
|
+
console.log('Removing keyboard event listeners')
|
132
137
|
window.removeEventListener('keydown', handleKeyDown)
|
133
138
|
window.removeEventListener('keyup', handleKeyUp)
|
134
139
|
document.body.style.userSelect = ''
|
135
140
|
}
|
136
|
-
}, [])
|
141
|
+
}, [setIsShiftPressed, setIsCtrlPressed])
|
137
142
|
|
138
143
|
// Calculate effective mode based on modifier key states
|
139
144
|
const effectiveMode = isShiftPressed ? 'highlight' :
|
@@ -436,6 +441,12 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
436
441
|
}, 1500);
|
437
442
|
}, []);
|
438
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
|
+
}, [])
|
449
|
+
|
439
450
|
return (
|
440
451
|
<Box sx={{
|
441
452
|
p: 3,
|
@@ -505,7 +516,10 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
505
516
|
|
506
517
|
<EditModal
|
507
518
|
open={Boolean(editModalSegment)}
|
508
|
-
onClose={() =>
|
519
|
+
onClose={() => {
|
520
|
+
setEditModalSegment(null)
|
521
|
+
handleSetModalSpacebarHandler(undefined)
|
522
|
+
}}
|
509
523
|
segment={editModalSegment?.segment ?? null}
|
510
524
|
segmentIndex={editModalSegment?.index ?? null}
|
511
525
|
originalSegment={editModalSegment?.originalSegment ?? null}
|
@@ -515,6 +529,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
515
529
|
onSplitSegment={handleSplitSegment}
|
516
530
|
onPlaySegment={handlePlaySegment}
|
517
531
|
currentTime={currentAudioTime}
|
532
|
+
setModalSpacebarHandler={handleSetModalSpacebarHandler}
|
518
533
|
/>
|
519
534
|
|
520
535
|
<ReviewChangesModal
|
@@ -524,6 +539,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
524
539
|
updatedData={data}
|
525
540
|
onSubmit={handleSubmitToServer}
|
526
541
|
apiClient={apiClient}
|
542
|
+
setModalSpacebarHandler={handleSetModalSpacebarHandler}
|
527
543
|
/>
|
528
544
|
|
529
545
|
{!isReadOnly && apiClient && (
|
@@ -7,12 +7,14 @@ interface PreviewVideoSectionProps {
|
|
7
7
|
apiClient: ApiClient | null
|
8
8
|
isModalOpen: boolean
|
9
9
|
updatedData: CorrectionData
|
10
|
+
videoRef?: React.RefObject<HTMLVideoElement>
|
10
11
|
}
|
11
12
|
|
12
13
|
export default function PreviewVideoSection({
|
13
14
|
apiClient,
|
14
15
|
isModalOpen,
|
15
|
-
updatedData
|
16
|
+
updatedData,
|
17
|
+
videoRef
|
16
18
|
}: PreviewVideoSectionProps) {
|
17
19
|
const [previewState, setPreviewState] = useState<{
|
18
20
|
status: 'loading' | 'ready' | 'error';
|
@@ -100,6 +102,7 @@ export default function PreviewVideoSection({
|
|
100
102
|
margin: '0',
|
101
103
|
}}>
|
102
104
|
<video
|
105
|
+
ref={videoRef}
|
103
106
|
controls
|
104
107
|
src={previewState.videoUrl}
|
105
108
|
style={{
|
@@ -1,11 +1,26 @@
|
|
1
1
|
import { useMemo } from 'react'
|
2
|
-
import { Paper, Typography, Box } from '@mui/material'
|
2
|
+
import { Paper, Typography, Box, IconButton } from '@mui/material'
|
3
3
|
import { ReferenceViewProps } from './shared/types'
|
4
4
|
import { calculateReferenceLinePositions } from './shared/utils/referenceLineCalculator'
|
5
5
|
import { SourceSelector } from './shared/components/SourceSelector'
|
6
6
|
import { HighlightedText } from './shared/components/HighlightedText'
|
7
7
|
import { TranscriptionWordPosition } from './shared/types'
|
8
8
|
import { getWordsFromIds } from './shared/utils/wordUtils'
|
9
|
+
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
10
|
+
import { styled } from '@mui/material/styles'
|
11
|
+
|
12
|
+
const SegmentControls = styled(Box)({
|
13
|
+
display: 'flex',
|
14
|
+
alignItems: 'center',
|
15
|
+
gap: '4px',
|
16
|
+
paddingTop: '3px',
|
17
|
+
paddingRight: '8px'
|
18
|
+
})
|
19
|
+
|
20
|
+
const TextContainer = styled(Box)({
|
21
|
+
flex: 1,
|
22
|
+
minWidth: 0,
|
23
|
+
})
|
9
24
|
|
10
25
|
export default function ReferenceView({
|
11
26
|
referenceSources,
|
@@ -153,6 +168,11 @@ export default function ReferenceView({
|
|
153
168
|
// Get the segments for the current source
|
154
169
|
const currentSourceSegments = referenceSources[effectiveCurrentSource]?.segments || [];
|
155
170
|
|
171
|
+
// Helper function to copy text to clipboard
|
172
|
+
const copyToClipboard = (text: string) => {
|
173
|
+
navigator.clipboard.writeText(text);
|
174
|
+
};
|
175
|
+
|
156
176
|
return (
|
157
177
|
<Paper sx={{ p: 2 }}>
|
158
178
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
@@ -166,22 +186,39 @@ export default function ReferenceView({
|
|
166
186
|
/>
|
167
187
|
</Box>
|
168
188
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
189
|
+
{currentSourceSegments.map((segment, index) => (
|
190
|
+
<Box key={index} sx={{ display: 'flex', alignItems: 'flex-start', width: '100%' }}>
|
191
|
+
<SegmentControls>
|
192
|
+
<IconButton
|
193
|
+
size="small"
|
194
|
+
onClick={() => copyToClipboard(segment.words.map(w => w.text).join(' '))}
|
195
|
+
sx={{ padding: '2px' }}
|
196
|
+
>
|
197
|
+
<ContentCopyIcon fontSize="small" />
|
198
|
+
</IconButton>
|
199
|
+
</SegmentControls>
|
200
|
+
<TextContainer>
|
201
|
+
<HighlightedText
|
202
|
+
wordPositions={referenceWordPositions.filter(wp =>
|
203
|
+
segment.words.some(w => w.id === wp.word.id)
|
204
|
+
)}
|
205
|
+
segments={[segment]}
|
206
|
+
anchors={anchors}
|
207
|
+
onElementClick={onElementClick}
|
208
|
+
onWordClick={onWordClick}
|
209
|
+
flashingType={flashingType}
|
210
|
+
highlightInfo={highlightInfo}
|
211
|
+
mode={mode}
|
212
|
+
isReference={true}
|
213
|
+
currentSource={effectiveCurrentSource}
|
214
|
+
linePositions={linePositions}
|
215
|
+
referenceCorrections={referenceCorrections}
|
216
|
+
gaps={gaps}
|
217
|
+
preserveSegments={true}
|
218
|
+
/>
|
219
|
+
</TextContainer>
|
220
|
+
</Box>
|
221
|
+
))}
|
185
222
|
</Box>
|
186
223
|
</Paper>
|
187
224
|
)
|
@@ -9,7 +9,7 @@ import {
|
|
9
9
|
Paper
|
10
10
|
} from '@mui/material'
|
11
11
|
import { CorrectionData } from '../types'
|
12
|
-
import { useMemo } from 'react'
|
12
|
+
import { useMemo, useRef, useEffect } from 'react'
|
13
13
|
import { ApiClient } from '../api'
|
14
14
|
import PreviewVideoSection from './PreviewVideoSection'
|
15
15
|
|
@@ -20,6 +20,7 @@ interface ReviewChangesModalProps {
|
|
20
20
|
updatedData: CorrectionData
|
21
21
|
onSubmit: () => void
|
22
22
|
apiClient: ApiClient | null
|
23
|
+
setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
|
23
24
|
}
|
24
25
|
|
25
26
|
interface DiffResult {
|
@@ -66,8 +67,36 @@ export default function ReviewChangesModal({
|
|
66
67
|
originalData,
|
67
68
|
updatedData,
|
68
69
|
onSubmit,
|
69
|
-
apiClient
|
70
|
+
apiClient,
|
71
|
+
setModalSpacebarHandler
|
70
72
|
}: ReviewChangesModalProps) {
|
73
|
+
// Add ref to video element
|
74
|
+
const videoRef = useRef<HTMLVideoElement>(null)
|
75
|
+
|
76
|
+
// Add effect to handle spacebar
|
77
|
+
useEffect(() => {
|
78
|
+
if (open) {
|
79
|
+
setModalSpacebarHandler(() => (e: KeyboardEvent) => {
|
80
|
+
e.preventDefault()
|
81
|
+
e.stopPropagation()
|
82
|
+
|
83
|
+
if (videoRef.current) {
|
84
|
+
if (videoRef.current.paused) {
|
85
|
+
videoRef.current.play()
|
86
|
+
} else {
|
87
|
+
videoRef.current.pause()
|
88
|
+
}
|
89
|
+
}
|
90
|
+
})
|
91
|
+
} else {
|
92
|
+
setModalSpacebarHandler(undefined)
|
93
|
+
}
|
94
|
+
|
95
|
+
return () => {
|
96
|
+
setModalSpacebarHandler(undefined)
|
97
|
+
}
|
98
|
+
}, [open, setModalSpacebarHandler])
|
99
|
+
|
71
100
|
const differences = useMemo(() => {
|
72
101
|
const diffs: DiffResult[] = []
|
73
102
|
|
@@ -252,6 +281,7 @@ export default function ReviewChangesModal({
|
|
252
281
|
apiClient={apiClient}
|
253
282
|
isModalOpen={open}
|
254
283
|
updatedData={updatedData}
|
284
|
+
videoRef={videoRef} // Pass the ref to PreviewVideoSection
|
255
285
|
/>
|
256
286
|
|
257
287
|
<Box sx={{ p: 2, mt: 0 }}>
|
@@ -48,7 +48,6 @@ export default function TranscriptionView({
|
|
48
48
|
currentTime = 0,
|
49
49
|
anchors = []
|
50
50
|
}: TranscriptionViewProps) {
|
51
|
-
console.log('TranscriptionView props:', { flashingType, flashingHandler });
|
52
51
|
const [selectedSegmentIndex, setSelectedSegmentIndex] = useState<number | null>(null)
|
53
52
|
|
54
53
|
return (
|
@@ -58,8 +58,6 @@ export function HighlightedText({
|
|
58
58
|
flashingHandler,
|
59
59
|
corrections = [],
|
60
60
|
}: HighlightedTextProps) {
|
61
|
-
console.log('HighlightedText props:', { flashingType, flashingHandler });
|
62
|
-
|
63
61
|
const { handleWordClick } = useWordClick({
|
64
62
|
mode,
|
65
63
|
onElementClick,
|
@@ -73,7 +71,6 @@ export function HighlightedText({
|
|
73
71
|
|
74
72
|
const shouldWordFlash = (wordPos: TranscriptionWordPosition | { word: string; id: string }): boolean => {
|
75
73
|
if (!flashingType) {
|
76
|
-
console.log('No flashingType');
|
77
74
|
return false;
|
78
75
|
}
|
79
76
|
|