lyrics-transcriber 0.44.0__py3-none-any.whl → 0.45.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.
Files changed (22) hide show
  1. lyrics_transcriber/frontend/dist/assets/{index-DVoI6Z16.js → index-ZCT0s9MG.js} +2635 -1967
  2. lyrics_transcriber/frontend/dist/assets/index-ZCT0s9MG.js.map +1 -0
  3. lyrics_transcriber/frontend/dist/index.html +1 -1
  4. lyrics_transcriber/frontend/src/App.tsx +1 -1
  5. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
  6. lyrics_transcriber/frontend/src/components/EditModal.tsx +376 -303
  7. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +373 -0
  8. lyrics_transcriber/frontend/src/components/EditWordList.tsx +308 -0
  9. lyrics_transcriber/frontend/src/components/Header.tsx +7 -7
  10. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +458 -62
  11. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +7 -7
  12. lyrics_transcriber/frontend/src/components/WordDivider.tsx +4 -3
  13. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +1 -2
  14. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +68 -46
  15. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  16. {lyrics_transcriber-0.44.0.dist-info → lyrics_transcriber-0.45.0.dist-info}/METADATA +1 -1
  17. {lyrics_transcriber-0.44.0.dist-info → lyrics_transcriber-0.45.0.dist-info}/RECORD +20 -18
  18. lyrics_transcriber/frontend/dist/assets/index-DVoI6Z16.js.map +0 -1
  19. lyrics_transcriber/frontend/src/components/GlobalSyncEditor.tsx +0 -675
  20. {lyrics_transcriber-0.44.0.dist-info → lyrics_transcriber-0.45.0.dist-info}/LICENSE +0 -0
  21. {lyrics_transcriber-0.44.0.dist-info → lyrics_transcriber-0.45.0.dist-info}/WHEEL +0 -0
  22. {lyrics_transcriber-0.44.0.dist-info → lyrics_transcriber-0.45.0.dist-info}/entry_points.txt +0 -0
