lyrics-transcriber 0.49.3__py3-none-any.whl → 0.52.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lyrics_transcriber/cli/cli_main.py +5 -1
- lyrics_transcriber/frontend/dist/assets/{index-DSQidWB1.js → index-C5ftSgQo.js} +633 -60
- lyrics_transcriber/frontend/dist/assets/index-C5ftSgQo.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/components/Header.tsx +31 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +125 -18
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +26 -3
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +16 -1
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +8 -0
- lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/output/cdg.py +10 -0
- lyrics_transcriber/transcribers/audioshake.py +1 -1
- {lyrics_transcriber-0.49.3.dist-info → lyrics_transcriber-0.52.0.dist-info}/LICENSE +1 -1
- {lyrics_transcriber-0.49.3.dist-info → lyrics_transcriber-0.52.0.dist-info}/METADATA +10 -9
- {lyrics_transcriber-0.49.3.dist-info → lyrics_transcriber-0.52.0.dist-info}/RECORD +19 -17
- {lyrics_transcriber-0.49.3.dist-info → lyrics_transcriber-0.52.0.dist-info}/WHEEL +1 -1
- lyrics_transcriber/frontend/dist/assets/index-DSQidWB1.js.map +0 -1
- {lyrics_transcriber-0.49.3.dist-info → lyrics_transcriber-0.52.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-C5ftSgQo.js"></script>
|
9
9
|
</head>
|
10
10
|
<body>
|
11
11
|
<div id="root"></div>
|
@@ -5,6 +5,7 @@ import FindReplaceIcon from '@mui/icons-material/FindReplace'
|
|
5
5
|
import EditIcon from '@mui/icons-material/Edit'
|
6
6
|
import UndoIcon from '@mui/icons-material/Undo'
|
7
7
|
import RedoIcon from '@mui/icons-material/Redo'
|
8
|
+
import TimerIcon from '@mui/icons-material/Timer'
|
8
9
|
import { CorrectionData, InteractionMode } from '../types'
|
9
10
|
import CorrectionMetrics from './CorrectionMetrics'
|
10
11
|
import ModeSelector from './ModeSelector'
|
@@ -31,6 +32,8 @@ interface HeaderProps {
|
|
31
32
|
onHandlerClick?: (handler: string) => void
|
32
33
|
onFindReplace?: () => void
|
33
34
|
onEditAll?: () => void
|
35
|
+
onTimingOffset?: () => void
|
36
|
+
timingOffsetMs?: number
|
34
37
|
onUndo: () => void
|
35
38
|
onRedo: () => void
|
36
39
|
canUndo: boolean
|
@@ -52,6 +55,8 @@ export default function Header({
|
|
52
55
|
onHandlerClick,
|
53
56
|
onFindReplace,
|
54
57
|
onEditAll,
|
58
|
+
onTimingOffset,
|
59
|
+
timingOffsetMs = 0,
|
55
60
|
onUndo,
|
56
61
|
onRedo,
|
57
62
|
canUndo,
|
@@ -311,6 +316,32 @@ export default function Header({
|
|
311
316
|
Edit All
|
312
317
|
</Button>
|
313
318
|
)}
|
319
|
+
{!isReadOnly && onTimingOffset && (
|
320
|
+
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
321
|
+
<Button
|
322
|
+
variant="outlined"
|
323
|
+
size="small"
|
324
|
+
onClick={onTimingOffset}
|
325
|
+
startIcon={<TimerIcon />}
|
326
|
+
color={timingOffsetMs !== 0 ? "secondary" : "primary"}
|
327
|
+
sx={{ minWidth: 'fit-content', height: '32px' }}
|
328
|
+
>
|
329
|
+
Timing Offset
|
330
|
+
</Button>
|
331
|
+
{timingOffsetMs !== 0 && (
|
332
|
+
<Typography
|
333
|
+
variant="body2"
|
334
|
+
sx={{
|
335
|
+
ml: 1,
|
336
|
+
fontWeight: 'bold',
|
337
|
+
color: theme.palette.secondary.main
|
338
|
+
}}
|
339
|
+
>
|
340
|
+
{timingOffsetMs > 0 ? '+' : ''}{timingOffsetMs}ms
|
341
|
+
</Typography>
|
342
|
+
)}
|
343
|
+
</Box>
|
344
|
+
)}
|
314
345
|
<AudioPlayer
|
315
346
|
apiClient={apiClient}
|
316
347
|
onTimeUpdate={onTimeUpdate}
|
@@ -31,6 +31,8 @@ import { getWordsFromIds } from './shared/utils/wordUtils'
|
|
31
31
|
import AddLyricsModal from './AddLyricsModal'
|
32
32
|
import { RestoreFromTrash, OndemandVideo } from '@mui/icons-material'
|
33
33
|
import FindReplaceModal from './FindReplaceModal'
|
34
|
+
import TimingOffsetModal from './TimingOffsetModal'
|
35
|
+
import { applyOffsetToCorrectionData, applyOffsetToSegment } from './shared/utils/timingUtils'
|
34
36
|
|
35
37
|
// Add type for window augmentation at the top of the file
|
36
38
|
declare global {
|
@@ -185,6 +187,8 @@ interface MemoizedHeaderProps {
|
|
185
187
|
onAddLyrics?: () => void
|
186
188
|
onFindReplace?: () => void
|
187
189
|
onEditAll?: () => void
|
190
|
+
onTimingOffset: () => void
|
191
|
+
timingOffsetMs: number
|
188
192
|
onUndo: () => void
|
189
193
|
onRedo: () => void
|
190
194
|
canUndo: boolean
|
@@ -207,6 +211,8 @@ const MemoizedHeader = memo(function MemoizedHeader({
|
|
207
211
|
onHandlerClick,
|
208
212
|
onFindReplace,
|
209
213
|
onEditAll,
|
214
|
+
onTimingOffset,
|
215
|
+
timingOffsetMs,
|
210
216
|
onUndo,
|
211
217
|
onRedo,
|
212
218
|
canUndo,
|
@@ -228,6 +234,8 @@ const MemoizedHeader = memo(function MemoizedHeader({
|
|
228
234
|
onHandlerClick={onHandlerClick}
|
229
235
|
onFindReplace={onFindReplace}
|
230
236
|
onEditAll={onEditAll}
|
237
|
+
onTimingOffset={onTimingOffset}
|
238
|
+
timingOffsetMs={timingOffsetMs}
|
231
239
|
onUndo={onUndo}
|
232
240
|
onRedo={onRedo}
|
233
241
|
canUndo={canUndo}
|
@@ -270,6 +278,8 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
270
278
|
const [isAddLyricsModalOpen, setIsAddLyricsModalOpen] = useState(false)
|
271
279
|
const [isAnyModalOpen, setIsAnyModalOpen] = useState(false)
|
272
280
|
const [isFindReplaceModalOpen, setIsFindReplaceModalOpen] = useState(false)
|
281
|
+
const [isTimingOffsetModalOpen, setIsTimingOffsetModalOpen] = useState(false)
|
282
|
+
const [timingOffsetMs, setTimingOffsetMs] = useState(0)
|
273
283
|
const theme = useTheme()
|
274
284
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
275
285
|
|
@@ -387,10 +397,11 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
387
397
|
isReviewModalOpen ||
|
388
398
|
isAddLyricsModalOpen ||
|
389
399
|
isFindReplaceModalOpen ||
|
390
|
-
isEditAllModalOpen
|
400
|
+
isEditAllModalOpen ||
|
401
|
+
isTimingOffsetModalOpen
|
391
402
|
)
|
392
403
|
setIsAnyModalOpen(modalOpen)
|
393
|
-
}, [modalContent, editModalSegment, isReviewModalOpen, isAddLyricsModalOpen, isFindReplaceModalOpen, isEditAllModalOpen])
|
404
|
+
}, [modalContent, editModalSegment, isReviewModalOpen, isAddLyricsModalOpen, isFindReplaceModalOpen, isEditAllModalOpen, isTimingOffsetModalOpen])
|
394
405
|
|
395
406
|
// Calculate effective mode based on modifier key states
|
396
407
|
const effectiveMode = isCtrlPressed ? 'delete_word' : (isShiftPressed ? 'highlight' : interactionMode)
|
@@ -595,8 +606,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
595
606
|
}, [data, updateDataWithHistory])
|
596
607
|
|
597
608
|
const handleFinishReview = useCallback(() => {
|
609
|
+
console.log(`[TIMING] handleFinishReview - Current timing offset: ${timingOffsetMs}ms`);
|
598
610
|
setIsReviewModalOpen(true)
|
599
|
-
}, [])
|
611
|
+
}, [timingOffsetMs])
|
600
612
|
|
601
613
|
const handleSubmitToServer = useCallback(async () => {
|
602
614
|
if (!apiClient) return
|
@@ -605,7 +617,28 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
605
617
|
if (debugLog) {
|
606
618
|
console.log('Submitting changes to server')
|
607
619
|
}
|
608
|
-
|
620
|
+
|
621
|
+
// Debug logging for timing offset
|
622
|
+
console.log(`[TIMING] handleSubmitToServer - Current timing offset: ${timingOffsetMs}ms`);
|
623
|
+
|
624
|
+
// Apply timing offset to the data before submission if needed
|
625
|
+
const dataToSubmit = timingOffsetMs !== 0
|
626
|
+
? applyOffsetToCorrectionData(data, timingOffsetMs)
|
627
|
+
: data
|
628
|
+
|
629
|
+
// Log some example timestamps after potential offset application
|
630
|
+
if (dataToSubmit.corrected_segments.length > 0) {
|
631
|
+
const firstSegment = dataToSubmit.corrected_segments[0];
|
632
|
+
console.log(`[TIMING] Submitting data - First segment id: ${firstSegment.id}`);
|
633
|
+
console.log(`[TIMING] - start_time: ${firstSegment.start_time}, end_time: ${firstSegment.end_time}`);
|
634
|
+
|
635
|
+
if (firstSegment.words.length > 0) {
|
636
|
+
const firstWord = firstSegment.words[0];
|
637
|
+
console.log(`[TIMING] - first word "${firstWord.text}" time: ${firstWord.start_time} -> ${firstWord.end_time}`);
|
638
|
+
}
|
639
|
+
}
|
640
|
+
|
641
|
+
await apiClient.submitCorrections(dataToSubmit)
|
609
642
|
|
610
643
|
setIsReviewComplete(true)
|
611
644
|
setIsReviewModalOpen(false)
|
@@ -616,14 +649,19 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
616
649
|
console.error('Failed to submit corrections:', error)
|
617
650
|
alert('Failed to submit corrections. Please try again.')
|
618
651
|
}
|
619
|
-
}, [apiClient, data])
|
652
|
+
}, [apiClient, data, timingOffsetMs])
|
620
653
|
|
621
654
|
// Update play segment handler
|
622
655
|
const handlePlaySegment = useCallback((startTime: number) => {
|
623
656
|
if (window.seekAndPlayAudio) {
|
624
|
-
|
657
|
+
// Apply the timing offset to the start time
|
658
|
+
const adjustedStartTime = timingOffsetMs !== 0
|
659
|
+
? startTime + (timingOffsetMs / 1000)
|
660
|
+
: startTime;
|
661
|
+
|
662
|
+
window.seekAndPlayAudio(adjustedStartTime)
|
625
663
|
}
|
626
|
-
}, [])
|
664
|
+
}, [timingOffsetMs])
|
627
665
|
|
628
666
|
const handleResetCorrections = useCallback(() => {
|
629
667
|
if (window.confirm('Are you sure you want to reset all corrections? This cannot be undone.')) {
|
@@ -993,7 +1031,41 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
993
1031
|
|
994
1032
|
// Determine if any modal is open to disable highlighting
|
995
1033
|
const isAnyModalOpenMemo = useMemo(() => isAnyModalOpen, [isAnyModalOpen]);
|
1034
|
+
|
1035
|
+
// For the TranscriptionView, we need to apply the timing offset when displaying
|
1036
|
+
const displayData = useMemo(() => {
|
1037
|
+
return timingOffsetMs !== 0
|
1038
|
+
? applyOffsetToCorrectionData(data, timingOffsetMs)
|
1039
|
+
: data;
|
1040
|
+
}, [data, timingOffsetMs]);
|
1041
|
+
|
1042
|
+
// Handler for opening the timing offset modal
|
1043
|
+
const handleOpenTimingOffsetModal = useCallback(() => {
|
1044
|
+
setIsTimingOffsetModalOpen(true)
|
1045
|
+
}, [])
|
996
1046
|
|
1047
|
+
// Handler for applying the timing offset
|
1048
|
+
const handleApplyTimingOffset = useCallback((offsetMs: number) => {
|
1049
|
+
// Only update if the offset has changed
|
1050
|
+
if (offsetMs !== timingOffsetMs) {
|
1051
|
+
console.log(`[TIMING] handleApplyTimingOffset: Changing offset from ${timingOffsetMs}ms to ${offsetMs}ms`);
|
1052
|
+
setTimingOffsetMs(offsetMs)
|
1053
|
+
|
1054
|
+
// If we're applying an offset, we don't need to update history
|
1055
|
+
// since we're not modifying the original data
|
1056
|
+
if (debugLog) {
|
1057
|
+
console.log(`[DEBUG] handleApplyTimingOffset: Setting offset to ${offsetMs}ms`);
|
1058
|
+
}
|
1059
|
+
} else {
|
1060
|
+
console.log(`[TIMING] handleApplyTimingOffset: Offset unchanged at ${offsetMs}ms`);
|
1061
|
+
}
|
1062
|
+
}, [timingOffsetMs])
|
1063
|
+
|
1064
|
+
// Add logging for timing offset changes
|
1065
|
+
useEffect(() => {
|
1066
|
+
console.log(`[TIMING] timingOffsetMs changed to: ${timingOffsetMs}ms`);
|
1067
|
+
}, [timingOffsetMs]);
|
1068
|
+
|
997
1069
|
return (
|
998
1070
|
<Box sx={{
|
999
1071
|
p: 1,
|
@@ -1014,9 +1086,10 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
1014
1086
|
onHandlerToggle={handleHandlerToggle}
|
1015
1087
|
isUpdatingHandlers={isUpdatingHandlers}
|
1016
1088
|
onHandlerClick={handleHandlerClick}
|
1017
|
-
onAddLyrics={() => setIsAddLyricsModalOpen(true)}
|
1018
1089
|
onFindReplace={() => setIsFindReplaceModalOpen(true)}
|
1019
1090
|
onEditAll={handleEditAll}
|
1091
|
+
onTimingOffset={handleOpenTimingOffsetModal}
|
1092
|
+
timingOffsetMs={timingOffsetMs}
|
1020
1093
|
onUndo={handleUndo}
|
1021
1094
|
onRedo={handleRedo}
|
1022
1095
|
canUndo={canUndo}
|
@@ -1026,7 +1099,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
1026
1099
|
<Grid container direction={isMobile ? 'column' : 'row'}>
|
1027
1100
|
<Grid item xs={12} md={6}>
|
1028
1101
|
<MemoizedTranscriptionView
|
1029
|
-
data={
|
1102
|
+
data={displayData}
|
1030
1103
|
mode={effectiveMode}
|
1031
1104
|
onElementClick={setModalContent}
|
1032
1105
|
onWordClick={handleWordClick}
|
@@ -1098,14 +1171,26 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
1098
1171
|
setOriginalTranscribedGlobalSegment(null)
|
1099
1172
|
handleSetModalSpacebarHandler(undefined)
|
1100
1173
|
}}
|
1101
|
-
segment={globalEditSegment
|
1174
|
+
segment={globalEditSegment ?
|
1175
|
+
timingOffsetMs !== 0 ?
|
1176
|
+
applyOffsetToSegment(globalEditSegment, timingOffsetMs) :
|
1177
|
+
globalEditSegment :
|
1178
|
+
null}
|
1102
1179
|
segmentIndex={null}
|
1103
|
-
originalSegment={originalGlobalSegment
|
1180
|
+
originalSegment={originalGlobalSegment ?
|
1181
|
+
timingOffsetMs !== 0 ?
|
1182
|
+
applyOffsetToSegment(originalGlobalSegment, timingOffsetMs) :
|
1183
|
+
originalGlobalSegment :
|
1184
|
+
null}
|
1104
1185
|
onSave={handleSaveGlobalEdit}
|
1105
1186
|
onPlaySegment={handlePlaySegment}
|
1106
1187
|
currentTime={currentAudioTime}
|
1107
1188
|
setModalSpacebarHandler={handleSetModalSpacebarHandler}
|
1108
|
-
originalTranscribedSegment={originalTranscribedGlobalSegment
|
1189
|
+
originalTranscribedSegment={originalTranscribedGlobalSegment ?
|
1190
|
+
timingOffsetMs !== 0 ?
|
1191
|
+
applyOffsetToSegment(originalTranscribedGlobalSegment, timingOffsetMs) :
|
1192
|
+
originalTranscribedGlobalSegment :
|
1193
|
+
null}
|
1109
1194
|
isGlobal={true}
|
1110
1195
|
isLoading={isLoadingGlobalEdit}
|
1111
1196
|
/>
|
@@ -1116,9 +1201,17 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
1116
1201
|
setEditModalSegment(null)
|
1117
1202
|
handleSetModalSpacebarHandler(undefined)
|
1118
1203
|
}}
|
1119
|
-
segment={editModalSegment?.segment
|
1204
|
+
segment={editModalSegment?.segment ?
|
1205
|
+
timingOffsetMs !== 0 ?
|
1206
|
+
applyOffsetToSegment(editModalSegment.segment, timingOffsetMs) :
|
1207
|
+
editModalSegment.segment :
|
1208
|
+
null}
|
1120
1209
|
segmentIndex={editModalSegment?.index ?? null}
|
1121
|
-
originalSegment={editModalSegment?.originalSegment
|
1210
|
+
originalSegment={editModalSegment?.originalSegment ?
|
1211
|
+
timingOffsetMs !== 0 ?
|
1212
|
+
applyOffsetToSegment(editModalSegment.originalSegment, timingOffsetMs) :
|
1213
|
+
editModalSegment.originalSegment :
|
1214
|
+
null}
|
1122
1215
|
onSave={handleUpdateSegment}
|
1123
1216
|
onDelete={handleDeleteSegment}
|
1124
1217
|
onAddSegment={handleAddSegment}
|
@@ -1128,10 +1221,16 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
1128
1221
|
currentTime={currentAudioTime}
|
1129
1222
|
setModalSpacebarHandler={handleSetModalSpacebarHandler}
|
1130
1223
|
originalTranscribedSegment={
|
1131
|
-
editModalSegment?.segment && editModalSegment?.index !== null
|
1132
|
-
?
|
1133
|
-
|
1134
|
-
|
1224
|
+
editModalSegment?.segment && editModalSegment?.index !== null && originalData.original_segments
|
1225
|
+
? (() => {
|
1226
|
+
const origSegment = originalData.original_segments.find(
|
1227
|
+
(s: LyricsSegment) => s.id === editModalSegment.segment.id
|
1228
|
+
) || null;
|
1229
|
+
|
1230
|
+
return origSegment && timingOffsetMs !== 0
|
1231
|
+
? applyOffsetToSegment(origSegment, timingOffsetMs)
|
1232
|
+
: origSegment;
|
1233
|
+
})()
|
1135
1234
|
: null
|
1136
1235
|
}
|
1137
1236
|
/>
|
@@ -1144,6 +1243,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
1144
1243
|
onSubmit={handleSubmitToServer}
|
1145
1244
|
apiClient={apiClient}
|
1146
1245
|
setModalSpacebarHandler={handleSetModalSpacebarHandler}
|
1246
|
+
timingOffsetMs={timingOffsetMs}
|
1147
1247
|
/>
|
1148
1248
|
|
1149
1249
|
<AddLyricsModal
|
@@ -1159,6 +1259,13 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
1159
1259
|
onReplace={handleFindReplace}
|
1160
1260
|
data={data}
|
1161
1261
|
/>
|
1262
|
+
|
1263
|
+
<TimingOffsetModal
|
1264
|
+
open={isTimingOffsetModalOpen}
|
1265
|
+
onClose={() => setIsTimingOffsetModalOpen(false)}
|
1266
|
+
currentOffset={timingOffsetMs}
|
1267
|
+
onApply={handleApplyTimingOffset}
|
1268
|
+
/>
|
1162
1269
|
</Box>
|
1163
1270
|
)
|
1164
1271
|
}
|
@@ -2,19 +2,22 @@ import { Box, Typography, CircularProgress, Alert, Button } from '@mui/material'
|
|
2
2
|
import { useState, useEffect } from 'react'
|
3
3
|
import { ApiClient } from '../api'
|
4
4
|
import { CorrectionData } from '../types'
|
5
|
+
import { applyOffsetToCorrectionData } from './shared/utils/timingUtils'
|
5
6
|
|
6
7
|
interface PreviewVideoSectionProps {
|
7
8
|
apiClient: ApiClient | null
|
8
9
|
isModalOpen: boolean
|
9
10
|
updatedData: CorrectionData
|
10
11
|
videoRef?: React.RefObject<HTMLVideoElement>
|
12
|
+
timingOffsetMs?: number
|
11
13
|
}
|
12
14
|
|
13
15
|
export default function PreviewVideoSection({
|
14
16
|
apiClient,
|
15
17
|
isModalOpen,
|
16
18
|
updatedData,
|
17
|
-
videoRef
|
19
|
+
videoRef,
|
20
|
+
timingOffsetMs = 0
|
18
21
|
}: PreviewVideoSectionProps) {
|
19
22
|
const [previewState, setPreviewState] = useState<{
|
20
23
|
status: 'loading' | 'ready' | 'error';
|
@@ -28,7 +31,27 @@ export default function PreviewVideoSection({
|
|
28
31
|
const generatePreview = async () => {
|
29
32
|
setPreviewState({ status: 'loading' });
|
30
33
|
try {
|
31
|
-
|
34
|
+
// Debug logging for timing offset
|
35
|
+
console.log(`[TIMING] PreviewVideoSection - Current timing offset: ${timingOffsetMs}ms`);
|
36
|
+
|
37
|
+
// Apply timing offset if needed
|
38
|
+
const dataToPreview = timingOffsetMs !== 0
|
39
|
+
? applyOffsetToCorrectionData(updatedData, timingOffsetMs)
|
40
|
+
: updatedData;
|
41
|
+
|
42
|
+
// Log some example timestamps after potential offset application
|
43
|
+
if (dataToPreview.corrected_segments.length > 0) {
|
44
|
+
const firstSegment = dataToPreview.corrected_segments[0];
|
45
|
+
console.log(`[TIMING] Preview - First segment id: ${firstSegment.id}`);
|
46
|
+
console.log(`[TIMING] - start_time: ${firstSegment.start_time}, end_time: ${firstSegment.end_time}`);
|
47
|
+
|
48
|
+
if (firstSegment.words.length > 0) {
|
49
|
+
const firstWord = firstSegment.words[0];
|
50
|
+
console.log(`[TIMING] - first word "${firstWord.text}" time: ${firstWord.start_time} -> ${firstWord.end_time}`);
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
const response = await apiClient.generatePreviewVideo(dataToPreview);
|
32
55
|
|
33
56
|
if (response.status === 'error') {
|
34
57
|
setPreviewState({
|
@@ -61,7 +84,7 @@ export default function PreviewVideoSection({
|
|
61
84
|
|
62
85
|
generatePreview();
|
63
86
|
}
|
64
|
-
}, [isModalOpen, apiClient, updatedData]);
|
87
|
+
}, [isModalOpen, apiClient, updatedData, timingOffsetMs]);
|
65
88
|
|
66
89
|
if (!apiClient) return null;
|
67
90
|
|
@@ -22,6 +22,7 @@ interface ReviewChangesModalProps {
|
|
22
22
|
onSubmit: () => void
|
23
23
|
apiClient: ApiClient | null
|
24
24
|
setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
|
25
|
+
timingOffsetMs?: number
|
25
26
|
}
|
26
27
|
|
27
28
|
interface DiffResult {
|
@@ -69,7 +70,8 @@ export default function ReviewChangesModal({
|
|
69
70
|
updatedData,
|
70
71
|
onSubmit,
|
71
72
|
apiClient,
|
72
|
-
setModalSpacebarHandler
|
73
|
+
setModalSpacebarHandler,
|
74
|
+
timingOffsetMs = 0
|
73
75
|
}: ReviewChangesModalProps) {
|
74
76
|
// Add ref to video element
|
75
77
|
const videoRef = useRef<HTMLVideoElement>(null)
|
@@ -107,6 +109,13 @@ export default function ReviewChangesModal({
|
|
107
109
|
}
|
108
110
|
}, [open, setModalSpacebarHandler])
|
109
111
|
|
112
|
+
// Debug logging for timing offset
|
113
|
+
useEffect(() => {
|
114
|
+
if (open) {
|
115
|
+
console.log(`[TIMING] ReviewChangesModal opened - timingOffsetMs: ${timingOffsetMs}ms`);
|
116
|
+
}
|
117
|
+
}, [open, timingOffsetMs]);
|
118
|
+
|
110
119
|
const differences = useMemo(() => {
|
111
120
|
const diffs: DiffResult[] = []
|
112
121
|
|
@@ -292,9 +301,15 @@ export default function ReviewChangesModal({
|
|
292
301
|
isModalOpen={open}
|
293
302
|
updatedData={updatedData}
|
294
303
|
videoRef={videoRef} // Pass the ref to PreviewVideoSection
|
304
|
+
timingOffsetMs={timingOffsetMs}
|
295
305
|
/>
|
296
306
|
|
297
307
|
<Box sx={{ p: 2, mt: 0 }}>
|
308
|
+
{timingOffsetMs !== 0 && (
|
309
|
+
<Typography variant="body2" fontWeight="bold" sx={{ mt: 1 }}>
|
310
|
+
Global Timing Offset applied to all words: {timingOffsetMs > 0 ? '+' : ''}{timingOffsetMs}ms
|
311
|
+
</Typography>
|
312
|
+
)}
|
298
313
|
{differences.length === 0 ? (
|
299
314
|
<Box>
|
300
315
|
<Typography color="text.secondary">
|
@@ -0,0 +1,131 @@
|
|
1
|
+
import {
|
2
|
+
Dialog,
|
3
|
+
DialogTitle,
|
4
|
+
DialogContent,
|
5
|
+
DialogActions,
|
6
|
+
Button,
|
7
|
+
TextField,
|
8
|
+
Box,
|
9
|
+
Typography,
|
10
|
+
ButtonGroup,
|
11
|
+
IconButton,
|
12
|
+
} from '@mui/material';
|
13
|
+
import CloseIcon from '@mui/icons-material/Close';
|
14
|
+
import { useState, useEffect } from 'react';
|
15
|
+
|
16
|
+
interface TimingOffsetModalProps {
|
17
|
+
open: boolean;
|
18
|
+
onClose: () => void;
|
19
|
+
currentOffset: number;
|
20
|
+
onApply: (offsetMs: number) => void;
|
21
|
+
}
|
22
|
+
|
23
|
+
export default function TimingOffsetModal({
|
24
|
+
open,
|
25
|
+
onClose,
|
26
|
+
currentOffset,
|
27
|
+
onApply,
|
28
|
+
}: TimingOffsetModalProps) {
|
29
|
+
const [offsetMs, setOffsetMs] = useState(currentOffset);
|
30
|
+
|
31
|
+
// Reset the offset value when the modal opens
|
32
|
+
useEffect(() => {
|
33
|
+
if (open) {
|
34
|
+
setOffsetMs(currentOffset);
|
35
|
+
}
|
36
|
+
}, [open, currentOffset]);
|
37
|
+
|
38
|
+
// Handle preset buttons click
|
39
|
+
const handlePresetClick = (value: number) => {
|
40
|
+
setOffsetMs((prev) => prev + value);
|
41
|
+
};
|
42
|
+
|
43
|
+
// Handle direct input change
|
44
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
45
|
+
const value = e.target.value === '' ? 0 : parseInt(e.target.value, 10);
|
46
|
+
if (!isNaN(value)) {
|
47
|
+
setOffsetMs(value);
|
48
|
+
}
|
49
|
+
};
|
50
|
+
|
51
|
+
// Apply the offset
|
52
|
+
const handleApply = () => {
|
53
|
+
onApply(offsetMs);
|
54
|
+
onClose();
|
55
|
+
};
|
56
|
+
|
57
|
+
return (
|
58
|
+
<Dialog
|
59
|
+
open={open}
|
60
|
+
onClose={onClose}
|
61
|
+
maxWidth="sm"
|
62
|
+
fullWidth
|
63
|
+
PaperProps={{
|
64
|
+
sx: {
|
65
|
+
overflowY: 'visible',
|
66
|
+
}
|
67
|
+
}}
|
68
|
+
>
|
69
|
+
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
70
|
+
Adjust Global Timing Offset
|
71
|
+
<IconButton onClick={onClose} size="small">
|
72
|
+
<CloseIcon />
|
73
|
+
</IconButton>
|
74
|
+
</DialogTitle>
|
75
|
+
<DialogContent>
|
76
|
+
<Box sx={{ mb: 3, mt: 1 }}>
|
77
|
+
<Typography variant="body2" sx={{ mb: 1 }}>
|
78
|
+
Adjust the timing of all words in the transcription. Positive values delay the timing, negative values advance it.
|
79
|
+
</Typography>
|
80
|
+
|
81
|
+
<Typography variant="body2" sx={{ fontStyle: 'italic', mb: 2 }}>
|
82
|
+
Note: This offset is applied globally but doesn't modify the original timestamps.
|
83
|
+
</Typography>
|
84
|
+
|
85
|
+
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
86
|
+
<Typography variant="body1" sx={{ mr: 2 }}>
|
87
|
+
Offset:
|
88
|
+
</Typography>
|
89
|
+
<TextField
|
90
|
+
value={offsetMs}
|
91
|
+
onChange={handleInputChange}
|
92
|
+
type="number"
|
93
|
+
variant="outlined"
|
94
|
+
size="small"
|
95
|
+
InputProps={{
|
96
|
+
endAdornment: <Typography variant="body2" sx={{ ml: 1 }}>ms</Typography>,
|
97
|
+
}}
|
98
|
+
sx={{ width: 120 }}
|
99
|
+
/>
|
100
|
+
</Box>
|
101
|
+
|
102
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
103
|
+
<Typography variant="body2">Quick adjust:</Typography>
|
104
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 1 }}>
|
105
|
+
<ButtonGroup size="small">
|
106
|
+
<Button onClick={() => handlePresetClick(-100)}>-100ms</Button>
|
107
|
+
<Button onClick={() => handlePresetClick(-50)}>-50ms</Button>
|
108
|
+
<Button onClick={() => handlePresetClick(-10)}>-10ms</Button>
|
109
|
+
</ButtonGroup>
|
110
|
+
<ButtonGroup size="small">
|
111
|
+
<Button onClick={() => handlePresetClick(10)}>+10ms</Button>
|
112
|
+
<Button onClick={() => handlePresetClick(50)}>+50ms</Button>
|
113
|
+
<Button onClick={() => handlePresetClick(100)}>+100ms</Button>
|
114
|
+
</ButtonGroup>
|
115
|
+
</Box>
|
116
|
+
</Box>
|
117
|
+
</Box>
|
118
|
+
</DialogContent>
|
119
|
+
<DialogActions>
|
120
|
+
<Button onClick={onClose}>Cancel</Button>
|
121
|
+
<Button
|
122
|
+
onClick={handleApply}
|
123
|
+
variant="contained"
|
124
|
+
color={offsetMs === 0 ? "warning" : "primary"}
|
125
|
+
>
|
126
|
+
{offsetMs === 0 ? "Remove Offset" : "Apply Offset"}
|
127
|
+
</Button>
|
128
|
+
</DialogActions>
|
129
|
+
</Dialog>
|
130
|
+
);
|
131
|
+
}
|
@@ -109,6 +109,14 @@ export const setupKeyboardHandlers = (state: KeyboardState) => {
|
|
109
109
|
})
|
110
110
|
}
|
111
111
|
|
112
|
+
// Ignore keyup events in input and textarea elements
|
113
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
114
|
+
if (debugLog) {
|
115
|
+
console.log(`[${handlerId}] Ignoring keyup in input/textarea`)
|
116
|
+
}
|
117
|
+
return
|
118
|
+
}
|
119
|
+
|
112
120
|
// Always reset the modifier states regardless of the key which was released
|
113
121
|
// to help prevent accidentally getting stuck in a mode or accidentally deleting words
|
114
122
|
resetModifierStates()
|