karaoke-gen 0.71.42__py3-none-any.whl → 0.75.16__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 (32) hide show
  1. karaoke_gen/__init__.py +32 -1
  2. karaoke_gen/audio_fetcher.py +476 -56
  3. karaoke_gen/audio_processor.py +11 -3
  4. karaoke_gen/instrumental_review/server.py +154 -860
  5. karaoke_gen/instrumental_review/static/index.html +1506 -0
  6. karaoke_gen/karaoke_finalise/karaoke_finalise.py +62 -1
  7. karaoke_gen/karaoke_gen.py +114 -1
  8. karaoke_gen/lyrics_processor.py +81 -4
  9. karaoke_gen/utils/bulk_cli.py +3 -0
  10. karaoke_gen/utils/cli_args.py +4 -2
  11. karaoke_gen/utils/gen_cli.py +196 -5
  12. karaoke_gen/utils/remote_cli.py +523 -34
  13. {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.16.dist-info}/METADATA +4 -1
  14. {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.16.dist-info}/RECORD +31 -25
  15. lyrics_transcriber/frontend/package.json +1 -1
  16. lyrics_transcriber/frontend/src/components/Header.tsx +38 -12
  17. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +17 -3
  18. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +185 -0
  19. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +704 -0
  20. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +80 -0
  21. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +905 -0
  22. lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +127 -0
  23. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +190 -542
  24. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  25. lyrics_transcriber/frontend/web_assets/assets/{index-DdJTDWH3.js → index-COYImAcx.js} +1722 -489
  26. lyrics_transcriber/frontend/web_assets/assets/index-COYImAcx.js.map +1 -0
  27. lyrics_transcriber/frontend/web_assets/index.html +1 -1
  28. lyrics_transcriber/review/server.py +5 -5
  29. lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js.map +0 -1
  30. {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.16.dist-info}/WHEEL +0 -0
  31. {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.16.dist-info}/entry_points.txt +0 -0
  32. {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.16.dist-info}/licenses/LICENSE +0 -0
@@ -7,19 +7,17 @@ import {
7
7
  Box,
8
8
  Button,
9
9
  Typography,
10
- TextField,
11
- Paper,
12
- Divider
10
+ TextField
13
11
  } from '@mui/material'
14
12
  import CloseIcon from '@mui/icons-material/Close'
15
13
  import ContentPasteIcon from '@mui/icons-material/ContentPaste'
16
14
  import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
15
+ import ArrowBackIcon from '@mui/icons-material/ArrowBack'
17
16
  import { LyricsSegment, Word } from '../types'
18
- import { useState, useEffect, useCallback, useMemo, useRef, memo } from 'react'
17
+ import { useState, useEffect, useCallback, useMemo } from 'react'
19
18
  import { nanoid } from 'nanoid'
20
- import useManualSync from '../hooks/useManualSync'
21
- import EditTimelineSection from './EditTimelineSection'
22
- import EditActionBar from './EditActionBar'
19
+ import ModeSelectionModal from './ModeSelectionModal'
20
+ import LyricsSynchronizer from './LyricsSynchronizer'
23
21
 
24
22
  // Augment window type for audio functions
25
23
  declare global {
@@ -30,6 +28,8 @@ declare global {
30
28
  }
31
29
  }
32
30
 
