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.
@@ -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-DSQidWB1.js"></script>
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
- await apiClient.submitCorrections(data)
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
- window.seekAndPlayAudio(startTime)
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={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 ?? null}
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 ?? null}
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
- ? originalData.original_segments.find(
1133
- (s: LyricsSegment) => s.id === editModalSegment.segment.id
1134
- ) || null
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
- const response = await apiClient.generatePreviewVideo(updatedData);
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()