@@ -1,675 +0,0 @@
1
- import {
2
- Dialog,
3
- DialogTitle,
4
- DialogContent,
5
- DialogActions,
6
- IconButton,
7
- Box,
8
- Button,
9
- Typography,
10
- Slider,
11
- ButtonGroup
12
- } from '@mui/material'
13
- import CloseIcon from '@mui/icons-material/Close'
14
- import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
15
- import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline'
16
- import StopCircleIcon from '@mui/icons-material/StopCircle'
17
- import CancelIcon from '@mui/icons-material/Cancel'
18
- import ZoomInIcon from '@mui/icons-material/ZoomIn'
19
- import ZoomOutIcon from '@mui/icons-material/ZoomOut'
20
- import { LyricsSegment, Word } from '../types'
21
- import { useState, useEffect, useCallback, useRef } from 'react'
22
- import TimelineEditor from './TimelineEditor'
23
-
24
- interface GlobalSyncEditorProps {
25
- open: boolean
26
- onClose: () => void
27
- segments: LyricsSegment[]
28
- onSave: (updatedSegments: LyricsSegment[]) => void
29
- onPlaySegment?: (startTime: number) => void
30
- currentTime?: number
31
- setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
32
- }
33
-
34
- // Zoom levels in seconds per screen width - more granular at the lower end
35
- const ZOOM_LEVELS = [5, 10, 15, 20, 30, 45, 60, 90, 120, 180, 300, 600, 1200];
36
-
37
- interface FlattenedWord {
38
- word: Word;
39
- segmentIndex: number;
40
- wordIndex: number;
41
- }
42
-
43
- export default function GlobalSyncEditor({
44
- open,
45
- onClose,
46
- segments,
47
- onSave,
48
- onPlaySegment,
49
- currentTime = 0,
50
- setModalSpacebarHandler
51
- }: GlobalSyncEditorProps) {
52
- const [editedSegments, setEditedSegments] = useState<LyricsSegment[]>([]);
53
- const [allWords, setAllWords] = useState<FlattenedWord[]>([]);
54
- const [isManualSyncing, setIsManualSyncing] = useState(false);
55
- const [syncWordIndex, setSyncWordIndex] = useState<number>(-1);
56
- const [zoomLevel, setZoomLevel] = useState<number>(3); // Default to 20 seconds per screen
57
- const [scrollPosition, setScrollPosition] = useState<number>(0);
58
- const [songDuration, setSongDuration] = useState<number>(0);
59
- const [visibleTimeRange, setVisibleTimeRange] = useState<{start: number, end: number}>({start: 0, end: 20});
60
- const [isPlaying, setIsPlaying] = useState(false);
61
- const timelineContainerRef = useRef<HTMLDivElement>(null);
62
-
63
- // Initialize the edited segments and flatten all words when the modal opens
64
- useEffect(() => {
65
- if (open && segments) {
66
- const segmentsCopy = [...segments];
67
- setEditedSegments(segmentsCopy);
68
-
69
- // Create flattened word list with references to original segment and word indices
70
- const flattenedWords: FlattenedWord[] = [];
71
- segmentsCopy.forEach((segment, segmentIndex) => {
72
- segment.words.forEach((word, wordIndex) => {
73
- flattenedWords.push({
74
- word,
75
- segmentIndex,
76
- wordIndex
77
- });
78
- });
79
- });
80
-
81
- // Sort words by start time
82
- flattenedWords.sort((a, b) => {
83
- const aStart = a.word.start_time ?? 0;
84
- const bStart = b.word.start_time ?? 0;
85
- return aStart - bStart;
86
- });
87
-
88
- setAllWords(flattenedWords);
89
-
90
- // Calculate song duration
91
- const allEndTimes = flattenedWords.map(item => item.word.end_time).filter((t): t is number => t !== null);
92
- const duration = allEndTimes.length > 0 ? Math.max(...allEndTimes) + 5 : 60; // Add 5 seconds padding
93
- setSongDuration(duration);
94
-
95
- // Reset scroll position
96
- setScrollPosition(0);
97
- updateVisibleTimeRange(0, zoomLevel);
98
- }
99
- }, [open, segments, zoomLevel]);
100
-
101
- // Update visible time range when scroll position or zoom level changes
102
- const updateVisibleTimeRange = useCallback((scrollPos: number, zoom: number) => {
103
- const secondsPerScreen = ZOOM_LEVELS[zoom];
104
- const start = scrollPos * songDuration / 100;
105
- const end = start + secondsPerScreen;
106
- setVisibleTimeRange({start, end});
107
- }, [songDuration]);
108
-
109
- // Handle scroll position change
110
- const handleScroll = useCallback(() => {
111
- if (!timelineContainerRef.current) return;
112
-
113
- const container = timelineContainerRef.current;
114
- const scrollPercentage = (container.scrollLeft / (container.scrollWidth - container.clientWidth)) * 100;
115
- setScrollPosition(scrollPercentage);
116
- updateVisibleTimeRange(scrollPercentage, zoomLevel);
117
- }, [zoomLevel, updateVisibleTimeRange]);
118
-
119
- // Handle zoom level change
120
- const handleZoomChange = useCallback((_event: React.SyntheticEvent | Event, newValue: number | number[]) => {
121
- const newZoom = Array.isArray(newValue) ? newValue[0] : newValue;
122
- setZoomLevel(newZoom);
123
- updateVisibleTimeRange(scrollPosition, newZoom);
124
- }, [scrollPosition, updateVisibleTimeRange]);
125
-
126
- // Handle word update
127
- const handleWordUpdate = useCallback((flatWordIndex: number, updates: Partial<Word>) => {
128
- if (flatWordIndex < 0 || flatWordIndex >= allWords.length) return;
129
-
130
- const { segmentIndex, wordIndex } = allWords[flatWordIndex];
131
- const newSegments = [...editedSegments];
132
-
133
- if (segmentIndex < 0 || segmentIndex >= newSegments.length) return;
134
- const segment = newSegments[segmentIndex];
135
-
136
- if (wordIndex < 0 || wordIndex >= segment.words.length) return;
137
-
138
- // Update the word
139
- const newWords = [...segment.words];
140
- newWords[wordIndex] = {
141
- ...newWords[wordIndex],
142
- ...updates
143
- };
144
-
145
- // Update segment start/end times based on words
146
- const validStartTimes = newWords.map(w => w.start_time).filter((t): t is number => t !== null);
147
- const validEndTimes = newWords.map(w => w.end_time).filter((t): t is number => t !== null);
148
-
149
- const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : segment.start_time;
150
- const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : segment.end_time;
151
-
152
- newSegments[segmentIndex] = {
153
- ...segment,
154
- words: newWords,
155
- start_time: segmentStartTime,
156
- end_time: segmentEndTime
157
- };
158
-
159
- setEditedSegments(newSegments);
160
-
161
- // Update the flattened words array
162
- const newAllWords = [...allWords];
163
- newAllWords[flatWordIndex] = {
164
- ...newAllWords[flatWordIndex],
165
- word: {
166
- ...newAllWords[flatWordIndex].word,
167
- ...updates
168
- }
169
- };
170
- setAllWords(newAllWords);
171
- }, [allWords, editedSegments]);
172
-
173
- // Create a custom manual sync hook for the global timeline
174
- const useGlobalManualSync = () => {
175
- const [isSpacebarPressed, setIsSpacebarPressed] = useState(false);
176
- const wordStartTimeRef = useRef<number | null>(null);
177
- const spacebarPressTimeRef = useRef<number | null>(null);
178
- const currentTimeRef = useRef(currentTime);
179
-
180
- // Keep currentTimeRef up to date
181
- useEffect(() => {
182
- currentTimeRef.current = currentTime;
183
- }, [currentTime]);
184
-
185
- const cleanupManualSync = useCallback(() => {
186
- setIsManualSyncing(false);
187
- setSyncWordIndex(-1);
188
- setIsSpacebarPressed(false);
189
- wordStartTimeRef.current = null;
190
- spacebarPressTimeRef.current = null;
191
- }, []);
192
-
193
- const startManualSyncFromBeginning = useCallback(() => {
194
- if (isManualSyncing) {
195
- cleanupManualSync();
196
- return;
197
- }
198
-
199
- if (!onPlaySegment || allWords.length === 0) return;
200
-
201
- setIsManualSyncing(true);
202
- setSyncWordIndex(0);
203
- setIsSpacebarPressed(false);
204
- wordStartTimeRef.current = null;
205
- spacebarPressTimeRef.current = null;
206
-
207
- // Start playing 3 seconds before the first word
208
- const firstWordStartTime = allWords[0].word.start_time ?? 0;
209
- onPlaySegment(Math.max(0, firstWordStartTime - 3));
210
- setIsPlaying(true);
211
- }, [isManualSyncing, allWords, onPlaySegment, cleanupManualSync]);
212
-
213
- const startManualSyncFromCurrent = useCallback(() => {
214
- if (isManualSyncing) {
215
- cleanupManualSync();
216
- return;
217
- }
218
-
219
- if (!onPlaySegment || allWords.length === 0) return;
220
-
221
- // Find the word closest to the current time
222
- const currentT = currentTimeRef.current;
223
- let closestIndex = 0;
224
- let minDiff = Number.MAX_VALUE;
225
-
226
- allWords.forEach((item, index) => {
227
- const wordStart = item.word.start_time ?? 0;
228
- const diff = Math.abs(wordStart - currentT);
229
- if (diff < minDiff) {
230
- minDiff = diff;
231
- closestIndex = index;
232
- }
233
- });
234
-
235
- setIsManualSyncing(true);
236
- setSyncWordIndex(closestIndex);
237
- setIsSpacebarPressed(false);
238
- wordStartTimeRef.current = null;
239
- spacebarPressTimeRef.current = null;
240
-
241
- // Start playing 3 seconds before the selected word
242
- const wordStartTime = allWords[closestIndex].word.start_time ?? 0;
243
- onPlaySegment(Math.max(0, wordStartTime - 3));
244
- setIsPlaying(true);
245
- }, [isManualSyncing, allWords, onPlaySegment, cleanupManualSync, currentTimeRef]);
246
-
247
- const handleKeyDown = useCallback((e: KeyboardEvent) => {
248
- if (e.code !== 'Space') return;
249
-
250
- console.log('GlobalSyncEditor - Spacebar pressed down', {
251
- isManualSyncing,
252
- syncWordIndex,
253
- currentTime: currentTimeRef.current
254
- });
255
-
256
- e.preventDefault();
257
- e.stopPropagation();
258
-
259
- if (isManualSyncing && !isSpacebarPressed && syncWordIndex >= 0 && syncWordIndex < allWords.length) {
260
- const currentWord = allWords[syncWordIndex];
261
- console.log('GlobalSyncEditor - Recording word start time', {
262
- wordIndex: syncWordIndex,
263
- wordText: currentWord.word.text,
264
- time: currentTimeRef.current
265
- });
266
-
267
- setIsSpacebarPressed(true);
268
-
269
- // Record the start time of the current word
270
- wordStartTimeRef.current = currentTimeRef.current;
271
-
272
- // Record when the spacebar was pressed (for tap detection)
273
- spacebarPressTimeRef.current = Date.now();
274
-
275
- // Update the word's start time immediately
276
- handleWordUpdate(syncWordIndex, { start_time: currentTimeRef.current });
277
- } else if (!isManualSyncing && onPlaySegment) {
278
- // Toggle playback when not in manual sync mode
279
- if (window.toggleAudioPlayback) {
280
- window.toggleAudioPlayback();
281
- setIsPlaying(prev => !prev);
282
- }
283
- }
284
- }, [isManualSyncing, syncWordIndex, allWords, isSpacebarPressed, handleWordUpdate, onPlaySegment]);
285
-
286
- const handleKeyUp = useCallback((e: KeyboardEvent) => {
287
- if (e.code !== 'Space') return;
288
-
289
- console.log('GlobalSyncEditor - Spacebar released', {
290
- isManualSyncing,
291
- syncWordIndex,
292
- currentTime: currentTimeRef.current,
293
- wordStartTime: wordStartTimeRef.current
294
- });
295
-
296
- e.preventDefault();
297
- e.stopPropagation();
298
-
299
- if (isManualSyncing && isSpacebarPressed && syncWordIndex >= 0 && syncWordIndex < allWords.length) {
300
- const currentWord = allWords[syncWordIndex];
301
- const pressDuration = spacebarPressTimeRef.current ? Date.now() - spacebarPressTimeRef.current : 0;
302
- const isTap = pressDuration < 200; // If pressed for less than 200ms, consider it a tap
303
-
304
- console.log('GlobalSyncEditor - Recording word end time', {
305
- wordIndex: syncWordIndex,
306
- wordText: currentWord.word.text,
307
- startTime: wordStartTimeRef.current,
308
- endTime: currentTimeRef.current,
309
- pressDuration: `${pressDuration}ms`,
310
- isTap,
311
- duration: (currentTimeRef.current - (wordStartTimeRef.current || 0)).toFixed(2) + 's'
312
- });
313
-
314
- setIsSpacebarPressed(false);
315
-
316
- // Set the end time for the current word based on whether it was a tap or hold
317
- if (isTap) {
318
- // For a tap, set a default duration of 1 second
319
- const defaultEndTime = (wordStartTimeRef.current || currentTimeRef.current) + 1.0;
320
- handleWordUpdate(syncWordIndex, { end_time: defaultEndTime });
321
- } else {
322
- // For a hold, use the current time as the end time
323
- handleWordUpdate(syncWordIndex, { end_time: currentTimeRef.current });
324
- }
325
-
326
- // Move to the next word
327
- if (syncWordIndex === allWords.length - 1) {
328
- // If this was the last word, finish manual sync
329
- console.log('GlobalSyncEditor - Completed manual sync for all words');
330
- setIsManualSyncing(false);
331
- setSyncWordIndex(-1);
332
- wordStartTimeRef.current = null;
333
- spacebarPressTimeRef.current = null;
334
- } else {
335
- // Otherwise, move to the next word
336
- const nextWord = allWords[syncWordIndex + 1];
337
- console.log('GlobalSyncEditor - Moving to next word', {
338
- nextWordIndex: syncWordIndex + 1,
339
- nextWordText: nextWord.word.text
340
- });
341
- setSyncWordIndex(syncWordIndex + 1);
342
-
343
- // If the next word's start time would overlap with the current word's end time,
344
- // adjust the next word's start time
345
- const currentEndTime = currentWord.word.end_time;
346
- const nextStartTime = nextWord.word.start_time;
347
-
348
- if (currentEndTime !== null && nextStartTime !== null && currentEndTime > nextStartTime) {
349
- handleWordUpdate(syncWordIndex + 1, { start_time: currentEndTime + 0.01 });
350
- }
351
- }
352
- }
353
- }, [isManualSyncing, syncWordIndex, allWords, isSpacebarPressed, handleWordUpdate]);
354
-
355
- // Combine the key handlers into a single function for external use
356
- const handleSpacebar = useCallback((e: KeyboardEvent) => {
357
- if (e.type === 'keydown') {
358
- handleKeyDown(e);
359
- } else if (e.type === 'keyup') {
360
- handleKeyUp(e);
361
- }
362
- }, [handleKeyDown, handleKeyUp]);
363
-
364
- return {
365
- isSpacebarPressed,
366
- startManualSyncFromBeginning,
367
- startManualSyncFromCurrent,
368
- cleanupManualSync,
369
- handleSpacebar
370
- };
371
- };
372
-
373
- const {
374
- isSpacebarPressed,
375
- startManualSyncFromBeginning,
376
- startManualSyncFromCurrent,
377
- cleanupManualSync,
378
- handleSpacebar
379
- } = useGlobalManualSync();
380
-
381
- // Update the spacebar handler when modal state changes
382
- useEffect(() => {
383
- const spacebarHandler = handleSpacebar; // Capture the current handler
384
-
385
- if (open) {
386
- console.log('GlobalSyncEditor - Setting up modal spacebar handler');
387
-
388
- // Create a function that will be called by the global event listeners
389
- const handleKeyEvent = (e: KeyboardEvent) => {
390
- if (e.code === 'Space') {
391
- spacebarHandler(e);
392
- }
393
- };
394
-
395
- // Wrap the handler function to match the expected signature
396
- setModalSpacebarHandler(() => () => handleKeyEvent);
397
-
398
- // Only cleanup when the effect is re-run or the modal is closed
399
- return () => {
400
- if (!open) {
401
- console.log('GlobalSyncEditor - Cleanup: clearing modal spacebar handler');
402
- setModalSpacebarHandler(undefined);
403
- }
404
- };
405
- }
406
- }, [
407
- open,
408
- handleSpacebar,
409
- setModalSpacebarHandler
410
- ]);
411
-
412
- // Update isPlaying state when currentTime changes
413
- useEffect(() => {
414
- if (window.isAudioPlaying !== undefined) {
415
- setIsPlaying(window.isAudioPlaying);
416
- }
417
- }, [currentTime]);
418
-
419
- const handleClose = useCallback(() => {
420
- cleanupManualSync();
421
- onClose();
422
- }, [onClose, cleanupManualSync]);
423
-
424
- const handleSave = useCallback(() => {
425
- onSave(editedSegments);
426
- onClose();
427
- }, [editedSegments, onSave, onClose]);
428
-
429
- const handlePlayFromTime = useCallback((time: number) => {
430
- if (onPlaySegment) {
431
- onPlaySegment(time);
432
- setIsPlaying(true);
433
- }
434
- }, [onPlaySegment]);
435
-
436
- const handlePlayPause = useCallback(() => {
437
- if (window.toggleAudioPlayback) {
438
- window.toggleAudioPlayback();
439
- setIsPlaying(prev => !prev);
440
- }
441
- }, []);
442
-
443
- const handleStop = useCallback(() => {
444
- if (window.isAudioPlaying && window.toggleAudioPlayback) {
445
- window.toggleAudioPlayback(); // Pause the audio
446
- setIsPlaying(false);
447
- }
448
- }, []);
449
-
450
- // Scroll to current time
451
- const scrollToCurrentTime = useCallback(() => {
452
- if (!timelineContainerRef.current) return;
453
-
454
- const container = timelineContainerRef.current;
455
- const totalWidth = container.scrollWidth;
456
- const viewportWidth = container.clientWidth;
457
-
458
- // Calculate the position of the current time as a percentage of the total duration
459
- const currentTimePosition = (currentTime / songDuration) * totalWidth;
460
-
461
- // Calculate the scroll position to center the current time in the viewport
462
- const scrollLeft = Math.max(0, currentTimePosition - (viewportWidth / 2));
463
-
464
- // Smoothly scroll to the position
465
- container.scrollTo({
466
- left: scrollLeft,
467
- behavior: 'smooth'
468
- });
469
- }, [currentTime, songDuration]);
470
-
471
- // Early return if no segments
472
- if (!segments || segments.length === 0) return null;
473
-
474
- // Calculate zoom-related values
475
- const secondsPerScreen = ZOOM_LEVELS[zoomLevel];
476
- const totalWidthPercentage = (songDuration / secondsPerScreen) * 100;
477
-
478
- return (
479
- <Dialog
480
- open={open}
481
- onClose={handleClose}
482
- maxWidth="lg"
483
- fullWidth
484
- PaperProps={{
485
- sx: {
486
- height: '80vh',
487
- display: 'flex',
488
- flexDirection: 'column'
489
- }
490
- }}
491
- >
492
- <DialogTitle>
493
- Edit Sync - All Words
494
- <IconButton
495
- aria-label="close"
496
- onClick={handleClose}
497
- sx={{
498
- position: 'absolute',
499
- right: 8,
500
- top: 8,
501
- color: (theme) => theme.palette.grey[500],
502
- }}
503
- >
504
- <CloseIcon />
505
- </IconButton>
506
- </DialogTitle>
507
-
508
- <DialogContent sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
509
- {/* Zoom controls */}
510
- <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
511
- <Typography variant="body2">Zoom:</Typography>
512
- <IconButton
513
- size="small"
514
- onClick={() => setZoomLevel(Math.max(0, zoomLevel - 1))}
515
- disabled={zoomLevel === 0}
516
- >
517
- <ZoomOutIcon />
518
- </IconButton>
519
-
520
- <Slider
521
- value={zoomLevel}
522
- min={0}
523
- max={ZOOM_LEVELS.length - 1}
524
- step={1}
525
- onChange={handleZoomChange}
526
- marks
527
- sx={{ flexGrow: 1, maxWidth: 300 }}
528
- />
529
-
530
- <IconButton
531
- size="small"
532
- onClick={() => setZoomLevel(Math.min(ZOOM_LEVELS.length - 1, zoomLevel + 1))}
533
- disabled={zoomLevel === ZOOM_LEVELS.length - 1}
534
- >
535
- <ZoomInIcon />
536
- </IconButton>
537
-
538
- <Typography variant="body2">
539
- {ZOOM_LEVELS[zoomLevel]} seconds per screen
540
- </Typography>
541
- </Box>
542
-
543
- {/* Playback and manual sync controls */}
544
- <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
545
- <ButtonGroup variant="outlined">
546
- <Button
547
- onClick={handlePlayPause}
548
- startIcon={isPlaying ? <PauseCircleOutlineIcon /> : <PlayCircleOutlineIcon />}
549
- >
550
- {isPlaying ? "Pause" : "Play"}
551
- </Button>
552
- <Button
553
- onClick={handleStop}
554
- startIcon={<StopCircleIcon />}
555
- disabled={!isPlaying}
556
- >
557
- Stop
558
- </Button>
559
- <Button
560
- onClick={scrollToCurrentTime}
561
- disabled={!currentTime}
562
- >
563
- Go to Current Time
564
- </Button>
565
- </ButtonGroup>
566
-
567
- <Button
568
- variant={isManualSyncing ? "outlined" : "contained"}
569
- onClick={startManualSyncFromBeginning}
570
- disabled={!onPlaySegment || allWords.length === 0}
571
- startIcon={isManualSyncing ? <CancelIcon /> : <PlayCircleOutlineIcon />}
572
- color={isManualSyncing ? "error" : "primary"}
573
- >
574
- {isManualSyncing ? "Cancel Sync" : "Sync From Start"}
575
- </Button>
576
-
577
- <Button
578
- variant="contained"
579
- onClick={startManualSyncFromCurrent}
580
- disabled={!onPlaySegment || allWords.length === 0 || isManualSyncing}
581
- startIcon={<PlayCircleOutlineIcon />}
582
- >
583
- Sync From Current Time
584
- </Button>
585
- </Box>
586
-
587
- {/* Manual sync status */}
588
- {isManualSyncing && syncWordIndex >= 0 && syncWordIndex < allWords.length && (
589
- <Box sx={{ mb: 2 }}>
590
- <Typography variant="body2">
591
- Word {syncWordIndex + 1} of {allWords.length}: <strong>{allWords[syncWordIndex].word.text || ''}</strong>
592
- </Typography>
593
- <Typography variant="caption" color="text.secondary">
594
- {isSpacebarPressed ?
595
- "Holding spacebar... Release when word ends" :
596
- "Press spacebar when word starts (tap for short words, hold for long words)"}
597
- </Typography>
598
- </Box>
599
- )}
600
-
601
- {/* Timeline container with horizontal scrolling */}
602
- <Box
603
- ref={timelineContainerRef}
604
- sx={{
605
- flexGrow: 1,
606
- overflowX: 'auto',
607
- overflowY: 'hidden',
608
- position: 'relative',
609
- '&::-webkit-scrollbar': {
610
- height: '10px',
611
- },
612
- '&::-webkit-scrollbar-thumb': {
613
- backgroundColor: 'rgba(0,0,0,0.2)',
614
- borderRadius: '10px',
615
- },
616
- }}
617
- onScroll={handleScroll}
618
- >
619
- <Box sx={{
620
- width: `${totalWidthPercentage}%`,
621
- minWidth: '100%',
622
- height: '100%',
623
- position: 'relative',
624
- }}>
625
- {/* Timeline editor */}
626
- <TimelineEditor
627
- words={allWords.map(item => item.word)}
628
- startTime={0}
629
- endTime={songDuration}
630
- onWordUpdate={(index, updates) => handleWordUpdate(index, updates)}
631
- currentTime={currentTime}
632
- onPlaySegment={handlePlayFromTime}
633
- showPlaybackIndicator={false} // Disable the built-in indicator to avoid duplication
634
- />
635
-
636
- {/* Custom current time indicator */}
637
- {currentTime >= 0 && currentTime <= songDuration && (
638
- <Box
639
- sx={{
640
- position: 'absolute',
641
- top: 0,
642
- left: `${(currentTime / songDuration) * 100}%`,
643
- width: '2px',
644
- height: '100%',
645
- backgroundColor: 'error.main',
646
- zIndex: 10,
647
- }}
648
- />
649
- )}
650
- </Box>
651
- </Box>
652
-
653
- {/* Time range indicator */}
654
- <Box sx={{ mt: 1, display: 'flex', justifyContent: 'space-between' }}>
655
- <Typography variant="body2">
656
- Visible: {visibleTimeRange.start.toFixed(1)}s - {visibleTimeRange.end.toFixed(1)}s
657
- </Typography>
658
- <Typography variant="body2">
659
- Total Duration: {songDuration.toFixed(1)}s
660
- </Typography>
661
- </Box>
662
-
663
- {/* Word count indicator */}
664
- <Typography variant="body2" sx={{ mt: 1 }}>
665
- Total Words: {allWords.length}
666
- </Typography>
667
- </DialogContent>
668
-
669
- <DialogActions>
670
- <Button onClick={handleClose}>Cancel</Button>
671
- <Button onClick={handleSave} variant="contained" color="primary">Save</Button>
672
- </DialogActions>
673
- </Dialog>
674
- );
675
- }