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.
Files changed (50) hide show
  1. lyrics_transcriber/core/controller.py +58 -24
  2. lyrics_transcriber/correction/anchor_sequence.py +22 -8
  3. lyrics_transcriber/correction/corrector.py +47 -3
  4. lyrics_transcriber/correction/handlers/llm.py +15 -12
  5. lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
  6. lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
  7. lyrics_transcriber/frontend/dist/assets/{index-D0Gr3Ep7.js → index-DVoI6Z16.js} +10799 -7490
  8. lyrics_transcriber/frontend/dist/assets/index-DVoI6Z16.js.map +1 -0
  9. lyrics_transcriber/frontend/dist/index.html +1 -1
  10. lyrics_transcriber/frontend/src/App.tsx +4 -4
  11. lyrics_transcriber/frontend/src/api.ts +37 -0
  12. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
  13. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +14 -10
  14. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +62 -56
  15. lyrics_transcriber/frontend/src/components/EditModal.tsx +232 -237
  16. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  17. lyrics_transcriber/frontend/src/components/GlobalSyncEditor.tsx +675 -0
  18. lyrics_transcriber/frontend/src/components/Header.tsx +141 -101
  19. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +146 -80
  20. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +22 -13
  21. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +1 -0
  22. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +29 -12
  23. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +21 -4
  24. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +29 -15
  25. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +34 -16
  26. lyrics_transcriber/frontend/src/components/WordDivider.tsx +186 -0
  27. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +89 -41
  28. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +9 -2
  29. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +28 -3
  30. lyrics_transcriber/frontend/src/components/shared/types.ts +17 -2
  31. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +63 -14
  32. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +192 -0
  33. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +267 -0
  34. lyrics_transcriber/frontend/src/main.tsx +7 -1
  35. lyrics_transcriber/frontend/src/theme.ts +177 -0
  36. lyrics_transcriber/frontend/src/types.ts +1 -1
  37. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  38. lyrics_transcriber/lyrics/base_lyrics_provider.py +2 -2
  39. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  40. lyrics_transcriber/output/generator.py +40 -12
  41. lyrics_transcriber/output/video.py +18 -8
  42. lyrics_transcriber/review/server.py +238 -8
  43. {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/METADATA +3 -2
  44. {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/RECORD +47 -41
  45. lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +0 -1
  46. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +0 -252
  47. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +0 -110
  48. {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/LICENSE +0 -0
  49. {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/WHEEL +0 -0
  50. {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
- wordPositions?: TranscriptionWordPosition[]
108
+ segments?: LyricsSegment[]
109
+ wordPositions: TranscriptionWordPosition[] | ReferenceWordPosition[]
104
110
  anchors: AnchorSequence[]
105
- gaps: GapSequence[]
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: (value: boolean) => void
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(`[${handlerId}] Spacebar pressed:`, {
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(`[${handlerId}] Using modal spacebar handler`)
67
+ console.log('Keyboard handler - Delegating to modal handler')
47
68
  currentModalHandler(e)
48
- }
49
- // Otherwise use global audio control
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
- <App />
8
+ <ThemeProvider theme={theme}>
9
+ <CssBaseline />
10
+ <App />
11
+ </ThemeProvider>
6
12
  )