31
+ type ModalMode = 'selection' | 'replace' | 'resync'
32
+
33
33
  interface ReplaceAllLyricsModalProps {
34
34
  open: boolean
35
35
  onClose: () => void
@@ -37,6 +37,7 @@ interface ReplaceAllLyricsModalProps {
37
37
  onPlaySegment?: (startTime: number) => void
38
38
  currentTime?: number
39
39
  setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
40
+ existingSegments?: LyricsSegment[] // Current segments for re-sync mode
40
41
  }
41
42
 
42
43
  export default function ReplaceAllLyricsModal({
@@ -45,22 +46,21 @@ export default function ReplaceAllLyricsModal({
45
46
  onSave,
46
47
  onPlaySegment,
47
48
  currentTime = 0,
48
- setModalSpacebarHandler
49
+ setModalSpacebarHandler,
50
+ existingSegments = []
49
51
  }: ReplaceAllLyricsModalProps) {
52
+ const [mode, setMode] = useState<ModalMode>('selection')
50
53
  const [inputText, setInputText] = useState('')
51
- const [isReplaced, setIsReplaced] = useState(false)
52
- const [globalSegment, setGlobalSegment] = useState<LyricsSegment | null>(null)
53
- const [originalSegments, setOriginalSegments] = useState<LyricsSegment[]>([])
54
- const [currentSegments, setCurrentSegments] = useState<LyricsSegment[]>([])
54
+ const [newSegments, setNewSegments] = useState<LyricsSegment[]>([])
55
55
 
56
- // Get the real audio duration, with fallback
57
- const getAudioDuration = useCallback(() => {
58
- if (window.getAudioDuration) {
59
- const duration = window.getAudioDuration()
60
- return duration > 0 ? duration : 600 // Use real duration or 10 min fallback
56
+ // Reset state when modal opens
57
+ useEffect(() => {
58
+ if (open) {
59
+ setMode('selection')
60
+ setInputText('')
61
+ setNewSegments([])
61
62
  }
62
- return 600 // 10 minute fallback if audio not loaded
63
- }, [])
63
+ }, [open])
64
64
 
65
65
  // Parse the input text to get line and word counts
66
66
  const parseInfo = useMemo(() => {
@@ -79,61 +79,31 @@ export default function ReplaceAllLyricsModal({
79
79
  if (!inputText.trim()) return
80
80
 
81
81
  const lines = inputText.trim().split('\n').filter(line => line.trim().length > 0)
82
- const newSegments: LyricsSegment[] = []
83
- const allWords: Word[] = []
82
+ const segments: LyricsSegment[] = []
84
83
 
85
84
  lines.forEach((line) => {
86
85
  const words = line.trim().split(/\s+/).filter(word => word.length > 0)
87
- const segmentWords: Word[] = []
88
-
89
- words.forEach((wordText) => {
90
- const word: Word = {
91
- id: nanoid(),
92
- text: wordText,
93
- start_time: null,
94
- end_time: null,
95
- confidence: 1.0,
96
- created_during_correction: true
97
- }
98
- segmentWords.push(word)
99
- allWords.push(word)
100
- })
86
+ const segmentWords: Word[] = words.map((wordText) => ({
87
+ id: nanoid(),
88
+ text: wordText,
89
+ start_time: null,
90
+ end_time: null,
91
+ confidence: 1.0,
92
+ created_during_correction: true
93
+ }))
101
94
 
102
- const segment: LyricsSegment = {
95
+ segments.push({
103
96
  id: nanoid(),
104
97
  text: line.trim(),
105
98
  words: segmentWords,
106
99
  start_time: null,
107
100
  end_time: null
108
- }
109
-
110
- newSegments.push(segment)
111
- })
112
-
113
- // Create a global segment with all words for manual sync
114
- // Set a very large end time to ensure manual sync doesn't stop prematurely
115
- const audioDuration = getAudioDuration()
116
- const endTime = Math.max(audioDuration, 3600) // At least 1 hour to prevent auto-stop
117
-
118
- console.log('ReplaceAllLyricsModal - Creating global segment', {
119
- audioDuration,
120
- endTime,
121
- wordCount: allWords.length
101
+ })
122
102
  })
123
-
124
- const globalSegment: LyricsSegment = {
125
- id: 'global-replacement',
126
- text: allWords.map(w => w.text).join(' '),
127
- words: allWords,
128
- start_time: 0,
129
- end_time: endTime
130
- }
131
103
 
132
- setCurrentSegments(newSegments)
133
- setOriginalSegments(JSON.parse(JSON.stringify(newSegments)))
134
- setGlobalSegment(globalSegment)
135
- setIsReplaced(true)
136
- }, [inputText, getAudioDuration])
104
+ setNewSegments(segments)
105
+ setMode('resync') // Go to synchronizer with the new segments
106
+ }, [inputText])
137
107
 
138
108
  // Handle paste from clipboard
139
109
  const handlePasteFromClipboard = useCallback(async () => {
@@ -146,311 +116,90 @@ export default function ReplaceAllLyricsModal({
146
116
  }
147
117
  }, [])
148
118
 
149
- // Update segment when words change during manual sync
150
- const updateSegment = useCallback((newWords: Word[]) => {
151
- if (!globalSegment) return
152
-
153
- const validStartTimes = newWords.map(w => w.start_time).filter((t): t is number => t !== null)
154
- const validEndTimes = newWords.map(w => w.end_time).filter((t): t is number => t !== null)
155
-
156
- const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null
157
- const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null
158
-
159
- const updatedGlobalSegment = {
160
- ...globalSegment,
161
- words: newWords,
162
- text: newWords.map(w => w.text).join(' '),
163
- start_time: segmentStartTime,
164
- end_time: segmentEndTime
165
- }
166
-
167
- // Batch state updates to prevent multiple re-renders
168
- setGlobalSegment(updatedGlobalSegment)
169
-
170
- // Update individual segment timing as words get synced - but only calculate when needed
171
- const updatedSegments = currentSegments.map(segment => {
172
- // Find words that belong to this segment and have been timed
173
- const segmentWordsWithTiming = segment.words.map(segmentWord => {
174
- const globalWord = newWords.find(w => w.id === segmentWord.id)
175
- return globalWord || segmentWord
176
- })
177
-
178
- // Calculate segment timing if all words have timing
179
- const wordsWithTiming = segmentWordsWithTiming.filter(w =>
180
- w.start_time !== null && w.end_time !== null
181
- )
182
-
183
- if (wordsWithTiming.length === segmentWordsWithTiming.length && wordsWithTiming.length > 0) {
184
- // All words in this segment have timing - update segment timing
185
- const segmentStart = Math.min(...wordsWithTiming.map(w => w.start_time!))
186
- const segmentEnd = Math.max(...wordsWithTiming.map(w => w.end_time!))
187
-
188
- return {
189
- ...segment,
190
- words: segmentWordsWithTiming,
191
- start_time: segmentStart,
192
- end_time: segmentEnd
193
- }
194
- } else {
195
- // Some words still don't have timing - just update the words
196
- return {
197
- ...segment,
198
- words: segmentWordsWithTiming
199
- }
200
- }
201
- })
202
-
203
- setCurrentSegments(updatedSegments)
204
- }, [globalSegment, currentSegments])
205
-
206
- // Use the manual sync hook
207
- const {
208
- isManualSyncing,
209
- isPaused,
210
- syncWordIndex,
211
- startManualSync,
212
- pauseManualSync,
213
- resumeManualSync,
214
- cleanupManualSync,
215
- handleSpacebar,
216
- isSpacebarPressed
217
- } = useManualSync({
218
- editedSegment: globalSegment,
219
- currentTime,
220
- onPlaySegment,
221
- updateSegment
222
- })
223
-
224
- // Handle manual word updates (drag/resize in timeline)
225
- const handleWordUpdate = useCallback((wordIndex: number, updates: Partial<Word>) => {
226
- if (!globalSegment) return
227
-
228
- // Only allow manual adjustments when manual sync is paused or not active
229
- if (isManualSyncing && !isPaused) {
230
- console.log('ReplaceAllLyricsModal - Ignoring word update during active manual sync')
231
- return
232
- }
233
-
234
- console.log('ReplaceAllLyricsModal - Manual word update', {
235
- wordIndex,
236
- wordText: globalSegment.words[wordIndex]?.text,
237
- updates,
238
- isManualSyncing,
239
- isPaused
240
- })
241
-
242
- // Update the word in the global segment
243
- const newWords = [...globalSegment.words]
244
- newWords[wordIndex] = {
245
- ...newWords[wordIndex],
246
- ...updates
247
- }
248
-
249
- // Update the global segment through the existing updateSegment function
250
- updateSegment(newWords)
251
- }, [globalSegment, updateSegment, isManualSyncing, isPaused])
252
-
253
- // Handle un-syncing a word (right-click context menu)
254
- const handleUnsyncWord = useCallback((wordIndex: number) => {
255
- if (!globalSegment) return
256
-
257
- console.log('ReplaceAllLyricsModal - Un-syncing word', {
258
- wordIndex,
259
- wordText: globalSegment.words[wordIndex]?.text
260
- })
261
-
262
- // Update the word to remove timing
263
- const newWords = [...globalSegment.words]
264
- newWords[wordIndex] = {
265
- ...newWords[wordIndex],
266
- start_time: null,
267
- end_time: null
268
- }
269
-
270
- // Update the global segment through the existing updateSegment function
271
- updateSegment(newWords)
272
- }, [globalSegment, updateSegment])
273
-
274
119
  // Handle modal close
275
120
  const handleClose = useCallback(() => {
276
- cleanupManualSync()
121
+ setMode('selection')
277
122
  setInputText('')
278
- setIsReplaced(false)
279
- setGlobalSegment(null)
280
- setOriginalSegments([])
281
- setCurrentSegments([])
123
+ setNewSegments([])
282
124
  onClose()
283
- }, [onClose, cleanupManualSync])
284
-
285
- // Handle save
286
- const handleSave = useCallback(() => {
287
- if (!globalSegment || !currentSegments.length) return
288
-
289
- // Distribute the timed words back to their original segments
290
- const finalSegments: LyricsSegment[] = []
291
- let wordIndex = 0
292
-
293
- currentSegments.forEach((segment) => {
294
- const originalWordCount = segment.words.length
295
- const segmentWords = globalSegment.words.slice(wordIndex, wordIndex + originalWordCount)
296
- wordIndex += originalWordCount
297
-
298
- if (segmentWords.length > 0) {
299
- // Recalculate segment start and end times
300
- const validStartTimes = segmentWords.map(w => w.start_time).filter((t): t is number => t !== null)
301
- const validEndTimes = segmentWords.map(w => w.end_time).filter((t): t is number => t !== null)
302
-
303
- const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null
304
- const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null
305
-
306
- finalSegments.push({
307
- ...segment,
308
- words: segmentWords,
309
- text: segmentWords.map(w => w.text).join(' '),
310
- start_time: segmentStartTime,
311
- end_time: segmentEndTime
312
- })
313
- }
314
- })
315
-
316
- console.log('ReplaceAllLyricsModal - Saving new segments:', {
317
- originalSegmentCount: currentSegments.length,
318
- finalSegmentCount: finalSegments.length,
319
- totalWords: finalSegments.reduce((count, seg) => count + seg.words.length, 0)
320
- })
125
+ }, [onClose])
321
126
 
322
- onSave(finalSegments)
127
+ // Handle save from synchronizer
128
+ const handleSave = useCallback((segments: LyricsSegment[]) => {
129
+ onSave(segments)
323
130
  handleClose()
324
- }, [globalSegment, currentSegments, onSave, handleClose])
325
-
326
- // Handle reset
327
- const handleReset = useCallback(() => {
328
- if (!originalSegments.length) return
329
-
330
- console.log('ReplaceAllLyricsModal - Resetting to original state')
331
-
332
- // Reset all words to have null timing (ready for fresh manual sync)
333
- const resetWords = originalSegments.flatMap(segment =>
334
- segment.words.map(word => ({
335
- ...word,
336
- start_time: null,
337
- end_time: null
338
- }))
339
- )
340
-
341
- const audioDuration = getAudioDuration()
342
- const resetGlobalSegment: LyricsSegment = {
343
- id: 'global-replacement',
344
- text: resetWords.map(w => w.text).join(' '),
345
- words: resetWords,
346
- start_time: 0,
347
- end_time: Math.max(audioDuration, 3600) // At least 1 hour to prevent auto-stop
348
- }
349
-
350
- // Also reset the current segments to have null timing
351
- const resetCurrentSegments = originalSegments.map(segment => ({
352
- ...segment,
353
- words: segment.words.map(word => ({
354
- ...word,
355
- start_time: null,
356
- end_time: null
357
- })),
358
- start_time: null,
359
- end_time: null
360
- }))
361
-
362
- setGlobalSegment(resetGlobalSegment)
363
- setCurrentSegments(resetCurrentSegments)
364
- }, [originalSegments, getAudioDuration])
131
+ }, [onSave, handleClose])
365
132
 
366
- // Keep a ref to the current spacebar handler to avoid closure issues
367
- const spacebarHandlerRef = useRef(handleSpacebar)
368
- spacebarHandlerRef.current = handleSpacebar
369
-
370
- // Update the spacebar handler when modal state changes
371
- useEffect(() => {
372
- if (open && isReplaced) {
373
- console.log('ReplaceAllLyricsModal - Setting up spacebar handler')
374
-
375
- const handleKeyEvent = (e: KeyboardEvent) => {
376
- if (e.code === 'Space') {
377
- console.log('ReplaceAllLyricsModal - Spacebar captured in modal')
378
- e.preventDefault()
379
- e.stopPropagation()
380
- // Use the ref to get the current handler
381
- spacebarHandlerRef.current(e)
382
- }
383
- }
133
+ // Handle mode selection
134
+ const handleSelectReplace = useCallback(() => {
135
+ setMode('replace')
136
+ }, [])
384
137
 
385
- setModalSpacebarHandler(() => handleKeyEvent)
138
+ const handleSelectResync = useCallback(() => {
139
+ setMode('resync')
140
+ }, [])
386
141
 
387
- return () => {
388
- if (!open) {
389
- console.log('ReplaceAllLyricsModal - Clearing spacebar handler')
390
- setModalSpacebarHandler(undefined)
391
- }
392
- }
393
- } else if (open) {
394
- // Clear handler when not in replaced state
395
- setModalSpacebarHandler(undefined)
396
- }
397
- }, [open, isReplaced, setModalSpacebarHandler])
142
+ // Handle back to selection
143
+ const handleBackToSelection = useCallback(() => {
144
+ setMode('selection')
145
+ setInputText('')
146
+ setNewSegments([])
147
+ }, [])
398
148
 
399
- // Memoize timeline range to prevent recalculation
400
- const timeRange = useMemo(() => {
401
- const audioDuration = getAudioDuration()
402
- // Always use full song duration for replace-all mode
403
- return { start: 0, end: audioDuration }
404
- }, [getAudioDuration])
149
+ // Determine which segments to use for synchronizer
150
+ const segmentsForSync = mode === 'resync' && newSegments.length > 0
151
+ ? newSegments
152
+ : existingSegments
405
153
 
406
- // Memoize the segment progress props to prevent unnecessary re-renders
407
- const segmentProgressProps = useMemo(() => ({
408
- currentSegments,
409
- globalSegment,
410
- syncWordIndex
411
- }), [currentSegments, globalSegment, syncWordIndex])
154
+ // Check if we have existing lyrics
155
+ const hasExistingLyrics = existingSegments.length > 0 &&
156
+ existingSegments.some(s => s.words.length > 0)
412
157
 
413
158
  return (
414
- <Dialog
415
- open={open}
416
- onClose={handleClose}
417
- maxWidth={false}
418
- fullWidth={true}
419
- onKeyDown={(e) => {
420
- if (e.key === 'Enter' && !e.shiftKey && isReplaced) {
421
- e.preventDefault()
422
- handleSave()
423
- }
424
- }}
425
- PaperProps={{
426
- sx: {
427
- height: '90vh',
428
- margin: '5vh 2vh',
429
- maxWidth: 'calc(100vw - 4vh)',
430
- width: 'calc(100vw - 4vh)'
431
- }
432
- }}
433
- >
434
- <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
435
- <Box sx={{ flex: 1 }}>
436
- Replace All Lyrics
437
- </Box>
438
- <IconButton onClick={handleClose} sx={{ ml: 'auto' }}>
439
- <CloseIcon />
440
- </IconButton>
441
- </DialogTitle>
442
-
443
- <DialogContent
444
- dividers
445
- sx={{
446
- display: 'flex',
447
- flexDirection: 'column',
448
- flexGrow: 1,
449
- overflow: 'hidden'
159
+ <>
160
+ {/* Mode Selection Modal */}
161
+ <ModeSelectionModal
162
+ open={open && mode === 'selection'}
163
+ onClose={handleClose}
164
+ onSelectReplace={handleSelectReplace}
165
+ onSelectResync={handleSelectResync}
166
+ hasExistingLyrics={hasExistingLyrics}
167
+ />
168
+
169
+ {/* Replace All Lyrics Modal (Paste Phase) */}
170
+ <Dialog
171
+ open={open && mode === 'replace'}
172
+ onClose={handleClose}
173
+ maxWidth="md"
174
+ fullWidth
175
+ PaperProps={{
176
+ sx: {
177
+ height: '80vh',
178
+ maxHeight: '80vh'
179
+ }
450
180
  }}
451
181
  >
452
- {!isReplaced ? (
453
- // Step 1: Input new lyrics
182
+ <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
183
+ <IconButton onClick={handleBackToSelection} size="small">
184
+ <ArrowBackIcon />
185
+ </IconButton>
186
+ <Box sx={{ flex: 1 }}>
187
+ Replace All Lyrics
188
+ </Box>
189
+ <IconButton onClick={handleClose} sx={{ ml: 'auto' }}>
190
+ <CloseIcon />
191
+ </IconButton>
192
+ </DialogTitle>
193
+
194
+ <DialogContent
195
+ dividers
196
+ sx={{
197
+ display: 'flex',
198
+ flexDirection: 'column',
199
+ flexGrow: 1,
200
+ overflow: 'hidden'
201
+ }}
202
+ >
454
203
  <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, height: '100%' }}>
455
204
  <Typography variant="h6" gutterBottom>
456
205
  Paste your new lyrics below:
@@ -492,197 +241,96 @@ export default function ReplaceAllLyricsModal({
492
241
  }
493
242
  }}
494
243
  />
495
-
496
- <Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
497
- <Button
498
- variant="contained"
499
- onClick={processLyrics}
500
- disabled={!inputText.trim()}
501
- startIcon={<AutoFixHighIcon />}
502
- >
503
- Replace All Lyrics
504
- </Button>
505
- </Box>
506
244
  </Box>
507
- ) : (
508
- // Step 2: Manual sync interface
509
- <Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', gap: 2 }}>
510
- <Paper sx={{ p: 2, bgcolor: 'background.paper' }}>
511
- <Typography variant="h6" gutterBottom>
512
- Lyrics Replaced Successfully
513
- </Typography>
514
- <Typography variant="body2" color="text.secondary">
515
- Created {currentSegments.length} segments with {globalSegment?.words.length} words total.
516
- Use Manual Sync to set timing for all words.
517
- </Typography>
518
- </Paper>
519
-
520
- <Divider />
521
-
522
- {globalSegment && (
523
- <Box sx={{ display: 'flex', gap: 2, flexGrow: 1, minHeight: 0 }}>
524
- {/* Timeline Section */}
525
- <Box sx={{ flex: 2, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
526
- <EditTimelineSection
527
- words={globalSegment.words}
528
- startTime={timeRange.start}
529
- endTime={timeRange.end}
530
- originalStartTime={0}
531
- originalEndTime={getAudioDuration()}
532
- currentStartTime={globalSegment.start_time}
533
- currentEndTime={globalSegment.end_time}
534
- currentTime={currentTime}
535
- isManualSyncing={isManualSyncing}
536
- syncWordIndex={syncWordIndex}
537
- isSpacebarPressed={isSpacebarPressed}
538
- onWordUpdate={handleWordUpdate}
539
- onUnsyncWord={handleUnsyncWord}
540
- onPlaySegment={onPlaySegment}
541
- onStopAudio={() => {
542
- // Stop audio playback using global function
543
- if (window.toggleAudioPlayback && window.isAudioPlaying) {
544
- window.toggleAudioPlayback()
545
- }
546
- }}
547
- startManualSync={startManualSync}
548
- pauseManualSync={pauseManualSync}
549
- resumeManualSync={resumeManualSync}
550
- isPaused={isPaused}
551
- isGlobal={true}
552
- defaultZoomLevel={10} // Show 10 seconds by default
553
- isReplaceAllMode={true} // Prevent zoom changes during sync
554
- />
555
- </Box>
556
-
557
- {/* Segment Progress Section */}
558
- <SegmentProgressPanel
559
- currentSegments={segmentProgressProps.currentSegments}
560
- globalSegment={segmentProgressProps.globalSegment}
561
- syncWordIndex={segmentProgressProps.syncWordIndex}
562
- />
563
- </Box>
564
- )}
565
- </Box>
566
- )}
567
- </DialogContent>
568
-
569
- <DialogActions>
570
- {isReplaced && (
571
- <EditActionBar
572
- onReset={handleReset}
573
- onClose={handleClose}
574
- onSave={handleSave}
575
- editedSegment={globalSegment}
576
- isGlobal={true}
577
- />
578
- )}
579
- </DialogActions>
580
- </Dialog>
581
- )
582
- }
583
-
584
- // Memoized Segment Progress Item to prevent unnecessary re-renders
585
- const SegmentProgressItem = memo(({
586
- segment,
587
- index,
588
- isActive
589
- }: {
590
- segment: LyricsSegment
591
- index: number
592
- isActive: boolean
593
- }) => {
594
- const wordsWithTiming = segment.words.filter(w =>
595
- w.start_time !== null && w.end_time !== null
596
- ).length
597
- const totalWords = segment.words.length
598
- const isComplete = wordsWithTiming === totalWords
599
-
600
- return (
601
- <Paper
602
- key={segment.id}
603
- ref={isActive ? (el) => {
604
- // Auto-scroll to active segment
605
- if (el) {
606
- el.scrollIntoView({
607
- behavior: 'smooth',
608
- block: 'center'
609
- })
610
- }
611
- } : undefined}
612
- sx={{
613
- p: 1,
614
- mb: 1,
615
- bgcolor: isActive ? 'primary.light' :
616
- isComplete ? 'success.light' : 'background.paper',
617
- border: isActive ? 2 : 1,
618
- borderColor: isActive ? 'primary.main' : 'divider'
619
- }}
620
- >
621
- <Typography
622
- variant="body2"
623
- sx={{
624
- fontWeight: isActive ? 'bold' : 'normal',
625
- mb: 0.5
245
+ </DialogContent>
246
+
247
+ <DialogActions>
248
+ <Button onClick={handleClose} color="inherit">
249
+ Cancel
250
+ </Button>
251
+ <Button
252
+ variant="contained"
253
+ onClick={processLyrics}
254
+ disabled={!inputText.trim()}
255
+ startIcon={<AutoFixHighIcon />}
256
+ >
257
+ Continue to Sync
258
+ </Button>
259
+ </DialogActions>
260
+ </Dialog>
261
+
262
+ {/* Synchronizer Modal */}
263
+ <Dialog
264
+ open={open && mode === 'resync'}
265
+ onClose={handleClose}
266
+ maxWidth={false}
267
+ fullWidth
268
+ PaperProps={{
269
+ sx: {
270
+ height: '90vh',
271
+ margin: '5vh 2vw',
272
+ maxWidth: 'calc(100vw - 4vw)',
273
+ width: 'calc(100vw - 4vw)'
274
+ }
626
275
  }}
627
276
  >
628
- Segment {index + 1}: {segment.text.slice(0, 50)}
629
- {segment.text.length > 50 ? '...' : ''}
630
- </Typography>
631
- <Typography variant="caption" color="text.secondary">
632
- {wordsWithTiming}/{totalWords} words synced
633
- {isComplete && segment.start_time !== null && segment.end_time !== null && (
634
- <>
635
- <br />
636
- {segment.start_time.toFixed(2)}s - {segment.end_time.toFixed(2)}s
637
- </>
638
- )}
639
- </Typography>
640
- </Paper>
641
- )
642
- })
643
-
644
- // Memoized Segment Progress Panel
645
- const SegmentProgressPanel = memo(({
646
- currentSegments,
647
- globalSegment,
648
- syncWordIndex
649
- }: {
650
- currentSegments: LyricsSegment[]
651
- globalSegment: LyricsSegment | null
652
- syncWordIndex: number
653
- }) => {
654
- return (
655
- <Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
656
- <Typography variant="h6" gutterBottom>
657
- Segment Progress
658
- </Typography>
659
- <Box sx={{
660
- overflow: 'auto',
661
- flexGrow: 1,
662
- border: 1,
663
- borderColor: 'divider',
664
- borderRadius: 1,
665
- p: 1
666
- }}>
667
- {currentSegments.map((segment, index) => {
668
- const isActive = Boolean(
669
- globalSegment &&
670
- syncWordIndex >= 0 &&
671
- syncWordIndex < globalSegment.words.length &&
672
- globalSegment.words[syncWordIndex] &&
673
- segment.words.some(w => w.id === globalSegment.words[syncWordIndex].id)
674
- )
675
-
676
- return (
677
- <SegmentProgressItem
678
- key={segment.id}
679
- segment={segment}
680
- index={index}
681
- isActive={isActive}
277
+ <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
278
+ <IconButton onClick={handleBackToSelection} size="small">
279
+ <ArrowBackIcon />
280
+ </IconButton>
281
+ <Box sx={{ flex: 1 }}>
282
+ {newSegments.length > 0 ? 'Sync New Lyrics' : 'Re-sync Existing Lyrics'}
283
+ </Box>
284
+ <IconButton onClick={handleClose} sx={{ ml: 'auto' }}>
285
+ <CloseIcon />
286
+ </IconButton>
287
+ </DialogTitle>
288
+
289
+ <DialogContent
290
+ dividers
291
+ sx={{
292
+ display: 'flex',
293
+ flexDirection: 'column',
294
+ flexGrow: 1,
295
+ overflow: 'hidden',
296
+ p: 2
297
+ }}
298
+ >
299
+ {segmentsForSync.length > 0 ? (
300
+ <LyricsSynchronizer
301
+ segments={segmentsForSync}
302
+ currentTime={currentTime}
303
+ onPlaySegment={onPlaySegment}
304
+ onSave={handleSave}
305
+ onCancel={handleClose}
306
+ setModalSpacebarHandler={setModalSpacebarHandler}
682
307
  />
683
- )
684
- })}
685
- </Box>
686
- </Box>
308
+ ) : (
309
+ <Box sx={{
310
+ display: 'flex',
311
+ flexDirection: 'column',
312
+ alignItems: 'center',
313
+ justifyContent: 'center',
314
+ height: '100%',
315
+ gap: 2
316
+ }}>
317
+ <Typography variant="h6" color="text.secondary">
318
+ No lyrics to sync
319
+ </Typography>
320
+ <Typography variant="body2" color="text.secondary">
321
+ Go back and paste new lyrics, or close this modal.
322
+ </Typography>
323
+ <Button
324
+ variant="outlined"
325
+ onClick={handleBackToSelection}
326
+ startIcon={<ArrowBackIcon />}
327
+ >
328
+ Back to Selection
329
+ </Button>
330
+ </Box>
331
+ )}
332
+ </DialogContent>
333
+ </Dialog>
334
+ </>
687
335
  )
688
- })
336
+ }