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.
- karaoke_gen/__init__.py +32 -1
- karaoke_gen/audio_fetcher.py +476 -56
- karaoke_gen/audio_processor.py +11 -3
- karaoke_gen/instrumental_review/server.py +154 -860
- karaoke_gen/instrumental_review/static/index.html +1506 -0
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +62 -1
- karaoke_gen/karaoke_gen.py +114 -1
- karaoke_gen/lyrics_processor.py +81 -4
- karaoke_gen/utils/bulk_cli.py +3 -0
- karaoke_gen/utils/cli_args.py +4 -2
- karaoke_gen/utils/gen_cli.py +196 -5
- karaoke_gen/utils/remote_cli.py +523 -34
- {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.16.dist-info}/METADATA +4 -1
- {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.16.dist-info}/RECORD +31 -25
- lyrics_transcriber/frontend/package.json +1 -1
- lyrics_transcriber/frontend/src/components/Header.tsx +38 -12
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +17 -3
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +185 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +704 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +80 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +905 -0
- lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +127 -0
- lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +190 -542
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/web_assets/assets/{index-DdJTDWH3.js → index-COYImAcx.js} +1722 -489
- lyrics_transcriber/frontend/web_assets/assets/index-COYImAcx.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/index.html +1 -1
- lyrics_transcriber/review/server.py +5 -5
- lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js.map +0 -1
- {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.16.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.16.dist-info}/entry_points.txt +0 -0
- {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
|
|
17
|
+
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
19
18
|
import { nanoid } from 'nanoid'
|
|
20
|
-
import
|
|
21
|
-
import
|
|
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 [
|
|
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
|
-
//
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
// Reset state when modal opens
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (open) {
|
|
59
|
+
setMode('selection')
|
|
60
|
+
setInputText('')
|
|
61
|
+
setNewSegments([])
|
|
61
62
|
}
|
|
62
|
-
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
121
|
+
setMode('selection')
|
|
277
122
|
setInputText('')
|
|
278
|
-
|
|
279
|
-
setGlobalSegment(null)
|
|
280
|
-
setOriginalSegments([])
|
|
281
|
-
setCurrentSegments([])
|
|
123
|
+
setNewSegments([])
|
|
282
124
|
onClose()
|
|
283
|
-
}, [onClose
|
|
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
|
-
|
|
127
|
+
// Handle save from synchronizer
|
|
128
|
+
const handleSave = useCallback((segments: LyricsSegment[]) => {
|
|
129
|
+
onSave(segments)
|
|
323
130
|
handleClose()
|
|
324
|
-
}, [
|
|
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
|
-
//
|
|
367
|
-
const
|
|
368
|
-
|
|
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
|
-
|
|
138
|
+
const handleSelectResync = useCallback(() => {
|
|
139
|
+
setMode('resync')
|
|
140
|
+
}, [])
|
|
386
141
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
//
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
//
|
|
407
|
-
const
|
|
408
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
{
|
|
453
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
686
|
-
|
|
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
|
+
}
|