lyrics-transcriber 0.43.1__py3-none-any.whl → 0.45.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-ZCT0s9MG.js} +10174 -6197
- lyrics_transcriber/frontend/dist/assets/index-ZCT0s9MG.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/App.tsx +5 -5
- 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/EditActionBar.tsx +68 -0
- lyrics_transcriber/frontend/src/components/EditModal.tsx +467 -399
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +373 -0
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +308 -0
- lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
- lyrics_transcriber/frontend/src/components/Header.tsx +141 -101
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +569 -107
- 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 +36 -18
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -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 +27 -3
- lyrics_transcriber/frontend/src/components/shared/types.ts +17 -2
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +90 -19
- 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/review/server.py +238 -8
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/METADATA +3 -2
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/RECORD +48 -40
- 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.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/WHEEL +0 -0
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/entry_points.txt +0 -0
@@ -5,27 +5,148 @@ import {
|
|
5
5
|
DialogActions,
|
6
6
|
IconButton,
|
7
7
|
Box,
|
8
|
-
|
9
|
-
|
10
|
-
Typography,
|
11
|
-
Menu,
|
12
|
-
MenuItem,
|
8
|
+
CircularProgress,
|
9
|
+
Typography
|
13
10
|
} from '@mui/material'
|
14
11
|
import CloseIcon from '@mui/icons-material/Close'
|
15
|
-
import AddIcon from '@mui/icons-material/Add'
|
16
|
-
import DeleteIcon from '@mui/icons-material/Delete'
|
17
|
-
import MergeIcon from '@mui/icons-material/CallMerge'
|
18
|
-
import SplitIcon from '@mui/icons-material/CallSplit'
|
19
|
-
import RestoreIcon from '@mui/icons-material/RestoreFromTrash'
|
20
|
-
import MoreVertIcon from '@mui/icons-material/MoreVert'
|
21
|
-
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
|
22
12
|
import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
|
23
|
-
import CancelIcon from '@mui/icons-material/Cancel'
|
24
13
|
import StopIcon from '@mui/icons-material/Stop'
|
25
14
|
import { LyricsSegment, Word } from '../types'
|
26
|
-
import { useState, useEffect, useCallback } from 'react'
|
27
|
-
import TimelineEditor from './TimelineEditor'
|
15
|
+
import { useState, useEffect, useCallback, useMemo, memo } from 'react'
|
28
16
|
import { nanoid } from 'nanoid'
|
17
|
+
import useManualSync from '../hooks/useManualSync'
|
18
|
+
import EditTimelineSection from './EditTimelineSection'
|
19
|
+
import EditWordList from './EditWordList'
|
20
|
+
import EditActionBar from './EditActionBar'
|
21
|
+
|
22
|
+
// Extract TimelineSection into a separate memoized component
|
23
|
+
interface TimelineSectionProps {
|
24
|
+
words: Word[]
|
25
|
+
timeRange: { start: number, end: number }
|
26
|
+
originalSegment: LyricsSegment
|
27
|
+
editedSegment: LyricsSegment
|
28
|
+
currentTime: number
|
29
|
+
isManualSyncing: boolean
|
30
|
+
syncWordIndex: number
|
31
|
+
isSpacebarPressed: boolean
|
32
|
+
onWordUpdate: (index: number, updates: Partial<Word>) => void
|
33
|
+
onPlaySegment?: (startTime: number) => void
|
34
|
+
startManualSync: () => void
|
35
|
+
isGlobal: boolean
|
36
|
+
}
|
37
|
+
|
38
|
+
const MemoizedTimelineSection = memo(function TimelineSection({
|
39
|
+
words,
|
40
|
+
timeRange,
|
41
|
+
originalSegment,
|
42
|
+
editedSegment,
|
43
|
+
currentTime,
|
44
|
+
isManualSyncing,
|
45
|
+
syncWordIndex,
|
46
|
+
isSpacebarPressed,
|
47
|
+
onWordUpdate,
|
48
|
+
onPlaySegment,
|
49
|
+
startManualSync,
|
50
|
+
isGlobal
|
51
|
+
}: TimelineSectionProps) {
|
52
|
+
return (
|
53
|
+
<EditTimelineSection
|
54
|
+
words={words}
|
55
|
+
startTime={timeRange.start}
|
56
|
+
endTime={timeRange.end}
|
57
|
+
originalStartTime={originalSegment.start_time}
|
58
|
+
originalEndTime={originalSegment.end_time}
|
59
|
+
currentStartTime={editedSegment.start_time}
|
60
|
+
currentEndTime={editedSegment.end_time}
|
61
|
+
currentTime={currentTime}
|
62
|
+
isManualSyncing={isManualSyncing}
|
63
|
+
syncWordIndex={syncWordIndex}
|
64
|
+
isSpacebarPressed={isSpacebarPressed}
|
65
|
+
onWordUpdate={onWordUpdate}
|
66
|
+
onPlaySegment={onPlaySegment}
|
67
|
+
startManualSync={startManualSync}
|
68
|
+
isGlobal={isGlobal}
|
69
|
+
/>
|
70
|
+
)
|
71
|
+
})
|
72
|
+
|
73
|
+
// Extract WordList into a separate memoized component
|
74
|
+
interface WordListProps {
|
75
|
+
words: Word[]
|
76
|
+
onWordUpdate: (index: number, updates: Partial<Word>) => void
|
77
|
+
onSplitWord: (index: number) => void
|
78
|
+
onMergeWords: (index: number) => void
|
79
|
+
onAddWord: (index?: number) => void
|
80
|
+
onRemoveWord: (index: number) => void
|
81
|
+
onSplitSegment?: (wordIndex: number) => void
|
82
|
+
onAddSegment?: (beforeIndex: number) => void
|
83
|
+
onMergeSegment?: (mergeWithNext: boolean) => void
|
84
|
+
isGlobal: boolean
|
85
|
+
}
|
86
|
+
|
87
|
+
const MemoizedWordList = memo(function WordList({
|
88
|
+
words,
|
89
|
+
onWordUpdate,
|
90
|
+
onSplitWord,
|
91
|
+
onMergeWords,
|
92
|
+
onAddWord,
|
93
|
+
onRemoveWord,
|
94
|
+
onSplitSegment,
|
95
|
+
onAddSegment,
|
96
|
+
onMergeSegment,
|
97
|
+
isGlobal
|
98
|
+
}: WordListProps) {
|
99
|
+
return (
|
100
|
+
<EditWordList
|
101
|
+
words={words}
|
102
|
+
onWordUpdate={onWordUpdate}
|
103
|
+
onSplitWord={onSplitWord}
|
104
|
+
onMergeWords={onMergeWords}
|
105
|
+
onAddWord={onAddWord}
|
106
|
+
onRemoveWord={onRemoveWord}
|
107
|
+
onSplitSegment={onSplitSegment}
|
108
|
+
onAddSegment={onAddSegment}
|
109
|
+
onMergeSegment={onMergeSegment}
|
110
|
+
isGlobal={isGlobal}
|
111
|
+
/>
|
112
|
+
)
|
113
|
+
})
|
114
|
+
|
115
|
+
// Extract ActionBar into a separate memoized component
|
116
|
+
interface ActionBarProps {
|
117
|
+
onReset: () => void
|
118
|
+
onRevertToOriginal?: () => void
|
119
|
+
onDelete?: () => void
|
120
|
+
onClose: () => void
|
121
|
+
onSave: () => void
|
122
|
+
editedSegment: LyricsSegment | null
|
123
|
+
originalTranscribedSegment?: LyricsSegment | null
|
124
|
+
isGlobal: boolean
|
125
|
+
}
|
126
|
+
|
127
|
+
const MemoizedActionBar = memo(function ActionBar({
|
128
|
+
onReset,
|
129
|
+
onRevertToOriginal,
|
130
|
+
onDelete,
|
131
|
+
onClose,
|
132
|
+
onSave,
|
133
|
+
editedSegment,
|
134
|
+
originalTranscribedSegment,
|
135
|
+
isGlobal
|
136
|
+
}: ActionBarProps) {
|
137
|
+
return (
|
138
|
+
<EditActionBar
|
139
|
+
onReset={onReset}
|
140
|
+
onRevertToOriginal={onRevertToOriginal}
|
141
|
+
onDelete={onDelete}
|
142
|
+
onClose={onClose}
|
143
|
+
onSave={onSave}
|
144
|
+
editedSegment={editedSegment}
|
145
|
+
originalTranscribedSegment={originalTranscribedSegment}
|
146
|
+
isGlobal={isGlobal}
|
147
|
+
/>
|
148
|
+
)
|
149
|
+
})
|
29
150
|
|
30
151
|
interface EditModalProps {
|
31
152
|
open: boolean
|
@@ -39,7 +160,11 @@ interface EditModalProps {
|
|
39
160
|
onDelete?: (segmentIndex: number) => void
|
40
161
|
onAddSegment?: (segmentIndex: number) => void
|
41
162
|
onSplitSegment?: (segmentIndex: number, afterWordIndex: number) => void
|
163
|
+
onMergeSegment?: (segmentIndex: number, mergeWithNext: boolean) => void
|
42
164
|
setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
|
165
|
+
originalTranscribedSegment?: LyricsSegment | null
|
166
|
+
isGlobal?: boolean
|
167
|
+
isLoading?: boolean
|
43
168
|
}
|
44
169
|
|
45
170
|
export default function EditModal({
|
@@ -54,18 +179,26 @@ export default function EditModal({
|
|
54
179
|
onDelete,
|
55
180
|
onAddSegment,
|
56
181
|
onSplitSegment,
|
182
|
+
onMergeSegment,
|
57
183
|
setModalSpacebarHandler,
|
184
|
+
originalTranscribedSegment,
|
185
|
+
isGlobal = false,
|
186
|
+
isLoading = false
|
58
187
|
}: EditModalProps) {
|
59
|
-
|
188
|
+
console.log('EditModal - Render', {
|
189
|
+
open,
|
190
|
+
isGlobal,
|
191
|
+
isLoading,
|
192
|
+
hasSegment: !!segment,
|
193
|
+
segmentIndex,
|
194
|
+
hasOriginalSegment: !!originalSegment,
|
195
|
+
hasOriginalTranscribedSegment: !!originalTranscribedSegment
|
196
|
+
});
|
197
|
+
|
60
198
|
const [editedSegment, setEditedSegment] = useState<LyricsSegment | null>(segment)
|
61
|
-
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null)
|
62
|
-
const [selectedWordIndex, setSelectedWordIndex] = useState<number | null>(null)
|
63
|
-
const [replacementText, setReplacementText] = useState('')
|
64
|
-
const [isManualSyncing, setIsManualSyncing] = useState(false)
|
65
|
-
const [syncWordIndex, setSyncWordIndex] = useState<number>(-1)
|
66
199
|
const [isPlaying, setIsPlaying] = useState(false)
|
67
200
|
|
68
|
-
// Define updateSegment first since
|
201
|
+
// Define updateSegment first since the hook depends on it
|
69
202
|
const updateSegment = useCallback((newWords: Word[]) => {
|
70
203
|
if (!editedSegment) return;
|
71
204
|
|
@@ -84,84 +217,86 @@ export default function EditModal({
|
|
84
217
|
})
|
85
218
|
}, [editedSegment])
|
86
219
|
|
87
|
-
//
|
88
|
-
const
|
89
|
-
|
90
|
-
|
91
|
-
|
220
|
+
// Use the manual sync hook
|
221
|
+
const {
|
222
|
+
isManualSyncing,
|
223
|
+
syncWordIndex,
|
224
|
+
startManualSync,
|
225
|
+
cleanupManualSync,
|
226
|
+
handleSpacebar,
|
227
|
+
isSpacebarPressed
|
228
|
+
} = useManualSync({
|
229
|
+
editedSegment,
|
230
|
+
currentTime,
|
231
|
+
onPlaySegment,
|
232
|
+
updateSegment
|
233
|
+
})
|
92
234
|
|
93
235
|
const handleClose = useCallback(() => {
|
236
|
+
console.log('EditModal - handleClose called');
|
94
237
|
cleanupManualSync()
|
95
238
|
onClose()
|
96
239
|
}, [onClose, cleanupManualSync])
|
97
240
|
|
98
|
-
// All useEffect hooks
|
99
|
-
useEffect(() => {
|
100
|
-
setEditedSegment(segment)
|
101
|
-
}, [segment])
|
102
|
-
|
103
241
|
// Update the spacebar handler when modal state changes
|
104
242
|
useEffect(() => {
|
243
|
+
const spacebarHandler = handleSpacebar // Capture the current handler
|
244
|
+
|
105
245
|
if (open) {
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
// Handle manual sync mode
|
112
|
-
if (syncWordIndex < editedSegment.words.length) {
|
113
|
-
const newWords = [...editedSegment.words]
|
114
|
-
const currentWord = newWords[syncWordIndex]
|
115
|
-
const prevWord = syncWordIndex > 0 ? newWords[syncWordIndex - 1] : null
|
116
|
-
|
117
|
-
currentWord.start_time = currentTime
|
118
|
-
|
119
|
-
if (prevWord) {
|
120
|
-
prevWord.end_time = currentTime - 0.01
|
121
|
-
}
|
122
|
-
|
123
|
-
if (syncWordIndex === editedSegment.words.length - 1) {
|
124
|
-
currentWord.end_time = editedSegment.end_time
|
125
|
-
setIsManualSyncing(false)
|
126
|
-
setSyncWordIndex(-1)
|
127
|
-
updateSegment(newWords)
|
128
|
-
} else {
|
129
|
-
setSyncWordIndex(syncWordIndex + 1)
|
130
|
-
updateSegment(newWords)
|
131
|
-
}
|
132
|
-
}
|
133
|
-
} else if (editedSegment && onPlaySegment) {
|
134
|
-
// Toggle segment playback when not in manual sync mode
|
135
|
-
const startTime = editedSegment.start_time ?? 0
|
136
|
-
const endTime = editedSegment.end_time ?? 0
|
137
|
-
|
138
|
-
if (currentTime >= startTime && currentTime <= endTime) {
|
139
|
-
if (window.toggleAudioPlayback) {
|
140
|
-
window.toggleAudioPlayback()
|
141
|
-
}
|
142
|
-
} else {
|
143
|
-
onPlaySegment(startTime)
|
144
|
-
}
|
145
|
-
}
|
246
|
+
console.log('EditModal - Setting up modal spacebar handler', {
|
247
|
+
hasPlaySegment: !!onPlaySegment,
|
248
|
+
editedSegmentId: editedSegment?.id,
|
249
|
+
handlerFunction: spacebarHandler.toString().slice(0, 100),
|
250
|
+
isLoading
|
146
251
|
})
|
147
|
-
} else {
|
148
|
-
setModalSpacebarHandler(undefined)
|
149
|
-
}
|
150
252
|
|
151
|
-
|
152
|
-
|
253
|
+
// Create a function that will be called by the global event listeners
|
254
|
+
const handleKeyEvent = (e: KeyboardEvent) => {
|
255
|
+
if (e.code === 'Space') {
|
256
|
+
spacebarHandler(e)
|
257
|
+
}
|
258
|
+
}
|
259
|
+
|
260
|
+
setModalSpacebarHandler(() => handleKeyEvent)
|
261
|
+
|
262
|
+
// Only cleanup when the effect is re-run or the modal is closed
|
263
|
+
return () => {
|
264
|
+
if (!open) {
|
265
|
+
console.log('EditModal - Cleanup: clearing modal spacebar handler')
|
266
|
+
setModalSpacebarHandler(undefined)
|
267
|
+
}
|
268
|
+
}
|
153
269
|
}
|
154
270
|
}, [
|
155
271
|
open,
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
currentTime,
|
272
|
+
handleSpacebar,
|
273
|
+
setModalSpacebarHandler,
|
274
|
+
editedSegment?.id,
|
160
275
|
onPlaySegment,
|
161
|
-
|
162
|
-
setModalSpacebarHandler
|
276
|
+
isLoading
|
163
277
|
])
|
164
278
|
|
279
|
+
// Update isPlaying when currentTime changes
|
280
|
+
useEffect(() => {
|
281
|
+
if (editedSegment) {
|
282
|
+
const startTime = editedSegment.start_time ?? 0
|
283
|
+
const endTime = editedSegment.end_time ?? 0
|
284
|
+
const isWithinSegment = currentTime >= startTime && currentTime <= endTime
|
285
|
+
|
286
|
+
setIsPlaying(isWithinSegment && window.isAudioPlaying === true)
|
287
|
+
}
|
288
|
+
}, [currentTime, editedSegment])
|
289
|
+
|
290
|
+
// All useEffect hooks
|
291
|
+
useEffect(() => {
|
292
|
+
console.log('EditModal - segment changed', {
|
293
|
+
hasSegment: !!segment,
|
294
|
+
segmentId: segment?.id,
|
295
|
+
wordCount: segment?.words.length
|
296
|
+
});
|
297
|
+
setEditedSegment(segment)
|
298
|
+
}, [segment])
|
299
|
+
|
165
300
|
// Auto-stop sync if we go past the end time
|
166
301
|
useEffect(() => {
|
167
302
|
if (!editedSegment) return
|
@@ -171,48 +306,32 @@ export default function EditModal({
|
|
171
306
|
if (window.isAudioPlaying && currentTime > endTime) {
|
172
307
|
console.log('Stopping playback: current time exceeded end time')
|
173
308
|
window.toggleAudioPlayback?.()
|
174
|
-
|
175
|
-
setSyncWordIndex(-1)
|
309
|
+
cleanupManualSync()
|
176
310
|
}
|
177
311
|
|
178
|
-
}, [isManualSyncing, editedSegment, currentTime,
|
179
|
-
|
180
|
-
// Update isPlaying when currentTime changes
|
181
|
-
useEffect(() => {
|
182
|
-
if (editedSegment) {
|
183
|
-
const startTime = editedSegment.start_time ?? 0
|
184
|
-
const endTime = editedSegment.end_time ?? 0
|
185
|
-
const isWithinSegment = currentTime >= startTime && currentTime <= endTime
|
186
|
-
|
187
|
-
// Only consider it playing if it's within the segment AND audio is actually playing
|
188
|
-
setIsPlaying(isWithinSegment && window.isAudioPlaying === true)
|
189
|
-
}
|
190
|
-
}, [currentTime, editedSegment])
|
312
|
+
}, [isManualSyncing, editedSegment, currentTime, cleanupManualSync])
|
191
313
|
|
192
314
|
// Add a function to get safe time values
|
193
|
-
const getSafeTimeRange = (segment: LyricsSegment | null) => {
|
315
|
+
const getSafeTimeRange = useCallback((segment: LyricsSegment | null) => {
|
194
316
|
if (!segment) return { start: 0, end: 1 }; // Default 1-second range
|
195
317
|
const start = segment.start_time ?? 0;
|
196
318
|
const end = segment.end_time ?? (start + 1);
|
197
319
|
return { start, end };
|
198
|
-
}
|
199
|
-
|
200
|
-
// Early return after all hooks and function definitions
|
201
|
-
if (!segment || segmentIndex === null || !editedSegment || !originalSegment) return null
|
202
|
-
|
203
|
-
// Get safe time values for TimelineEditor
|
204
|
-
const timeRange = getSafeTimeRange(editedSegment)
|
320
|
+
}, [])
|
205
321
|
|
206
|
-
|
322
|
+
// Define all handler functions with useCallback before the early return
|
323
|
+
const handleWordChange = useCallback((index: number, updates: Partial<Word>) => {
|
324
|
+
if (!editedSegment) return;
|
207
325
|
const newWords = [...editedSegment.words]
|
208
326
|
newWords[index] = {
|
209
327
|
...newWords[index],
|
210
328
|
...updates
|
211
329
|
}
|
212
330
|
updateSegment(newWords)
|
213
|
-
}
|
331
|
+
}, [editedSegment, updateSegment])
|
214
332
|
|
215
|
-
const handleAddWord = (index?: number) => {
|
333
|
+
const handleAddWord = useCallback((index?: number) => {
|
334
|
+
if (!editedSegment) return;
|
216
335
|
const newWords = [...editedSegment.words]
|
217
336
|
let newWord: Word
|
218
337
|
|
@@ -250,45 +369,47 @@ export default function EditModal({
|
|
250
369
|
}
|
251
370
|
|
252
371
|
updateSegment(newWords)
|
253
|
-
}
|
372
|
+
}, [editedSegment, updateSegment])
|
254
373
|
|
255
|
-
const handleSplitWord = (index: number) => {
|
374
|
+
const handleSplitWord = useCallback((index: number) => {
|
375
|
+
if (!editedSegment) return;
|
256
376
|
const word = editedSegment.words[index]
|
257
377
|
const startTime = word.start_time ?? 0
|
258
378
|
const endTime = word.end_time ?? startTime + 0.5
|
259
|
-
const
|
260
|
-
|
379
|
+
const totalDuration = endTime - startTime
|
380
|
+
|
381
|
+
// Split on any number of spaces and filter out empty strings
|
382
|
+
const words = word.text.split(/\s+/).filter(w => w.length > 0)
|
261
383
|
|
262
384
|
if (words.length <= 1) {
|
263
|
-
//
|
385
|
+
// If no spaces found, split the word in half as before
|
264
386
|
const firstHalf = word.text.slice(0, Math.ceil(word.text.length / 2))
|
265
387
|
const secondHalf = word.text.slice(Math.ceil(word.text.length / 2))
|
266
388
|
words[0] = firstHalf
|
267
389
|
words[1] = secondHalf
|
268
390
|
}
|
269
391
|
|
270
|
-
|
271
|
-
|
272
|
-
{
|
273
|
-
id: nanoid(),
|
274
|
-
text: words[0],
|
275
|
-
start_time: startTime,
|
276
|
-
end_time: midTime,
|
277
|
-
confidence: 1.0
|
278
|
-
},
|
279
|
-
{
|
280
|
-
id: nanoid(),
|
281
|
-
text: words[1],
|
282
|
-
start_time: midTime,
|
283
|
-
end_time: endTime,
|
284
|
-
confidence: 1.0
|
285
|
-
}
|
286
|
-
)
|
392
|
+
// Calculate time per word
|
393
|
+
const timePerWord = totalDuration / words.length
|
287
394
|
|
288
|
-
|
289
|
-
|
395
|
+
// Create new word objects with evenly distributed times
|
396
|
+
const newWords = words.map((text, i) => ({
|
397
|
+
id: nanoid(),
|
398
|
+
text,
|
399
|
+
start_time: startTime + (i * timePerWord),
|
400
|
+
end_time: startTime + ((i + 1) * timePerWord),
|
401
|
+
confidence: 1.0
|
402
|
+
}))
|
403
|
+
|
404
|
+
// Replace the original word with the new words
|
405
|
+
const allWords = [...editedSegment.words]
|
406
|
+
allWords.splice(index, 1, ...newWords)
|
290
407
|
|
291
|
-
|
408
|
+
updateSegment(allWords)
|
409
|
+
}, [editedSegment, updateSegment])
|
410
|
+
|
411
|
+
const handleMergeWords = useCallback((index: number) => {
|
412
|
+
if (!editedSegment) return;
|
292
413
|
if (index >= editedSegment.words.length - 1) return
|
293
414
|
|
294
415
|
const word1 = editedSegment.words[index]
|
@@ -304,116 +425,79 @@ export default function EditModal({
|
|
304
425
|
})
|
305
426
|
|
306
427
|
updateSegment(newWords)
|
307
|
-
}
|
428
|
+
}, [editedSegment, updateSegment])
|
308
429
|
|
309
|
-
const handleRemoveWord = (index: number) => {
|
430
|
+
const handleRemoveWord = useCallback((index: number) => {
|
431
|
+
if (!editedSegment) return;
|
310
432
|
const newWords = editedSegment.words.filter((_, i) => i !== index)
|
311
433
|
updateSegment(newWords)
|
312
|
-
}
|
313
|
-
|
314
|
-
const handleReset = () => {
|
315
|
-
setEditedSegment(JSON.parse(JSON.stringify(originalSegment)))
|
316
|
-
}
|
317
|
-
|
318
|
-
const handleWordMenu = (event: React.MouseEvent<HTMLElement>, index: number) => {
|
319
|
-
setMenuAnchorEl(event.currentTarget)
|
320
|
-
setSelectedWordIndex(index)
|
321
|
-
}
|
322
|
-
|
323
|
-
const handleMenuClose = () => {
|
324
|
-
setMenuAnchorEl(null)
|
325
|
-
setSelectedWordIndex(null)
|
326
|
-
}
|
327
|
-
|
328
|
-
const handleSave = () => {
|
329
|
-
if (editedSegment) {
|
330
|
-
console.log('EditModal - Saving segment:', {
|
331
|
-
segmentIndex,
|
332
|
-
originalText: segment?.text,
|
333
|
-
editedText: editedSegment.text,
|
334
|
-
wordCount: editedSegment.words.length,
|
335
|
-
timeRange: `${editedSegment.start_time?.toFixed(4) ?? 'N/A'} - ${editedSegment.end_time?.toFixed(4) ?? 'N/A'}`
|
336
|
-
})
|
337
|
-
onSave(editedSegment)
|
338
|
-
onClose()
|
339
|
-
}
|
340
|
-
}
|
434
|
+
}, [editedSegment, updateSegment])
|
341
435
|
|
342
|
-
const
|
343
|
-
if (!
|
436
|
+
const handleReset = useCallback(() => {
|
437
|
+
if (!originalSegment) return
|
344
438
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
439
|
+
console.log('EditModal - Resetting to original:', {
|
440
|
+
isGlobal,
|
441
|
+
originalSegmentId: originalSegment.id,
|
442
|
+
originalWordCount: originalSegment.words.length
|
443
|
+
})
|
349
444
|
|
350
|
-
|
445
|
+
setEditedSegment(JSON.parse(JSON.stringify(originalSegment)))
|
446
|
+
}, [originalSegment, isGlobal])
|
351
447
|
|
352
|
-
|
353
|
-
|
354
|
-
updatedWords = editedSegment.words.map((word, index) => ({
|
355
|
-
id: word.id, // Keep original ID
|
356
|
-
text: newWords[index],
|
357
|
-
start_time: word.start_time,
|
358
|
-
end_time: word.end_time,
|
359
|
-
confidence: 1.0
|
360
|
-
}))
|
361
|
-
} else {
|
362
|
-
// If word count differs, distribute time evenly and generate new IDs
|
363
|
-
const avgWordDuration = segmentDuration / newWords.length
|
364
|
-
updatedWords = newWords.map((text, index) => ({
|
365
|
-
id: nanoid(), // Generate new ID
|
366
|
-
text,
|
367
|
-
start_time: startTime + (index * avgWordDuration),
|
368
|
-
end_time: startTime + ((index + 1) * avgWordDuration),
|
369
|
-
confidence: 1.0
|
370
|
-
}))
|
371
|
-
}
|
448
|
+
const handleRevertToOriginal = useCallback(() => {
|
449
|
+
if (!originalTranscribedSegment) return
|
372
450
|
|
373
|
-
|
374
|
-
|
375
|
-
|
451
|
+
console.log('EditModal - Reverting to original transcribed:', {
|
452
|
+
isGlobal,
|
453
|
+
originalTranscribedSegmentId: originalTranscribedSegment.id,
|
454
|
+
originalTranscribedWordCount: originalTranscribedSegment.words.length
|
455
|
+
})
|
376
456
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
457
|
+
setEditedSegment(JSON.parse(JSON.stringify(originalTranscribedSegment)))
|
458
|
+
}, [originalTranscribedSegment, isGlobal])
|
459
|
+
|
460
|
+
const handleSave = useCallback(() => {
|
461
|
+
if (!editedSegment || !segment) return;
|
462
|
+
|
463
|
+
console.log('EditModal - Saving segment:', {
|
464
|
+
isGlobal,
|
465
|
+
segmentIndex,
|
466
|
+
originalText: segment?.text,
|
467
|
+
editedText: editedSegment.text,
|
468
|
+
wordCount: editedSegment.words.length,
|
469
|
+
firstWord: editedSegment.words[0],
|
470
|
+
lastWord: editedSegment.words[editedSegment.words.length - 1],
|
471
|
+
timeRange: `${editedSegment.start_time?.toFixed(4) ?? 'N/A'} - ${editedSegment.end_time?.toFixed(4) ?? 'N/A'}`
|
472
|
+
})
|
473
|
+
onSave(editedSegment)
|
474
|
+
onClose()
|
475
|
+
}, [editedSegment, isGlobal, segmentIndex, segment, onSave, onClose])
|
383
476
|
|
384
|
-
const handleDelete = () => {
|
477
|
+
const handleDelete = useCallback(() => {
|
385
478
|
if (segmentIndex !== null) {
|
386
479
|
onDelete?.(segmentIndex)
|
387
480
|
onClose()
|
388
481
|
}
|
389
|
-
}
|
482
|
+
}, [segmentIndex, onDelete, onClose])
|
390
483
|
|
391
|
-
const handleSplitSegment = (wordIndex: number) => {
|
484
|
+
const handleSplitSegment = useCallback((wordIndex: number) => {
|
392
485
|
if (segmentIndex !== null && editedSegment) {
|
393
486
|
handleSave() // Save current changes first
|
394
487
|
onSplitSegment?.(segmentIndex, wordIndex)
|
395
488
|
}
|
396
|
-
}
|
489
|
+
}, [segmentIndex, editedSegment, handleSave, onSplitSegment])
|
397
490
|
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
return
|
491
|
+
const handleMergeSegment = useCallback((mergeWithNext: boolean) => {
|
492
|
+
if (segmentIndex !== null && editedSegment) {
|
493
|
+
handleSave() // Save current changes first
|
494
|
+
onMergeSegment?.(segmentIndex, mergeWithNext)
|
495
|
+
onClose()
|
404
496
|
}
|
405
|
-
|
406
|
-
if (!editedSegment || !onPlaySegment) return
|
407
|
-
|
408
|
-
setIsManualSyncing(true)
|
409
|
-
setSyncWordIndex(0)
|
410
|
-
// Start playing 3 seconds before segment start
|
411
|
-
const startTime = (editedSegment.start_time ?? 0) - 3
|
412
|
-
onPlaySegment(startTime)
|
413
|
-
}
|
497
|
+
}, [segmentIndex, editedSegment, handleSave, onMergeSegment, onClose])
|
414
498
|
|
415
499
|
// Handle play/stop button click
|
416
|
-
const handlePlayButtonClick = () => {
|
500
|
+
const handlePlayButtonClick = useCallback(() => {
|
417
501
|
if (!segment?.start_time || !onPlaySegment) return
|
418
502
|
|
419
503
|
if (isPlaying) {
|
@@ -425,19 +509,37 @@ export default function EditModal({
|
|
425
509
|
// Start playback
|
426
510
|
onPlaySegment(segment.start_time)
|
427
511
|
}
|
428
|
-
}
|
429
|
-
|
430
|
-
return
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
512
|
+
}, [segment?.start_time, onPlaySegment, isPlaying])
|
513
|
+
|
514
|
+
// Calculate timeRange before the early return
|
515
|
+
const timeRange = useMemo(() => {
|
516
|
+
if (!editedSegment) return { start: 0, end: 1 };
|
517
|
+
return getSafeTimeRange(editedSegment);
|
518
|
+
}, [getSafeTimeRange, editedSegment]);
|
519
|
+
|
520
|
+
// Memoize the dialog title to prevent re-renders
|
521
|
+
const dialogTitle = useMemo(() => {
|
522
|
+
console.log('EditModal - Rendering dialog title', { isLoading, isGlobal });
|
523
|
+
|
524
|
+
if (isLoading) {
|
525
|
+
return (
|
526
|
+
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
527
|
+
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
|
528
|
+
Loading {isGlobal ? 'All Words' : `Segment ${segmentIndex}`}...
|
529
|
+
</Box>
|
530
|
+
<IconButton onClick={onClose} sx={{ ml: 'auto' }}>
|
531
|
+
<CloseIcon />
|
532
|
+
</IconButton>
|
533
|
+
</DialogTitle>
|
534
|
+
);
|
535
|
+
}
|
536
|
+
|
537
|
+
if (!segment) return null;
|
538
|
+
|
539
|
+
return (
|
438
540
|
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
439
541
|
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
|
440
|
-
Edit Segment {segmentIndex}
|
542
|
+
Edit {isGlobal ? 'All Words' : `Segment ${segmentIndex}`}
|
441
543
|
{segment?.start_time !== null && onPlaySegment && (
|
442
544
|
<IconButton
|
443
545
|
size="small"
|
@@ -456,179 +558,145 @@ export default function EditModal({
|
|
456
558
|
<CloseIcon />
|
457
559
|
</IconButton>
|
458
560
|
</DialogTitle>
|
459
|
-
|
460
|
-
|
461
|
-
<TimelineEditor
|
462
|
-
words={editedSegment.words}
|
463
|
-
startTime={timeRange.start}
|
464
|
-
endTime={timeRange.end}
|
465
|
-
onWordUpdate={handleWordChange}
|
466
|
-
currentTime={currentTime}
|
467
|
-
onPlaySegment={onPlaySegment}
|
468
|
-
/>
|
469
|
-
</Box>
|
561
|
+
);
|
562
|
+
}, [isGlobal, segmentIndex, segment, onPlaySegment, handlePlayButtonClick, isPlaying, onClose, isLoading])
|
470
563
|
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
>
|
486
|
-
{isManualSyncing ? "Cancel Sync" : "Manual Sync"}
|
487
|
-
</Button>
|
488
|
-
{isManualSyncing && (
|
489
|
-
<Typography variant="body2">
|
490
|
-
Press spacebar for word {syncWordIndex + 1} of {editedSegment?.words.length}
|
491
|
-
</Typography>
|
492
|
-
)}
|
493
|
-
</Box>
|
494
|
-
</Box>
|
564
|
+
// Early return after all hooks and function definitions
|
565
|
+
if (!isLoading && (!segment || !editedSegment || !originalSegment)) {
|
566
|
+
console.log('EditModal - Early return: missing required data', {
|
567
|
+
hasSegment: !!segment,
|
568
|
+
hasEditedSegment: !!editedSegment,
|
569
|
+
hasOriginalSegment: !!originalSegment,
|
570
|
+
isLoading
|
571
|
+
});
|
572
|
+
return null;
|
573
|
+
}
|
574
|
+
if (!isLoading && !isGlobal && segmentIndex === null) {
|
575
|
+
console.log('EditModal - Early return: non-global mode with null segmentIndex');
|
576
|
+
return null;
|
577
|
+
}
|
495
578
|
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
value={word.text}
|
502
|
-
onChange={(e) => handleWordChange(index, { text: e.target.value })}
|
503
|
-
fullWidth
|
504
|
-
size="small"
|
505
|
-
/>
|
506
|
-
<TextField
|
507
|
-
label="Start Time"
|
508
|
-
value={word.start_time?.toFixed(2) ?? ''}
|
509
|
-
onChange={(e) => handleWordChange(index, { start_time: parseFloat(e.target.value) })}
|
510
|
-
type="number"
|
511
|
-
inputProps={{ step: 0.01 }}
|
512
|
-
sx={{ width: '150px' }}
|
513
|
-
size="small"
|
514
|
-
/>
|
515
|
-
<TextField
|
516
|
-
label="End Time"
|
517
|
-
value={word.end_time?.toFixed(2) ?? ''}
|
518
|
-
onChange={(e) => handleWordChange(index, { end_time: parseFloat(e.target.value) })}
|
519
|
-
type="number"
|
520
|
-
inputProps={{ step: 0.01 }}
|
521
|
-
sx={{ width: '150px' }}
|
522
|
-
size="small"
|
523
|
-
/>
|
524
|
-
<IconButton
|
525
|
-
onClick={() => handleRemoveWord(index)}
|
526
|
-
disabled={editedSegment.words.length <= 1}
|
527
|
-
sx={{ color: 'error.main' }}
|
528
|
-
>
|
529
|
-
<DeleteIcon fontSize="small" />
|
530
|
-
</IconButton>
|
531
|
-
<IconButton onClick={(e) => handleWordMenu(e, index)}>
|
532
|
-
<MoreVertIcon />
|
533
|
-
</IconButton>
|
534
|
-
</Box>
|
535
|
-
))}
|
536
|
-
</Box>
|
579
|
+
console.log('EditModal - Rendering dialog content', {
|
580
|
+
isLoading,
|
581
|
+
hasEditedSegment: !!editedSegment,
|
582
|
+
hasOriginalSegment: !!originalSegment
|
583
|
+
});
|
537
584
|
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
585
|
+
return (
|
586
|
+
<Dialog
|
587
|
+
open={open}
|
588
|
+
onClose={handleClose}
|
589
|
+
maxWidth="md"
|
590
|
+
fullWidth
|
591
|
+
onKeyDown={(e) => {
|
592
|
+
if (e.key === 'Enter' && !e.shiftKey && !isLoading) {
|
593
|
+
e.preventDefault()
|
594
|
+
handleSave()
|
595
|
+
}
|
596
|
+
}}
|
597
|
+
PaperProps={{
|
598
|
+
sx: {
|
599
|
+
height: '90vh',
|
600
|
+
margin: '5vh 0'
|
601
|
+
}
|
602
|
+
}}
|
603
|
+
>
|
604
|
+
{dialogTitle}
|
605
|
+
|
606
|
+
<DialogContent
|
607
|
+
dividers
|
608
|
+
sx={{
|
609
|
+
display: 'flex',
|
610
|
+
flexDirection: 'column',
|
611
|
+
flexGrow: 1,
|
612
|
+
overflow: 'hidden',
|
613
|
+
position: 'relative'
|
614
|
+
}}
|
615
|
+
>
|
616
|
+
{isLoading && (
|
617
|
+
<Box sx={{
|
618
|
+
display: 'flex',
|
619
|
+
flexDirection: 'column',
|
620
|
+
alignItems: 'center',
|
621
|
+
justifyContent: 'center',
|
622
|
+
height: '100%',
|
623
|
+
width: '100%',
|
624
|
+
position: 'absolute',
|
625
|
+
top: 0,
|
626
|
+
left: 0,
|
627
|
+
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
628
|
+
zIndex: 10
|
629
|
+
}}>
|
630
|
+
<CircularProgress size={60} thickness={4} />
|
631
|
+
<Typography variant="h6" sx={{ mt: 2, fontWeight: 'bold' }}>
|
632
|
+
Loading {isGlobal ? 'all words' : 'segment'}...
|
633
|
+
</Typography>
|
634
|
+
<Typography variant="body2" sx={{ mt: 1, maxWidth: '80%', textAlign: 'center' }}>
|
635
|
+
{isGlobal ? 'This may take a few seconds for songs with many words.' : 'Please wait...'}
|
636
|
+
</Typography>
|
637
|
+
</Box>
|
638
|
+
)}
|
639
|
+
|
640
|
+
{!isLoading && editedSegment && originalSegment && (
|
641
|
+
<>
|
642
|
+
<MemoizedTimelineSection
|
643
|
+
words={editedSegment.words}
|
644
|
+
timeRange={timeRange}
|
645
|
+
originalSegment={originalSegment}
|
646
|
+
editedSegment={editedSegment}
|
647
|
+
currentTime={currentTime}
|
648
|
+
isManualSyncing={isManualSyncing}
|
649
|
+
syncWordIndex={syncWordIndex}
|
650
|
+
isSpacebarPressed={isSpacebarPressed}
|
651
|
+
onWordUpdate={handleWordChange}
|
652
|
+
onPlaySegment={onPlaySegment}
|
653
|
+
startManualSync={startManualSync}
|
654
|
+
isGlobal={isGlobal}
|
655
|
+
/>
|
656
|
+
|
657
|
+
<MemoizedWordList
|
658
|
+
words={editedSegment.words}
|
659
|
+
onWordUpdate={handleWordChange}
|
660
|
+
onSplitWord={handleSplitWord}
|
661
|
+
onMergeWords={handleMergeWords}
|
662
|
+
onAddWord={handleAddWord}
|
663
|
+
onRemoveWord={handleRemoveWord}
|
664
|
+
onSplitSegment={handleSplitSegment}
|
665
|
+
onAddSegment={onAddSegment}
|
666
|
+
onMergeSegment={handleMergeSegment}
|
667
|
+
isGlobal={isGlobal}
|
668
|
+
/>
|
669
|
+
</>
|
670
|
+
)}
|
671
|
+
|
672
|
+
{!isLoading && (!editedSegment || !originalSegment) && (
|
673
|
+
<Box sx={{
|
674
|
+
display: 'flex',
|
675
|
+
alignItems: 'center',
|
676
|
+
justifyContent: 'center',
|
677
|
+
height: '100%'
|
678
|
+
}}>
|
679
|
+
<Typography variant="h6">
|
680
|
+
No segment data available
|
681
|
+
</Typography>
|
682
|
+
</Box>
|
683
|
+
)}
|
556
684
|
</DialogContent>
|
685
|
+
|
557
686
|
<DialogActions>
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
>
|
571
|
-
Add Segment Before
|
572
|
-
</Button>
|
573
|
-
<Button
|
574
|
-
startIcon={<DeleteIcon />}
|
575
|
-
onClick={handleDelete}
|
576
|
-
color="error"
|
577
|
-
>
|
578
|
-
Delete Segment
|
579
|
-
</Button>
|
580
|
-
</Box>
|
581
|
-
<Button onClick={handleClose}>Cancel</Button>
|
582
|
-
<Button onClick={() => {
|
583
|
-
cleanupManualSync()
|
584
|
-
onSave(editedSegment)
|
585
|
-
}}>
|
586
|
-
Save Changes
|
587
|
-
</Button>
|
687
|
+
{!isLoading && editedSegment && (
|
688
|
+
<MemoizedActionBar
|
689
|
+
onReset={handleReset}
|
690
|
+
onRevertToOriginal={handleRevertToOriginal}
|
691
|
+
onDelete={handleDelete}
|
692
|
+
onClose={handleClose}
|
693
|
+
onSave={handleSave}
|
694
|
+
editedSegment={editedSegment}
|
695
|
+
originalTranscribedSegment={originalTranscribedSegment}
|
696
|
+
isGlobal={isGlobal}
|
697
|
+
/>
|
698
|
+
)}
|
588
699
|
</DialogActions>
|
589
|
-
|
590
|
-
<Menu
|
591
|
-
anchorEl={menuAnchorEl}
|
592
|
-
open={Boolean(menuAnchorEl)}
|
593
|
-
onClose={handleMenuClose}
|
594
|
-
>
|
595
|
-
<MenuItem onClick={() => {
|
596
|
-
handleAddWord(selectedWordIndex!)
|
597
|
-
handleMenuClose()
|
598
|
-
}}>
|
599
|
-
<AddIcon sx={{ mr: 1 }} /> Add Word After
|
600
|
-
</MenuItem>
|
601
|
-
<MenuItem onClick={() => {
|
602
|
-
handleSplitWord(selectedWordIndex!)
|
603
|
-
handleMenuClose()
|
604
|
-
}}>
|
605
|
-
<SplitIcon sx={{ mr: 1 }} /> Split Word
|
606
|
-
</MenuItem>
|
607
|
-
<MenuItem onClick={() => {
|
608
|
-
handleSplitSegment(selectedWordIndex!)
|
609
|
-
handleMenuClose()
|
610
|
-
}}>
|
611
|
-
<SplitIcon sx={{ mr: 1 }} /> Split Segment After Word
|
612
|
-
</MenuItem>
|
613
|
-
<MenuItem
|
614
|
-
onClick={() => {
|
615
|
-
handleMergeWords(selectedWordIndex!)
|
616
|
-
handleMenuClose()
|
617
|
-
}}
|
618
|
-
disabled={selectedWordIndex === editedSegment.words.length - 1}
|
619
|
-
>
|
620
|
-
<MergeIcon sx={{ mr: 1 }} /> Merge with Next
|
621
|
-
</MenuItem>
|
622
|
-
<MenuItem
|
623
|
-
onClick={() => {
|
624
|
-
handleRemoveWord(selectedWordIndex!)
|
625
|
-
handleMenuClose()
|
626
|
-
}}
|
627
|
-
disabled={editedSegment.words.length <= 1}
|
628
|
-
>
|
629
|
-
<DeleteIcon sx={{ mr: 1 }} color="error" /> Remove
|
630
|
-
</MenuItem>
|
631
|
-
</Menu>
|
632
700
|
</Dialog>
|
633
701
|
)
|
634
702
|
}
|