lyrics-transcriber 0.43.0__py3-none-any.whl → 0.44.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.
- lyrics_transcriber/core/controller.py +58 -24
- lyrics_transcriber/correction/anchor_sequence.py +22 -8
- lyrics_transcriber/correction/corrector.py +47 -3
- lyrics_transcriber/correction/handlers/llm.py +15 -12
- lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
- lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
- lyrics_transcriber/frontend/dist/assets/{index-D0Gr3Ep7.js → index-DVoI6Z16.js} +10799 -7490
- lyrics_transcriber/frontend/dist/assets/index-DVoI6Z16.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/App.tsx +4 -4
- lyrics_transcriber/frontend/src/api.ts +37 -0
- lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +14 -10
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +62 -56
- lyrics_transcriber/frontend/src/components/EditModal.tsx +232 -237
- lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
- lyrics_transcriber/frontend/src/components/GlobalSyncEditor.tsx +675 -0
- lyrics_transcriber/frontend/src/components/Header.tsx +141 -101
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +146 -80
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +22 -13
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +1 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +29 -12
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +21 -4
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +29 -15
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +34 -16
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +186 -0
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +89 -41
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +9 -2
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +28 -3
- lyrics_transcriber/frontend/src/components/shared/types.ts +17 -2
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +63 -14
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +192 -0
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +267 -0
- lyrics_transcriber/frontend/src/main.tsx +7 -1
- lyrics_transcriber/frontend/src/theme.ts +177 -0
- lyrics_transcriber/frontend/src/types.ts +1 -1
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/lyrics/base_lyrics_provider.py +2 -2
- lyrics_transcriber/lyrics/user_input_provider.py +44 -0
- lyrics_transcriber/output/generator.py +40 -12
- lyrics_transcriber/output/video.py +18 -8
- lyrics_transcriber/review/server.py +238 -8
- {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/METADATA +3 -2
- {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/RECORD +47 -41
- lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +0 -1
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +0 -252
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +0 -110
- {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/WHEEL +0 -0
- {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/entry_points.txt +0 -0
@@ -58,6 +58,11 @@ export interface WordProps {
|
|
58
58
|
isCurrentlyPlaying?: boolean
|
59
59
|
padding?: string
|
60
60
|
onClick?: () => void
|
61
|
+
correction?: {
|
62
|
+
originalWord: string
|
63
|
+
handler: string
|
64
|
+
confidence: number
|
65
|
+
} | null
|
61
66
|
}
|
62
67
|
|
63
68
|
// Text segment props
|
@@ -100,11 +105,21 @@ export interface ReferenceViewProps extends BaseViewProps {
|
|
100
105
|
// Update HighlightedTextProps to include linePositions
|
101
106
|
export interface HighlightedTextProps extends BaseViewProps {
|
102
107
|
text?: string
|
103
|
-
|
108
|
+
segments?: LyricsSegment[]
|
109
|
+
wordPositions: TranscriptionWordPosition[] | ReferenceWordPosition[]
|
104
110
|
anchors: AnchorSequence[]
|
105
|
-
|
111
|
+
highlightInfo: HighlightInfo | null
|
112
|
+
mode: InteractionMode
|
113
|
+
onElementClick: (content: ModalContent) => void
|
114
|
+
onWordClick?: (info: WordClickInfo) => void
|
115
|
+
flashingType: FlashType
|
106
116
|
isReference?: boolean
|
107
117
|
currentSource?: string
|
108
118
|
preserveSegments?: boolean
|
109
119
|
linePositions?: LinePosition[]
|
120
|
+
currentTime?: number
|
121
|
+
referenceCorrections?: Map<string, string>
|
122
|
+
gaps?: GapSequence[]
|
123
|
+
flashingHandler?: string | null
|
124
|
+
corrections?: WordCorrection[]
|
110
125
|
}
|
@@ -4,7 +4,7 @@ let isModalOpen = false
|
|
4
4
|
|
5
5
|
type KeyboardState = {
|
6
6
|
setIsShiftPressed: (value: boolean) => void
|
7
|
-
setIsCtrlPressed
|
7
|
+
setIsCtrlPressed?: (value: boolean) => void
|
8
8
|
modalHandler?: {
|
9
9
|
isOpen: boolean
|
10
10
|
onSpacebar?: (e: KeyboardEvent) => void
|
@@ -13,6 +13,14 @@ type KeyboardState = {
|
|
13
13
|
|
14
14
|
// Add functions to update the modal handler state
|
15
15
|
export const setModalHandler = (handler: ((e: KeyboardEvent) => void) | undefined, open: boolean) => {
|
16
|
+
console.log('setModalHandler called', {
|
17
|
+
hasHandler: !!handler,
|
18
|
+
open,
|
19
|
+
previousState: {
|
20
|
+
hadHandler: !!currentModalHandler,
|
21
|
+
wasOpen: isModalOpen
|
22
|
+
}
|
23
|
+
})
|
16
24
|
currentModalHandler = handler
|
17
25
|
isModalOpen = open
|
18
26
|
}
|
@@ -22,6 +30,17 @@ export const setupKeyboardHandlers = (state: KeyboardState) => {
|
|
22
30
|
console.log(`Setting up keyboard handlers [${handlerId}]`)
|
23
31
|
|
24
32
|
const handleKeyDown = (e: KeyboardEvent) => {
|
33
|
+
console.log(`Keyboard event captured [${handlerId}]`, {
|
34
|
+
key: e.key,
|
35
|
+
code: e.code,
|
36
|
+
type: e.type,
|
37
|
+
target: e.target,
|
38
|
+
currentTarget: e.currentTarget,
|
39
|
+
eventPhase: e.eventPhase,
|
40
|
+
isModalOpen,
|
41
|
+
hasModalHandler: !!currentModalHandler
|
42
|
+
})
|
43
|
+
|
25
44
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
26
45
|
console.log(`[${handlerId}] Ignoring keydown in input/textarea`)
|
27
46
|
return
|
@@ -31,37 +50,67 @@ export const setupKeyboardHandlers = (state: KeyboardState) => {
|
|
31
50
|
state.setIsShiftPressed(true)
|
32
51
|
document.body.style.userSelect = 'none'
|
33
52
|
} else if (e.key === 'Meta') {
|
34
|
-
state.setIsCtrlPressed(true)
|
53
|
+
state.setIsCtrlPressed?.(true)
|
35
54
|
} else if (e.key === ' ' || e.code === 'Space') {
|
36
|
-
console.log(
|
55
|
+
console.log('Keyboard handler - Spacebar pressed down', {
|
37
56
|
modalOpen: isModalOpen,
|
38
57
|
hasModalHandler: !!currentModalHandler,
|
39
|
-
hasGlobalToggle: !!window.toggleAudioPlayback
|
58
|
+
hasGlobalToggle: !!window.toggleAudioPlayback,
|
59
|
+
target: e.target,
|
60
|
+
eventPhase: e.eventPhase,
|
61
|
+
handlerFunction: currentModalHandler?.toString().slice(0, 100)
|
40
62
|
})
|
41
|
-
|
63
|
+
|
42
64
|
e.preventDefault()
|
43
|
-
|
44
|
-
// If modal is open and has a handler, use that
|
65
|
+
|
45
66
|
if (isModalOpen && currentModalHandler) {
|
46
|
-
console.log(
|
67
|
+
console.log('Keyboard handler - Delegating to modal handler')
|
47
68
|
currentModalHandler(e)
|
48
|
-
}
|
49
|
-
|
50
|
-
else if (window.toggleAudioPlayback && !isModalOpen) {
|
51
|
-
console.log(`[${handlerId}] Using global audio toggle`)
|
69
|
+
} else if (window.toggleAudioPlayback && !isModalOpen) {
|
70
|
+
console.log('Keyboard handler - Using global audio toggle')
|
52
71
|
window.toggleAudioPlayback()
|
53
72
|
}
|
54
73
|
}
|
55
74
|
}
|
56
75
|
|
57
76
|
const handleKeyUp = (e: KeyboardEvent) => {
|
77
|
+
console.log(`Keyboard up event captured [${handlerId}]`, {
|
78
|
+
key: e.key,
|
79
|
+
code: e.code,
|
80
|
+
type: e.type,
|
81
|
+
target: e.target,
|
82
|
+
eventPhase: e.eventPhase,
|
83
|
+
isModalOpen,
|
84
|
+
hasModalHandler: !!currentModalHandler
|
85
|
+
})
|
86
|
+
|
58
87
|
if (e.key === 'Shift') {
|
59
88
|
state.setIsShiftPressed(false)
|
60
89
|
document.body.style.userSelect = ''
|
61
90
|
} else if (e.key === 'Meta') {
|
62
|
-
state.setIsCtrlPressed(false)
|
91
|
+
state.setIsCtrlPressed?.(false)
|
92
|
+
} else if (e.key === ' ' || e.code === 'Space') {
|
93
|
+
console.log('Keyboard handler - Spacebar released', {
|
94
|
+
modalOpen: isModalOpen,
|
95
|
+
hasModalHandler: !!currentModalHandler,
|
96
|
+
target: e.target,
|
97
|
+
eventPhase: e.eventPhase
|
98
|
+
})
|
99
|
+
|
100
|
+
e.preventDefault()
|
101
|
+
|
102
|
+
if (isModalOpen && currentModalHandler) {
|
103
|
+
console.log('Keyboard handler - Delegating keyup to modal handler')
|
104
|
+
currentModalHandler(e)
|
105
|
+
}
|
63
106
|
}
|
64
107
|
}
|
65
108
|
|
66
109
|
return { handleKeyDown, handleKeyUp }
|
67
|
-
}
|
110
|
+
}
|
111
|
+
|
112
|
+
// Export these for external use
|
113
|
+
export const getModalState = () => ({
|
114
|
+
currentModalHandler,
|
115
|
+
isModalOpen
|
116
|
+
})
|
@@ -117,5 +117,197 @@ export const updateSegment = (
|
|
117
117
|
|
118
118
|
newData.corrected_segments[segmentIndex] = updatedSegment
|
119
119
|
|
120
|
+
return newData
|
121
|
+
}
|
122
|
+
|
123
|
+
export function mergeSegment(data: CorrectionData, segmentIndex: number, mergeWithNext: boolean): CorrectionData {
|
124
|
+
const segments = [...data.corrected_segments]
|
125
|
+
const targetIndex = mergeWithNext ? segmentIndex + 1 : segmentIndex - 1
|
126
|
+
|
127
|
+
// Check if target segment exists
|
128
|
+
if (targetIndex < 0 || targetIndex >= segments.length) {
|
129
|
+
return data
|
130
|
+
}
|
131
|
+
|
132
|
+
const baseSegment = segments[segmentIndex]
|
133
|
+
const targetSegment = segments[targetIndex]
|
134
|
+
|
135
|
+
// Create merged segment
|
136
|
+
const mergedSegment: LyricsSegment = {
|
137
|
+
id: nanoid(),
|
138
|
+
words: mergeWithNext
|
139
|
+
? [...baseSegment.words, ...targetSegment.words]
|
140
|
+
: [...targetSegment.words, ...baseSegment.words],
|
141
|
+
text: mergeWithNext
|
142
|
+
? `${baseSegment.text} ${targetSegment.text}`
|
143
|
+
: `${targetSegment.text} ${baseSegment.text}`,
|
144
|
+
start_time: Math.min(
|
145
|
+
baseSegment.start_time ?? Infinity,
|
146
|
+
targetSegment.start_time ?? Infinity
|
147
|
+
),
|
148
|
+
end_time: Math.max(
|
149
|
+
baseSegment.end_time ?? -Infinity,
|
150
|
+
targetSegment.end_time ?? -Infinity
|
151
|
+
)
|
152
|
+
}
|
153
|
+
|
154
|
+
// Replace the two segments with the merged one
|
155
|
+
const minIndex = Math.min(segmentIndex, targetIndex)
|
156
|
+
segments.splice(minIndex, 2, mergedSegment)
|
157
|
+
|
158
|
+
return {
|
159
|
+
...data,
|
160
|
+
corrected_segments: segments
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
export function findAndReplace(
|
165
|
+
data: CorrectionData,
|
166
|
+
findText: string,
|
167
|
+
replaceText: string,
|
168
|
+
options: { caseSensitive: boolean, useRegex: boolean, fullTextMode?: boolean } = {
|
169
|
+
caseSensitive: false,
|
170
|
+
useRegex: false,
|
171
|
+
fullTextMode: false
|
172
|
+
}
|
173
|
+
): CorrectionData {
|
174
|
+
const newData = { ...data }
|
175
|
+
|
176
|
+
// If full text mode is enabled, perform replacements across word boundaries
|
177
|
+
if (options.fullTextMode) {
|
178
|
+
newData.corrected_segments = data.corrected_segments.map(segment => {
|
179
|
+
// Create a pattern for the full segment text
|
180
|
+
let pattern: RegExp
|
181
|
+
|
182
|
+
if (options.useRegex) {
|
183
|
+
pattern = new RegExp(findText, options.caseSensitive ? 'g' : 'gi')
|
184
|
+
} else {
|
185
|
+
const escapedFindText = findText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
186
|
+
pattern = new RegExp(escapedFindText, options.caseSensitive ? 'g' : 'gi')
|
187
|
+
}
|
188
|
+
|
189
|
+
// Get the full segment text
|
190
|
+
const segmentText = segment.text
|
191
|
+
|
192
|
+
// If no matches, return the segment unchanged
|
193
|
+
if (!pattern.test(segmentText)) {
|
194
|
+
return segment
|
195
|
+
}
|
196
|
+
|
197
|
+
// Reset pattern for replacement
|
198
|
+
pattern.lastIndex = 0
|
199
|
+
|
200
|
+
// Replace in the full segment text
|
201
|
+
const newSegmentText = segmentText.replace(pattern, replaceText)
|
202
|
+
|
203
|
+
// Split the new text into words
|
204
|
+
const newWordTexts = newSegmentText.trim().split(/\s+/).filter(text => text.length > 0)
|
205
|
+
|
206
|
+
// Create new word objects
|
207
|
+
// We'll try to preserve original word IDs and timing info where possible
|
208
|
+
const newWords = []
|
209
|
+
|
210
|
+
// If we have the same number of words, we can preserve IDs and timing
|
211
|
+
if (newWordTexts.length === segment.words.length) {
|
212
|
+
for (let i = 0; i < newWordTexts.length; i++) {
|
213
|
+
newWords.push({
|
214
|
+
...segment.words[i],
|
215
|
+
text: newWordTexts[i]
|
216
|
+
})
|
217
|
+
}
|
218
|
+
}
|
219
|
+
// If we have fewer words than before, some words were removed
|
220
|
+
else if (newWordTexts.length < segment.words.length) {
|
221
|
+
// Try to map new words to old words
|
222
|
+
let oldWordIndex = 0
|
223
|
+
for (let i = 0; i < newWordTexts.length; i++) {
|
224
|
+
// Find the next non-empty old word
|
225
|
+
while (oldWordIndex < segment.words.length &&
|
226
|
+
segment.words[oldWordIndex].text.trim() === '') {
|
227
|
+
oldWordIndex++
|
228
|
+
}
|
229
|
+
|
230
|
+
if (oldWordIndex < segment.words.length) {
|
231
|
+
newWords.push({
|
232
|
+
...segment.words[oldWordIndex],
|
233
|
+
text: newWordTexts[i]
|
234
|
+
})
|
235
|
+
oldWordIndex++
|
236
|
+
} else {
|
237
|
+
// If we run out of old words, create new ones
|
238
|
+
newWords.push({
|
239
|
+
id: nanoid(),
|
240
|
+
text: newWordTexts[i],
|
241
|
+
start_time: null,
|
242
|
+
end_time: null
|
243
|
+
})
|
244
|
+
}
|
245
|
+
}
|
246
|
+
}
|
247
|
+
// If we have more words than before, some words were added
|
248
|
+
else {
|
249
|
+
// Try to preserve original words where possible
|
250
|
+
for (let i = 0; i < newWordTexts.length; i++) {
|
251
|
+
if (i < segment.words.length) {
|
252
|
+
newWords.push({
|
253
|
+
...segment.words[i],
|
254
|
+
text: newWordTexts[i]
|
255
|
+
})
|
256
|
+
} else {
|
257
|
+
// For new words, create new IDs
|
258
|
+
newWords.push({
|
259
|
+
id: nanoid(),
|
260
|
+
text: newWordTexts[i],
|
261
|
+
start_time: null,
|
262
|
+
end_time: null
|
263
|
+
})
|
264
|
+
}
|
265
|
+
}
|
266
|
+
}
|
267
|
+
|
268
|
+
return {
|
269
|
+
...segment,
|
270
|
+
words: newWords,
|
271
|
+
text: newSegmentText
|
272
|
+
}
|
273
|
+
})
|
274
|
+
}
|
275
|
+
// Word-level replacement (original implementation)
|
276
|
+
else {
|
277
|
+
newData.corrected_segments = data.corrected_segments.map(segment => {
|
278
|
+
// Replace in each word
|
279
|
+
let newWords = segment.words.map(word => {
|
280
|
+
let pattern: RegExp
|
281
|
+
|
282
|
+
if (options.useRegex) {
|
283
|
+
// Create regex with or without case sensitivity
|
284
|
+
pattern = new RegExp(findText, options.caseSensitive ? 'g' : 'gi')
|
285
|
+
} else {
|
286
|
+
// Escape special regex characters for literal search
|
287
|
+
const escapedFindText = findText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
288
|
+
pattern = new RegExp(escapedFindText, options.caseSensitive ? 'g' : 'gi')
|
289
|
+
}
|
290
|
+
|
291
|
+
return {
|
292
|
+
...word,
|
293
|
+
text: word.text.replace(pattern, replaceText)
|
294
|
+
}
|
295
|
+
});
|
296
|
+
|
297
|
+
// Filter out words that have become empty
|
298
|
+
newWords = newWords.filter(word => word.text.trim() !== '');
|
299
|
+
|
300
|
+
// Update segment text
|
301
|
+
return {
|
302
|
+
...segment,
|
303
|
+
words: newWords,
|
304
|
+
text: newWords.map(w => w.text).join(' ')
|
305
|
+
}
|
306
|
+
});
|
307
|
+
}
|
308
|
+
|
309
|
+
// Filter out segments that have no words left
|
310
|
+
newData.corrected_segments = newData.corrected_segments.filter(segment => segment.words.length > 0);
|
311
|
+
|
120
312
|
return newData
|
121
313
|
}
|
@@ -0,0 +1,267 @@
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from 'react'
|
2
|
+
import { LyricsSegment, Word } from '../types'
|
3
|
+
|
4
|
+
interface UseManualSyncProps {
|
5
|
+
editedSegment: LyricsSegment | null
|
6
|
+
currentTime: number
|
7
|
+
onPlaySegment?: (startTime: number) => void
|
8
|
+
updateSegment: (words: Word[]) => void
|
9
|
+
}
|
10
|
+
|
11
|
+
// Constants for tap detection
|
12
|
+
const TAP_THRESHOLD_MS = 200 // If spacebar is pressed for less than this time, it's considered a tap
|
13
|
+
const DEFAULT_WORD_DURATION = 1.0 // Default duration in seconds when tapping
|
14
|
+
const OVERLAP_BUFFER = 0.01 // Buffer to prevent word overlap (10ms)
|
15
|
+
|
16
|
+
export default function useManualSync({
|
17
|
+
editedSegment,
|
18
|
+
currentTime,
|
19
|
+
onPlaySegment,
|
20
|
+
updateSegment
|
21
|
+
}: UseManualSyncProps) {
|
22
|
+
const [isManualSyncing, setIsManualSyncing] = useState(false)
|
23
|
+
const [syncWordIndex, setSyncWordIndex] = useState<number>(-1)
|
24
|
+
const currentTimeRef = useRef(currentTime)
|
25
|
+
const [isSpacebarPressed, setIsSpacebarPressed] = useState(false)
|
26
|
+
const wordStartTimeRef = useRef<number | null>(null)
|
27
|
+
const wordsRef = useRef<Word[]>([])
|
28
|
+
const spacebarPressTimeRef = useRef<number | null>(null)
|
29
|
+
|
30
|
+
// Keep currentTimeRef up to date
|
31
|
+
useEffect(() => {
|
32
|
+
currentTimeRef.current = currentTime
|
33
|
+
}, [currentTime])
|
34
|
+
|
35
|
+
// Keep wordsRef up to date
|
36
|
+
useEffect(() => {
|
37
|
+
if (editedSegment) {
|
38
|
+
wordsRef.current = [...editedSegment.words]
|
39
|
+
}
|
40
|
+
}, [editedSegment])
|
41
|
+
|
42
|
+
const cleanupManualSync = useCallback(() => {
|
43
|
+
setIsManualSyncing(false)
|
44
|
+
setSyncWordIndex(-1)
|
45
|
+
setIsSpacebarPressed(false)
|
46
|
+
wordStartTimeRef.current = null
|
47
|
+
spacebarPressTimeRef.current = null
|
48
|
+
}, [])
|
49
|
+
|
50
|
+
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
51
|
+
if (e.code !== 'Space') return
|
52
|
+
|
53
|
+
console.log('useManualSync - Spacebar pressed down', {
|
54
|
+
isManualSyncing,
|
55
|
+
hasEditedSegment: !!editedSegment,
|
56
|
+
syncWordIndex,
|
57
|
+
currentTime: currentTimeRef.current
|
58
|
+
})
|
59
|
+
|
60
|
+
e.preventDefault()
|
61
|
+
e.stopPropagation()
|
62
|
+
|
63
|
+
if (isManualSyncing && editedSegment && !isSpacebarPressed) {
|
64
|
+
const currentWord = syncWordIndex < editedSegment.words.length ? editedSegment.words[syncWordIndex] : null
|
65
|
+
console.log('useManualSync - Recording word start time', {
|
66
|
+
wordIndex: syncWordIndex,
|
67
|
+
wordText: currentWord?.text,
|
68
|
+
time: currentTimeRef.current
|
69
|
+
})
|
70
|
+
|
71
|
+
setIsSpacebarPressed(true)
|
72
|
+
|
73
|
+
// Record the start time of the current word
|
74
|
+
wordStartTimeRef.current = currentTimeRef.current
|
75
|
+
|
76
|
+
// Record when the spacebar was pressed (for tap detection)
|
77
|
+
spacebarPressTimeRef.current = Date.now()
|
78
|
+
|
79
|
+
// Update the word's start time immediately
|
80
|
+
if (syncWordIndex < editedSegment.words.length) {
|
81
|
+
const newWords = [...wordsRef.current]
|
82
|
+
const currentWord = newWords[syncWordIndex]
|
83
|
+
|
84
|
+
// Set the start time for the current word
|
85
|
+
currentWord.start_time = currentTimeRef.current
|
86
|
+
|
87
|
+
// Update our ref
|
88
|
+
wordsRef.current = newWords
|
89
|
+
|
90
|
+
// Update the segment
|
91
|
+
updateSegment(newWords)
|
92
|
+
}
|
93
|
+
} else if (!isManualSyncing && editedSegment && onPlaySegment) {
|
94
|
+
console.log('useManualSync - Handling segment playback')
|
95
|
+
// Toggle segment playback when not in manual sync mode
|
96
|
+
const startTime = editedSegment.start_time ?? 0
|
97
|
+
const endTime = editedSegment.end_time ?? 0
|
98
|
+
|
99
|
+
if (currentTimeRef.current >= startTime && currentTimeRef.current <= endTime) {
|
100
|
+
if (window.toggleAudioPlayback) {
|
101
|
+
window.toggleAudioPlayback()
|
102
|
+
}
|
103
|
+
} else {
|
104
|
+
onPlaySegment(startTime)
|
105
|
+
}
|
106
|
+
}
|
107
|
+
}, [isManualSyncing, editedSegment, syncWordIndex, onPlaySegment, updateSegment, isSpacebarPressed])
|
108
|
+
|
109
|
+
const handleKeyUp = useCallback((e: KeyboardEvent) => {
|
110
|
+
if (e.code !== 'Space') return
|
111
|
+
|
112
|
+
console.log('useManualSync - Spacebar released', {
|
113
|
+
isManualSyncing,
|
114
|
+
hasEditedSegment: !!editedSegment,
|
115
|
+
syncWordIndex,
|
116
|
+
currentTime: currentTimeRef.current,
|
117
|
+
wordStartTime: wordStartTimeRef.current
|
118
|
+
})
|
119
|
+
|
120
|
+
e.preventDefault()
|
121
|
+
e.stopPropagation()
|
122
|
+
|
123
|
+
if (isManualSyncing && editedSegment && isSpacebarPressed) {
|
124
|
+
const currentWord = syncWordIndex < editedSegment.words.length ? editedSegment.words[syncWordIndex] : null
|
125
|
+
const pressDuration = spacebarPressTimeRef.current ? Date.now() - spacebarPressTimeRef.current : 0
|
126
|
+
const isTap = pressDuration < TAP_THRESHOLD_MS
|
127
|
+
|
128
|
+
console.log('useManualSync - Recording word end time', {
|
129
|
+
wordIndex: syncWordIndex,
|
130
|
+
wordText: currentWord?.text,
|
131
|
+
startTime: wordStartTimeRef.current,
|
132
|
+
endTime: currentTimeRef.current,
|
133
|
+
pressDuration: `${pressDuration}ms`,
|
134
|
+
isTap,
|
135
|
+
duration: currentWord ? (currentTimeRef.current - (wordStartTimeRef.current || 0)).toFixed(2) + 's' : 'N/A'
|
136
|
+
})
|
137
|
+
|
138
|
+
setIsSpacebarPressed(false)
|
139
|
+
|
140
|
+
if (syncWordIndex < editedSegment.words.length) {
|
141
|
+
const newWords = [...wordsRef.current]
|
142
|
+
const currentWord = newWords[syncWordIndex]
|
143
|
+
|
144
|
+
// Set the end time for the current word based on whether it was a tap or hold
|
145
|
+
if (isTap) {
|
146
|
+
// For a tap, set a default duration
|
147
|
+
const defaultEndTime = (wordStartTimeRef.current || currentTimeRef.current) + DEFAULT_WORD_DURATION
|
148
|
+
currentWord.end_time = defaultEndTime
|
149
|
+
console.log('useManualSync - Tap detected, setting default duration', {
|
150
|
+
defaultEndTime,
|
151
|
+
duration: DEFAULT_WORD_DURATION
|
152
|
+
})
|
153
|
+
} else {
|
154
|
+
// For a hold, use the current time as the end time
|
155
|
+
currentWord.end_time = currentTimeRef.current
|
156
|
+
}
|
157
|
+
|
158
|
+
// Update our ref
|
159
|
+
wordsRef.current = newWords
|
160
|
+
|
161
|
+
// Move to the next word
|
162
|
+
if (syncWordIndex === editedSegment.words.length - 1) {
|
163
|
+
// If this was the last word, finish manual sync
|
164
|
+
console.log('useManualSync - Completed manual sync for all words')
|
165
|
+
setIsManualSyncing(false)
|
166
|
+
setSyncWordIndex(-1)
|
167
|
+
wordStartTimeRef.current = null
|
168
|
+
spacebarPressTimeRef.current = null
|
169
|
+
} else {
|
170
|
+
// Otherwise, move to the next word
|
171
|
+
const nextWord = editedSegment.words[syncWordIndex + 1]
|
172
|
+
console.log('useManualSync - Moving to next word', {
|
173
|
+
nextWordIndex: syncWordIndex + 1,
|
174
|
+
nextWordText: nextWord?.text
|
175
|
+
})
|
176
|
+
setSyncWordIndex(syncWordIndex + 1)
|
177
|
+
}
|
178
|
+
|
179
|
+
// Update the segment
|
180
|
+
updateSegment(newWords)
|
181
|
+
}
|
182
|
+
}
|
183
|
+
}, [isManualSyncing, editedSegment, syncWordIndex, updateSegment, isSpacebarPressed])
|
184
|
+
|
185
|
+
// Add a handler for when the next word starts to adjust previous word's end time if needed
|
186
|
+
useEffect(() => {
|
187
|
+
if (isManualSyncing && editedSegment && syncWordIndex > 0) {
|
188
|
+
const newWords = [...wordsRef.current]
|
189
|
+
const prevWord = newWords[syncWordIndex - 1]
|
190
|
+
const currentWord = newWords[syncWordIndex]
|
191
|
+
|
192
|
+
// If the previous word's end time overlaps with the current word's start time,
|
193
|
+
// adjust the previous word's end time
|
194
|
+
if (prevWord && currentWord &&
|
195
|
+
prevWord.end_time !== null && currentWord.start_time !== null &&
|
196
|
+
prevWord.end_time > currentWord.start_time) {
|
197
|
+
|
198
|
+
console.log('useManualSync - Adjusting previous word end time to prevent overlap', {
|
199
|
+
prevWordIndex: syncWordIndex - 1,
|
200
|
+
prevWordText: prevWord.text,
|
201
|
+
prevWordEndTime: prevWord.end_time,
|
202
|
+
currentWordStartTime: currentWord.start_time,
|
203
|
+
newEndTime: currentWord.start_time - OVERLAP_BUFFER
|
204
|
+
})
|
205
|
+
|
206
|
+
prevWord.end_time = currentWord.start_time - OVERLAP_BUFFER
|
207
|
+
|
208
|
+
// Update our ref
|
209
|
+
wordsRef.current = newWords
|
210
|
+
|
211
|
+
// Update the segment
|
212
|
+
updateSegment(newWords)
|
213
|
+
}
|
214
|
+
}
|
215
|
+
}, [syncWordIndex, isManualSyncing, editedSegment, updateSegment])
|
216
|
+
|
217
|
+
// Combine the key handlers into a single function for external use
|
218
|
+
const handleSpacebar = useCallback((e: KeyboardEvent) => {
|
219
|
+
if (e.type === 'keydown') {
|
220
|
+
handleKeyDown(e)
|
221
|
+
} else if (e.type === 'keyup') {
|
222
|
+
handleKeyUp(e)
|
223
|
+
}
|
224
|
+
}, [handleKeyDown, handleKeyUp])
|
225
|
+
|
226
|
+
const startManualSync = useCallback(() => {
|
227
|
+
if (isManualSyncing) {
|
228
|
+
cleanupManualSync()
|
229
|
+
return
|
230
|
+
}
|
231
|
+
|
232
|
+
if (!editedSegment || !onPlaySegment) return
|
233
|
+
|
234
|
+
// Make sure we have the latest words
|
235
|
+
wordsRef.current = [...editedSegment.words]
|
236
|
+
|
237
|
+
setIsManualSyncing(true)
|
238
|
+
setSyncWordIndex(0)
|
239
|
+
setIsSpacebarPressed(false)
|
240
|
+
wordStartTimeRef.current = null
|
241
|
+
spacebarPressTimeRef.current = null
|
242
|
+
// Start playing 3 seconds before segment start
|
243
|
+
onPlaySegment((editedSegment.start_time ?? 0) - 3)
|
244
|
+
}, [isManualSyncing, editedSegment, onPlaySegment, cleanupManualSync])
|
245
|
+
|
246
|
+
// Auto-stop sync if we go past the end time
|
247
|
+
useEffect(() => {
|
248
|
+
if (!editedSegment) return
|
249
|
+
|
250
|
+
const endTime = editedSegment.end_time ?? 0
|
251
|
+
|
252
|
+
if (window.isAudioPlaying && currentTimeRef.current > endTime) {
|
253
|
+
console.log('Stopping playback: current time exceeded end time')
|
254
|
+
window.toggleAudioPlayback?.()
|
255
|
+
cleanupManualSync()
|
256
|
+
}
|
257
|
+
}, [isManualSyncing, editedSegment, currentTimeRef, cleanupManualSync])
|
258
|
+
|
259
|
+
return {
|
260
|
+
isManualSyncing,
|
261
|
+
syncWordIndex,
|
262
|
+
startManualSync,
|
263
|
+
cleanupManualSync,
|
264
|
+
handleSpacebar,
|
265
|
+
isSpacebarPressed
|
266
|
+
}
|
267
|
+
}
|
@@ -1,6 +1,12 @@
|
|
1
1
|
import ReactDOM from 'react-dom/client'
|
2
|
+
import { ThemeProvider } from '@mui/material/styles'
|
3
|
+
import CssBaseline from '@mui/material/CssBaseline'
|
2
4
|
import App from './App'
|
5
|
+
import theme from './theme'
|
3
6
|
|
4
7
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
5
|
-
<
|
8
|
+
<ThemeProvider theme={theme}>
|
9
|
+
<CssBaseline />
|
10
|
+
<App />
|
11
|
+
</ThemeProvider>
|
6
12
|
)
|