lyrics-transcriber 0.68.0__py3-none-any.whl → 0.69.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.
@@ -14,10 +14,12 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack'
14
14
  import ArrowForwardIcon from '@mui/icons-material/ArrowForward'
15
15
  import AutorenewIcon from '@mui/icons-material/Autorenew'
16
16
  import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline'
17
+ import PlayArrowIcon from '@mui/icons-material/PlayArrow'
18
+ import StopIcon from '@mui/icons-material/Stop'
17
19
  import CenterFocusStrongIcon from '@mui/icons-material/CenterFocusStrong'
18
20
  import TimelineEditor from './TimelineEditor'
19
21
  import { Word } from '../types'
20
- import { useState, useEffect, useCallback, useRef } from 'react'
22
+ import { useState, useEffect, useCallback, useRef, useMemo, memo } from 'react'
21
23
 
22
24
  interface EditTimelineSectionProps {
23
25
  words: Word[]
@@ -32,11 +34,160 @@ interface EditTimelineSectionProps {
32
34
  syncWordIndex: number
33
35
  isSpacebarPressed: boolean
34
36
  onWordUpdate: (index: number, updates: Partial<Word>) => void
37
+ onUnsyncWord?: (index: number) => void
35
38
  onPlaySegment?: (time: number) => void
39
+ onStopAudio?: () => void
36
40
  startManualSync: () => void
41
+ pauseManualSync?: () => void
42
+ resumeManualSync?: () => void
43
+ isPaused?: boolean
37
44
  isGlobal?: boolean
45
+ defaultZoomLevel?: number
46
+ isReplaceAllMode?: boolean
38
47
  }
39
48
 
49
+ // Memoized control buttons to prevent unnecessary re-renders
50
+ const TimelineControls = memo(({
51
+ isGlobal,
52
+ visibleStartTime,
53
+ visibleEndTime,
54
+ startTime,
55
+ endTime,
56
+ zoomLevel,
57
+ autoScrollEnabled,
58
+ currentTime,
59
+ isManualSyncing,
60
+ isReplaceAllMode,
61
+ isPaused,
62
+ onScrollLeft,
63
+ onZoomOut,
64
+ onZoomIn,
65
+ onScrollRight,
66
+ onToggleAutoScroll,
67
+ onJumpToCurrentTime,
68
+ onStartManualSync,
69
+ onPauseResume,
70
+ onStopAudio
71
+ }: {
72
+ isGlobal: boolean
73
+ visibleStartTime: number
74
+ visibleEndTime: number
75
+ startTime: number
76
+ endTime: number
77
+ zoomLevel: number
78
+ autoScrollEnabled: boolean
79
+ currentTime?: number
80
+ isManualSyncing: boolean
81
+ isReplaceAllMode: boolean
82
+ isPaused: boolean
83
+ onScrollLeft: () => void
84
+ onZoomOut: () => void
85
+ onZoomIn: () => void
86
+ onScrollRight: () => void
87
+ onToggleAutoScroll: () => void
88
+ onJumpToCurrentTime: () => void
89
+ onStartManualSync: () => void
90
+ onPauseResume: () => void
91
+ onStopAudio?: () => void
92
+ }) => {
93
+ return (
94
+ <Stack direction="row" spacing={1} alignItems="center">
95
+ {isGlobal && (
96
+ <>
97
+ <Tooltip title="Scroll Left">
98
+ <IconButton
99
+ onClick={onScrollLeft}
100
+ disabled={visibleStartTime <= startTime}
101
+ size="small"
102
+ >
103
+ <ArrowBackIcon />
104
+ </IconButton>
105
+ </Tooltip>
106
+ <Tooltip title="Zoom Out (Show More Time)">
107
+ <IconButton
108
+ onClick={onZoomOut}
109
+ disabled={zoomLevel >= (endTime - startTime) || (isReplaceAllMode && isManualSyncing && !isPaused)}
110
+ size="small"
111
+ >
112
+ <ZoomOutIcon />
113
+ </IconButton>
114
+ </Tooltip>
115
+ <Tooltip title="Zoom In (Show Less Time)">
116
+ <IconButton
117
+ onClick={onZoomIn}
118
+ disabled={zoomLevel <= 2 || (isReplaceAllMode && isManualSyncing && !isPaused)}
119
+ size="small"
120
+ >
121
+ <ZoomInIcon />
122
+ </IconButton>
123
+ </Tooltip>
124
+ <Tooltip title="Scroll Right">
125
+ <IconButton
126
+ onClick={onScrollRight}
127
+ disabled={visibleEndTime >= endTime}
128
+ size="small"
129
+ >
130
+ <ArrowForwardIcon />
131
+ </IconButton>
132
+ </Tooltip>
133
+ <Tooltip
134
+ title={autoScrollEnabled ?
135
+ "Disable Auto-Page Turn During Playback" :
136
+ "Enable Auto-Page Turn During Playback"}
137
+ >
138
+ <IconButton
139
+ onClick={onToggleAutoScroll}
140
+ color={autoScrollEnabled ? "primary" : "default"}
141
+ size="small"
142
+ >
143
+ {autoScrollEnabled ? <AutorenewIcon /> : <PauseCircleOutlineIcon />}
144
+ </IconButton>
145
+ </Tooltip>
146
+ <Tooltip title="Jump to Current Playback Position">
147
+ <IconButton
148
+ onClick={onJumpToCurrentTime}
149
+ disabled={!currentTime}
150
+ size="small"
151
+ >
152
+ <CenterFocusStrongIcon />
153
+ </IconButton>
154
+ </Tooltip>
155
+ </>
156
+ )}
157
+ {isReplaceAllMode && onStopAudio && (
158
+ <Button
159
+ variant="outlined"
160
+ onClick={onStopAudio}
161
+ startIcon={<StopIcon />}
162
+ color="error"
163
+ size="small"
164
+ >
165
+ Stop Audio
166
+ </Button>
167
+ )}
168
+ <Button
169
+ variant={isManualSyncing ? "outlined" : "contained"}
170
+ onClick={onStartManualSync}
171
+ startIcon={isManualSyncing ? <CancelIcon /> : <PlayCircleOutlineIcon />}
172
+ color={isManualSyncing ? "error" : "primary"}
173
+ >
174
+ {isManualSyncing ? "Cancel Sync" : "Manual Sync"}
175
+ </Button>
176
+ {isManualSyncing && isReplaceAllMode && (
177
+ <Button
178
+ variant="outlined"
179
+ onClick={onPauseResume}
180
+ startIcon={isPaused ? <PlayArrowIcon /> : <PauseCircleOutlineIcon />}
181
+ color={isPaused ? "success" : "warning"}
182
+ size="small"
183
+ >
184
+ {isPaused ? "Resume" : "Pause"}
185
+ </Button>
186
+ )}
187
+ </Stack>
188
+ )
189
+ })
190
+
40
191
  export default function EditTimelineSection({
41
192
  words,
42
193
  startTime,
@@ -50,17 +201,38 @@ export default function EditTimelineSection({
50
201
  syncWordIndex,
51
202
  isSpacebarPressed,
52
203
  onWordUpdate,
204
+ onUnsyncWord,
53
205
  onPlaySegment,
206
+ onStopAudio,
54
207
  startManualSync,
55
- isGlobal = false
208
+ pauseManualSync,
209
+ resumeManualSync,
210
+ isPaused = false,
211
+ isGlobal = false,
212
+ defaultZoomLevel = 10,
213
+ isReplaceAllMode = false
56
214
  }: EditTimelineSectionProps) {
57
- // Add state for zoom level
58
- const [zoomLevel, setZoomLevel] = useState(10) // Default 10 seconds visible
215
+ // Add state for zoom level - use larger default for Replace All mode
216
+ const [zoomLevel, setZoomLevel] = useState(defaultZoomLevel)
59
217
  const [visibleStartTime, setVisibleStartTime] = useState(startTime)
60
218
  const [visibleEndTime, setVisibleEndTime] = useState(Math.min(startTime + zoomLevel, endTime))
61
219
  const [autoScrollEnabled, setAutoScrollEnabled] = useState(true) // Default to enabled
62
220
  const timelineRef = useRef<HTMLDivElement>(null)
63
221
 
222
+ // Memoize the effective time range to prevent recalculation
223
+ const effectiveTimeRange = useMemo(() => ({
224
+ start: isGlobal ? visibleStartTime : startTime,
225
+ end: isGlobal ? visibleEndTime : endTime
226
+ }), [isGlobal, visibleStartTime, visibleEndTime, startTime, endTime])
227
+
228
+ // Auto-enable auto-scroll when manual sync starts or resumes
229
+ useEffect(() => {
230
+ if (isManualSyncing && !isPaused) {
231
+ console.log('EditTimelineSection - Auto-enabling auto-scroll for manual sync')
232
+ setAutoScrollEnabled(true)
233
+ }
234
+ }, [isManualSyncing, isPaused])
235
+
64
236
  // Initial setup of visible time range
65
237
  useEffect(() => {
66
238
  if (isGlobal) {
@@ -74,11 +246,20 @@ export default function EditTimelineSection({
74
246
  }
75
247
  }, [startTime, endTime, zoomLevel, isGlobal])
76
248
 
77
- // Handle playback scrolling with "page turning" approach
249
+ // Throttled auto-scroll to reduce frequent updates during playback
250
+ const lastScrollUpdateRef = useRef<number>(0)
251
+ const SCROLL_THROTTLE_MS = 100 // Only update scroll position every 100ms
252
+
253
+ // Handle playback scrolling with "page turning" approach - throttled for performance
78
254
  useEffect(() => {
79
255
  // Skip if not in global mode, no current time, or auto-scroll is disabled
80
256
  if (!isGlobal || !currentTime || !autoScrollEnabled) return
81
257
 
258
+ // Throttle scroll updates for performance
259
+ const now = Date.now()
260
+ if (now - lastScrollUpdateRef.current < SCROLL_THROTTLE_MS) return
261
+ lastScrollUpdateRef.current = now
262
+
82
263
  // Only scroll when current time is outside or near the edge of the visible window
83
264
  if (currentTime < visibleStartTime) {
84
265
  // If current time is before visible window, jump to show it at the start
@@ -122,12 +303,25 @@ export default function EditTimelineSection({
122
303
  }
123
304
  }, [zoomLevel, startTime, endTime, isGlobal, visibleStartTime])
124
305
 
125
- // Toggle auto-scroll
126
- const toggleAutoScroll = () => {
306
+ // Memoized event handlers to prevent unnecessary re-renders
307
+ const handleZoomIn = useCallback(() => {
308
+ if (isReplaceAllMode && isManualSyncing && !isPaused) return // Prevent zoom changes during active sync (but allow when paused)
309
+ if (zoomLevel > 2) { // Minimum zoom level of 2 seconds
310
+ setZoomLevel(zoomLevel - 2)
311
+ }
312
+ }, [isReplaceAllMode, isManualSyncing, isPaused, zoomLevel])
313
+
314
+ const handleZoomOut = useCallback(() => {
315
+ if (isReplaceAllMode && isManualSyncing && !isPaused) return // Prevent zoom changes during active sync (but allow when paused)
316
+ if (zoomLevel < (endTime - startTime)) { // Maximum zoom is the full range
317
+ setZoomLevel(zoomLevel + 2)
318
+ }
319
+ }, [isReplaceAllMode, isManualSyncing, isPaused, zoomLevel, endTime, startTime])
320
+
321
+ const toggleAutoScroll = useCallback(() => {
127
322
  setAutoScrollEnabled(!autoScrollEnabled)
128
- }
323
+ }, [autoScrollEnabled])
129
324
 
130
- // Jump to current playback position
131
325
  const jumpToCurrentTime = useCallback(() => {
132
326
  if (!isGlobal || !currentTime) return
133
327
 
@@ -145,45 +339,7 @@ export default function EditTimelineSection({
145
339
  setVisibleEndTime(newEnd)
146
340
  }, [currentTime, zoomLevel, startTime, endTime, isGlobal])
147
341
 
148
- // Add keyboard shortcut for toggling auto-scroll
149
- useEffect(() => {
150
- const handleKeyDown = (e: KeyboardEvent) => {
151
- if (isGlobal) {
152
- // Alt+A to toggle auto-scroll
153
- if (e.altKey && e.key === 'a') {
154
- e.preventDefault()
155
- toggleAutoScroll()
156
- }
157
-
158
- // Alt+J to jump to current time
159
- if (e.altKey && e.key === 'j') {
160
- e.preventDefault()
161
- jumpToCurrentTime()
162
- }
163
- }
164
- }
165
-
166
- window.addEventListener('keydown', handleKeyDown)
167
- return () => {
168
- window.removeEventListener('keydown', handleKeyDown)
169
- }
170
- }, [isGlobal, toggleAutoScroll, jumpToCurrentTime])
171
-
172
- // Handle zoom in
173
- const handleZoomIn = () => {
174
- if (zoomLevel > 2) { // Minimum zoom level of 2 seconds
175
- setZoomLevel(zoomLevel - 2)
176
- }
177
- }
178
-
179
- // Handle zoom out
180
- const handleZoomOut = () => {
181
- if (zoomLevel < (endTime - startTime)) { // Maximum zoom is the full range
182
- setZoomLevel(zoomLevel + 2)
183
- }
184
- }
185
-
186
- // Handle horizontal scrolling
342
+ // Handle horizontal scrolling - throttled for performance
187
343
  const handleScroll = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
188
344
  if (isGlobal && event.deltaX !== 0) {
189
345
  event.preventDefault()
@@ -214,8 +370,7 @@ export default function EditTimelineSection({
214
370
  }
215
371
  }, [isGlobal, visibleStartTime, visibleEndTime, startTime, endTime, zoomLevel])
216
372
 
217
- // Handle scroll left button
218
- const handleScrollLeft = () => {
373
+ const handleScrollLeft = useCallback(() => {
219
374
  if (!isGlobal) return
220
375
 
221
376
  // Disable auto-scroll when user manually scrolls
@@ -228,10 +383,9 @@ export default function EditTimelineSection({
228
383
 
229
384
  setVisibleStartTime(newStart)
230
385
  setVisibleEndTime(newEnd)
231
- }
386
+ }, [isGlobal, zoomLevel, startTime, visibleStartTime])
232
387
 
233
- // Handle scroll right button
234
- const handleScrollRight = () => {
388
+ const handleScrollRight = useCallback(() => {
235
389
  if (!isGlobal) return
236
390
 
237
391
  // Disable auto-scroll when user manually scrolls
@@ -252,11 +406,28 @@ export default function EditTimelineSection({
252
406
  }
253
407
 
254
408
  setVisibleStartTime(newStart)
255
- }
409
+ }, [isGlobal, zoomLevel, endTime, visibleEndTime, startTime])
256
410
 
257
- // Get the effective time range to display
258
- const effectiveStartTime = isGlobal ? visibleStartTime : startTime
259
- const effectiveEndTime = isGlobal ? visibleEndTime : endTime
411
+ const handlePauseResume = useCallback(() => {
412
+ if (isPaused && resumeManualSync) {
413
+ resumeManualSync()
414
+ } else if (!isPaused && pauseManualSync) {
415
+ pauseManualSync()
416
+ }
417
+ }, [isPaused, resumeManualSync, pauseManualSync])
418
+
419
+ // Memoize current word info to prevent recalculation
420
+ const currentWordInfo = useMemo(() => {
421
+ if (!isManualSyncing || syncWordIndex < 0 || syncWordIndex >= words.length) {
422
+ return null
423
+ }
424
+
425
+ return {
426
+ index: syncWordIndex + 1,
427
+ total: words.length,
428
+ text: words[syncWordIndex]?.text || ''
429
+ }
430
+ }, [isManualSyncing, syncWordIndex, words])
260
431
 
261
432
  return (
262
433
  <>
@@ -267,11 +438,12 @@ export default function EditTimelineSection({
267
438
  >
268
439
  <TimelineEditor
269
440
  words={words}
270
- startTime={effectiveStartTime}
271
- endTime={effectiveEndTime}
441
+ startTime={effectiveTimeRange.start}
442
+ endTime={effectiveTimeRange.end}
272
443
  onWordUpdate={onWordUpdate}
273
444
  currentTime={currentTime}
274
445
  onPlaySegment={onPlaySegment}
446
+ onUnsyncWord={onUnsyncWord}
275
447
  />
276
448
  </Box>
277
449
 
@@ -282,82 +454,33 @@ export default function EditTimelineSection({
282
454
  Current Time Range: {currentStartTime?.toFixed(2) ?? 'N/A'} - {currentEndTime?.toFixed(2) ?? 'N/A'}
283
455
  </Typography>
284
456
 
285
- <Stack direction="row" spacing={1} alignItems="center">
286
- {isGlobal && (
287
- <>
288
- <Tooltip title="Scroll Left">
289
- <IconButton
290
- onClick={handleScrollLeft}
291
- disabled={visibleStartTime <= startTime}
292
- size="small"
293
- >
294
- <ArrowBackIcon />
295
- </IconButton>
296
- </Tooltip>
297
- <Tooltip title="Zoom Out (Show More Time)">
298
- <IconButton
299
- onClick={handleZoomOut}
300
- disabled={zoomLevel >= (endTime - startTime)}
301
- size="small"
302
- >
303
- <ZoomOutIcon />
304
- </IconButton>
305
- </Tooltip>
306
- <Tooltip title="Zoom In (Show Less Time)">
307
- <IconButton
308
- onClick={handleZoomIn}
309
- disabled={zoomLevel <= 2}
310
- size="small"
311
- >
312
- <ZoomInIcon />
313
- </IconButton>
314
- </Tooltip>
315
- <Tooltip title="Scroll Right">
316
- <IconButton
317
- onClick={handleScrollRight}
318
- disabled={visibleEndTime >= endTime}
319
- size="small"
320
- >
321
- <ArrowForwardIcon />
322
- </IconButton>
323
- </Tooltip>
324
- <Tooltip
325
- title={autoScrollEnabled ?
326
- "Disable Auto-Page Turn During Playback (Alt+A)" :
327
- "Enable Auto-Page Turn During Playback (Alt+A)"}
328
- >
329
- <IconButton
330
- onClick={toggleAutoScroll}
331
- color={autoScrollEnabled ? "primary" : "default"}
332
- size="small"
333
- >
334
- {autoScrollEnabled ? <AutorenewIcon /> : <PauseCircleOutlineIcon />}
335
- </IconButton>
336
- </Tooltip>
337
- <Tooltip title="Jump to Current Playback Position (Alt+J)">
338
- <IconButton
339
- onClick={jumpToCurrentTime}
340
- disabled={!currentTime}
341
- size="small"
342
- >
343
- <CenterFocusStrongIcon />
344
- </IconButton>
345
- </Tooltip>
346
- </>
347
- )}
348
- <Button
349
- variant={isManualSyncing ? "outlined" : "contained"}
350
- onClick={startManualSync}
351
- disabled={!onPlaySegment}
352
- startIcon={isManualSyncing ? <CancelIcon /> : <PlayCircleOutlineIcon />}
353
- color={isManualSyncing ? "error" : "primary"}
354
- >
355
- {isManualSyncing ? "Cancel Sync" : "Manual Sync"}
356
- </Button>
357
- {isManualSyncing && (
457
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
458
+ <TimelineControls
459
+ isGlobal={isGlobal}
460
+ visibleStartTime={visibleStartTime}
461
+ visibleEndTime={visibleEndTime}
462
+ startTime={startTime}
463
+ endTime={endTime}
464
+ zoomLevel={zoomLevel}
465
+ autoScrollEnabled={autoScrollEnabled}
466
+ currentTime={currentTime}
467
+ isManualSyncing={isManualSyncing}
468
+ isReplaceAllMode={isReplaceAllMode}
469
+ isPaused={isPaused}
470
+ onScrollLeft={handleScrollLeft}
471
+ onZoomOut={handleZoomOut}
472
+ onZoomIn={handleZoomIn}
473
+ onScrollRight={handleScrollRight}
474
+ onToggleAutoScroll={toggleAutoScroll}
475
+ onJumpToCurrentTime={jumpToCurrentTime}
476
+ onStartManualSync={startManualSync}
477
+ onPauseResume={handlePauseResume}
478
+ onStopAudio={onStopAudio}
479
+ />
480
+ {currentWordInfo && (
358
481
  <Box>
359
482
  <Typography variant="body2">
360
- Word {syncWordIndex + 1} of {words.length}: <strong>{words[syncWordIndex]?.text || ''}</strong>
483
+ Word {currentWordInfo.index} of {currentWordInfo.total}: <strong>{currentWordInfo.text}</strong>
361
484
  </Typography>
362
485
  <Typography variant="caption" color="text.secondary">
363
486
  {isSpacebarPressed ?
@@ -366,7 +489,7 @@ export default function EditTimelineSection({
366
489
  </Typography>
367
490
  </Box>
368
491
  )}
369
- </Stack>
492
+ </Box>
370
493
  </Box>
371
494
  </>
372
495
  )