lyrics-transcriber 0.41.0__py3-none-any.whl → 0.43.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 +30 -52
- lyrics_transcriber/correction/anchor_sequence.py +325 -150
- lyrics_transcriber/correction/corrector.py +224 -107
- lyrics_transcriber/correction/handlers/base.py +28 -10
- lyrics_transcriber/correction/handlers/extend_anchor.py +47 -24
- lyrics_transcriber/correction/handlers/levenshtein.py +75 -33
- lyrics_transcriber/correction/handlers/llm.py +290 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +81 -36
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +46 -26
- lyrics_transcriber/correction/handlers/repeat.py +28 -11
- lyrics_transcriber/correction/handlers/sound_alike.py +68 -32
- lyrics_transcriber/correction/handlers/syllables_match.py +80 -30
- lyrics_transcriber/correction/handlers/word_count_match.py +36 -19
- lyrics_transcriber/correction/handlers/word_operations.py +68 -22
- lyrics_transcriber/correction/text_utils.py +3 -7
- lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
- lyrics_transcriber/frontend/.yarn/releases/yarn-4.6.0.cjs +934 -0
- lyrics_transcriber/frontend/.yarnrc.yml +3 -0
- lyrics_transcriber/frontend/dist/assets/{index-DKnNJHRK.js → index-D0Gr3Ep7.js} +16509 -9038
- lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/package.json +6 -2
- lyrics_transcriber/frontend/src/App.tsx +18 -2
- lyrics_transcriber/frontend/src/api.ts +103 -6
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +14 -6
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +86 -59
- lyrics_transcriber/frontend/src/components/EditModal.tsx +281 -63
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +2 -2
- lyrics_transcriber/frontend/src/components/Header.tsx +249 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +320 -266
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +120 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +174 -52
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +158 -114
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +59 -78
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +39 -16
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +4 -10
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +134 -68
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +1 -1
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +85 -115
- lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
- lyrics_transcriber/frontend/src/components/shared/types.ts +15 -7
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +67 -0
- lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +7 -7
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +121 -0
- lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
- lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
- lyrics_transcriber/frontend/src/types.js +2 -0
- lyrics_transcriber/frontend/src/types.ts +70 -49
- lyrics_transcriber/frontend/src/validation.ts +132 -0
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/yarn.lock +3752 -0
- lyrics_transcriber/lyrics/base_lyrics_provider.py +75 -12
- lyrics_transcriber/lyrics/file_provider.py +6 -5
- lyrics_transcriber/lyrics/genius.py +5 -2
- lyrics_transcriber/lyrics/spotify.py +58 -21
- lyrics_transcriber/output/ass/config.py +16 -5
- lyrics_transcriber/output/cdg.py +1 -1
- lyrics_transcriber/output/generator.py +22 -8
- lyrics_transcriber/output/plain_text.py +15 -10
- lyrics_transcriber/output/segment_resizer.py +16 -3
- lyrics_transcriber/output/subtitles.py +27 -1
- lyrics_transcriber/output/video.py +107 -1
- lyrics_transcriber/review/__init__.py +0 -1
- lyrics_transcriber/review/server.py +337 -164
- lyrics_transcriber/transcribers/audioshake.py +3 -0
- lyrics_transcriber/transcribers/base_transcriber.py +11 -3
- lyrics_transcriber/transcribers/whisper.py +11 -1
- lyrics_transcriber/types.py +151 -105
- lyrics_transcriber/utils/word_utils.py +27 -0
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/METADATA +3 -1
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/RECORD +75 -61
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/WHEEL +1 -1
- lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js.map +0 -1
- lyrics_transcriber/frontend/package-lock.json +0 -4260
- lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +0 -202
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/entry_points.txt +0 -0
@@ -6,13 +6,12 @@ import {
|
|
6
6
|
Button,
|
7
7
|
Box,
|
8
8
|
Typography,
|
9
|
-
Paper
|
10
|
-
Collapse,
|
11
|
-
IconButton,
|
9
|
+
Paper
|
12
10
|
} from '@mui/material'
|
13
|
-
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
14
11
|
import { CorrectionData } from '../types'
|
15
|
-
import { useMemo,
|
12
|
+
import { useMemo, useRef, useEffect } from 'react'
|
13
|
+
import { ApiClient } from '../api'
|
14
|
+
import PreviewVideoSection from './PreviewVideoSection'
|
16
15
|
|
17
16
|
interface ReviewChangesModalProps {
|
18
17
|
open: boolean
|
@@ -20,6 +19,8 @@ interface ReviewChangesModalProps {
|
|
20
19
|
originalData: CorrectionData
|
21
20
|
updatedData: CorrectionData
|
22
21
|
onSubmit: () => void
|
22
|
+
apiClient: ApiClient | null
|
23
|
+
setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
|
23
24
|
}
|
24
25
|
|
25
26
|
interface DiffResult {
|
@@ -34,29 +35,29 @@ interface DiffResult {
|
|
34
35
|
// Add interfaces for the word and segment structures
|
35
36
|
interface Word {
|
36
37
|
text: string
|
37
|
-
start_time: number
|
38
|
-
end_time: number
|
38
|
+
start_time: number | null
|
39
|
+
end_time: number | null
|
39
40
|
id?: string
|
40
41
|
}
|
41
42
|
|
42
43
|
interface Segment {
|
43
44
|
text: string
|
44
|
-
start_time: number
|
45
|
-
end_time: number
|
45
|
+
start_time: number | null
|
46
|
+
end_time: number | null
|
46
47
|
words: Word[]
|
47
48
|
id?: string
|
48
49
|
}
|
49
50
|
|
50
51
|
const normalizeWordForComparison = (word: Word): Omit<Word, 'id'> => ({
|
51
52
|
text: word.text,
|
52
|
-
start_time: word.start_time,
|
53
|
-
end_time: word.end_time
|
53
|
+
start_time: word.start_time ?? 0, // Default to 0 for comparison
|
54
|
+
end_time: word.end_time ?? 0 // Default to 0 for comparison
|
54
55
|
})
|
55
56
|
|
56
57
|
const normalizeSegmentForComparison = (segment: Segment): Omit<Segment, 'id'> => ({
|
57
58
|
text: segment.text,
|
58
|
-
start_time: segment.start_time,
|
59
|
-
end_time: segment.end_time,
|
59
|
+
start_time: segment.start_time ?? 0, // Default to 0 for comparison
|
60
|
+
end_time: segment.end_time ?? 0, // Default to 0 for comparison
|
60
61
|
words: segment.words.map(normalizeWordForComparison)
|
61
62
|
})
|
62
63
|
|
@@ -65,14 +66,40 @@ export default function ReviewChangesModal({
|
|
65
66
|
onClose,
|
66
67
|
originalData,
|
67
68
|
updatedData,
|
68
|
-
onSubmit
|
69
|
+
onSubmit,
|
70
|
+
apiClient,
|
71
|
+
setModalSpacebarHandler
|
69
72
|
}: ReviewChangesModalProps) {
|
70
|
-
|
73
|
+
// Add ref to video element
|
74
|
+
const videoRef = useRef<HTMLVideoElement>(null)
|
75
|
+
|
76
|
+
// Add effect to handle spacebar
|
77
|
+
useEffect(() => {
|
78
|
+
if (open) {
|
79
|
+
setModalSpacebarHandler(() => (e: KeyboardEvent) => {
|
80
|
+
e.preventDefault()
|
81
|
+
e.stopPropagation()
|
82
|
+
|
83
|
+
if (videoRef.current) {
|
84
|
+
if (videoRef.current.paused) {
|
85
|
+
videoRef.current.play()
|
86
|
+
} else {
|
87
|
+
videoRef.current.pause()
|
88
|
+
}
|
89
|
+
}
|
90
|
+
})
|
91
|
+
} else {
|
92
|
+
setModalSpacebarHandler(undefined)
|
93
|
+
}
|
94
|
+
|
95
|
+
return () => {
|
96
|
+
setModalSpacebarHandler(undefined)
|
97
|
+
}
|
98
|
+
}, [open, setModalSpacebarHandler])
|
71
99
|
|
72
100
|
const differences = useMemo(() => {
|
73
101
|
const diffs: DiffResult[] = []
|
74
102
|
|
75
|
-
// Compare corrected segments
|
76
103
|
originalData.corrected_segments.forEach((originalSegment, index) => {
|
77
104
|
const updatedSegment = updatedData.corrected_segments[index]
|
78
105
|
if (!updatedSegment) {
|
@@ -89,26 +116,26 @@ export default function ReviewChangesModal({
|
|
89
116
|
const normalizedUpdated = normalizeSegmentForComparison(updatedSegment)
|
90
117
|
const wordChanges: DiffResult[] = []
|
91
118
|
|
92
|
-
// Compare word-level changes
|
93
|
-
normalizedOriginal.words.forEach((word
|
119
|
+
// Compare word-level changes
|
120
|
+
normalizedOriginal.words.forEach((word, wordIndex) => {
|
94
121
|
const updatedWord = normalizedUpdated.words[wordIndex]
|
95
122
|
if (!updatedWord) {
|
96
123
|
wordChanges.push({
|
97
124
|
type: 'removed',
|
98
125
|
path: `Word ${wordIndex}`,
|
99
|
-
oldValue: `"${word.text}" (${word.start_time
|
126
|
+
oldValue: `"${word.text}" (${word.start_time?.toFixed(4) ?? 'N/A'} - ${word.end_time?.toFixed(4) ?? 'N/A'})`
|
100
127
|
})
|
101
128
|
return
|
102
129
|
}
|
103
130
|
|
104
131
|
if (word.text !== updatedWord.text ||
|
105
|
-
Math.abs(word.start_time - updatedWord.start_time) > 0.0001 ||
|
106
|
-
Math.abs(word.end_time - updatedWord.end_time) > 0.0001) {
|
132
|
+
Math.abs((word.start_time ?? 0) - (updatedWord.start_time ?? 0)) > 0.0001 ||
|
133
|
+
Math.abs((word.end_time ?? 0) - (updatedWord.end_time ?? 0)) > 0.0001) {
|
107
134
|
wordChanges.push({
|
108
135
|
type: 'modified',
|
109
136
|
path: `Word ${wordIndex}`,
|
110
|
-
oldValue: `"${word.text}" (${word.start_time
|
111
|
-
newValue: `"${updatedWord.text}" (${updatedWord.start_time
|
137
|
+
oldValue: `"${word.text}" (${word.start_time?.toFixed(4) ?? 'N/A'} - ${word.end_time?.toFixed(4) ?? 'N/A'})`,
|
138
|
+
newValue: `"${updatedWord.text}" (${updatedWord.start_time?.toFixed(4) ?? 'N/A'} - ${updatedWord.end_time?.toFixed(4) ?? 'N/A'})`
|
112
139
|
})
|
113
140
|
}
|
114
141
|
})
|
@@ -120,21 +147,22 @@ export default function ReviewChangesModal({
|
|
120
147
|
wordChanges.push({
|
121
148
|
type: 'added',
|
122
149
|
path: `Word ${i}`,
|
123
|
-
newValue: `"${word.text}" (${word.start_time
|
150
|
+
newValue: `"${word.text}" (${word.start_time?.toFixed(4) ?? 'N/A'} - ${word.end_time?.toFixed(4) ?? 'N/A'})`
|
124
151
|
})
|
125
152
|
}
|
126
153
|
}
|
127
154
|
|
155
|
+
// Compare segment-level changes
|
128
156
|
if (normalizedOriginal.text !== normalizedUpdated.text ||
|
129
|
-
Math.abs(normalizedOriginal.start_time - normalizedUpdated.start_time) > 0.0001 ||
|
130
|
-
Math.abs(normalizedOriginal.end_time - normalizedUpdated.end_time) > 0.0001 ||
|
157
|
+
Math.abs((normalizedOriginal.start_time ?? 0) - (normalizedUpdated.start_time ?? 0)) > 0.0001 ||
|
158
|
+
Math.abs((normalizedOriginal.end_time ?? 0) - (normalizedUpdated.end_time ?? 0)) > 0.0001 ||
|
131
159
|
wordChanges.length > 0) {
|
132
160
|
diffs.push({
|
133
161
|
type: 'modified',
|
134
162
|
path: `Segment ${index}`,
|
135
163
|
segmentIndex: index,
|
136
|
-
oldValue: `"${normalizedOriginal.text}" (${normalizedOriginal.start_time
|
137
|
-
newValue: `"${normalizedUpdated.text}" (${normalizedUpdated.start_time
|
164
|
+
oldValue: `"${normalizedOriginal.text}" (${normalizedOriginal.start_time?.toFixed(4) ?? 'N/A'} - ${normalizedOriginal.end_time?.toFixed(4) ?? 'N/A'})`,
|
165
|
+
newValue: `"${normalizedUpdated.text}" (${normalizedUpdated.start_time?.toFixed(4) ?? 'N/A'} - ${normalizedUpdated.end_time?.toFixed(4) ?? 'N/A'})`,
|
138
166
|
wordChanges: wordChanges.length > 0 ? wordChanges : undefined
|
139
167
|
})
|
140
168
|
}
|
@@ -148,7 +176,7 @@ export default function ReviewChangesModal({
|
|
148
176
|
type: 'added',
|
149
177
|
path: `Segment ${i}`,
|
150
178
|
segmentIndex: i,
|
151
|
-
newValue: `"${segment.text}" (${segment.start_time
|
179
|
+
newValue: `"${segment.text}" (${segment.start_time?.toFixed(4) ?? 'N/A'} - ${segment.end_time?.toFixed(4) ?? 'N/A'})`
|
152
180
|
})
|
153
181
|
}
|
154
182
|
}
|
@@ -156,82 +184,81 @@ export default function ReviewChangesModal({
|
|
156
184
|
return diffs
|
157
185
|
}, [originalData, updatedData])
|
158
186
|
|
159
|
-
const
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
187
|
+
const renderCompactDiff = (diff: DiffResult) => {
|
188
|
+
if (diff.type !== 'modified') {
|
189
|
+
// For added/removed segments, show them as before but in a single line
|
190
|
+
return (
|
191
|
+
<Typography
|
192
|
+
key={diff.path}
|
193
|
+
color={diff.type === 'added' ? 'success.main' : 'error.main'}
|
194
|
+
sx={{ mb: 0.5 }}
|
195
|
+
>
|
196
|
+
{diff.segmentIndex}: {diff.type === 'added' ? '+ ' : '- '}
|
197
|
+
{diff.type === 'added' ? diff.newValue : diff.oldValue}
|
198
|
+
</Typography>
|
199
|
+
)
|
200
|
+
}
|
201
|
+
|
202
|
+
// For modified segments, create a unified inline diff view
|
203
|
+
const oldText = diff.oldValue?.split('"')[1] || ''
|
204
|
+
const newText = diff.newValue?.split('"')[1] || ''
|
205
|
+
const oldWords = oldText.split(' ')
|
206
|
+
const newWords = newText.split(' ')
|
207
|
+
|
208
|
+
// Extract timing info and format with 2 decimal places
|
209
|
+
const timingMatch = diff.newValue?.match(/\(([\d.]+) - ([\d.]+)\)/)
|
210
|
+
const timing = timingMatch ?
|
211
|
+
`(${parseFloat(timingMatch[1]).toFixed(2)} - ${parseFloat(timingMatch[2]).toFixed(2)})` :
|
212
|
+
''
|
166
213
|
|
167
|
-
|
168
|
-
const
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
214
|
+
// Create unified diff of words
|
215
|
+
const unifiedDiff = []
|
216
|
+
let i = 0, j = 0
|
217
|
+
|
218
|
+
while (i < oldWords.length || j < newWords.length) {
|
219
|
+
if (i < oldWords.length && j < newWords.length && oldWords[i] === newWords[j]) {
|
220
|
+
// Unchanged word
|
221
|
+
unifiedDiff.push({ type: 'unchanged', text: oldWords[i] })
|
222
|
+
i++
|
223
|
+
j++
|
224
|
+
} else if (i < oldWords.length && (!newWords[j] || oldWords[i] !== newWords[j])) {
|
225
|
+
// Deleted word
|
226
|
+
unifiedDiff.push({ type: 'deleted', text: oldWords[i] })
|
227
|
+
i++
|
228
|
+
} else if (j < newWords.length) {
|
229
|
+
// Added word
|
230
|
+
unifiedDiff.push({ type: 'added', text: newWords[j] })
|
231
|
+
j++
|
174
232
|
}
|
175
233
|
}
|
176
234
|
|
177
|
-
const isExpanded = diff.segmentIndex !== undefined &&
|
178
|
-
expandedSegments.includes(diff.segmentIndex)
|
179
|
-
|
180
235
|
return (
|
181
|
-
<
|
182
|
-
<
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
+ {diff.newValue}
|
195
|
-
</Typography>
|
196
|
-
)}
|
197
|
-
</Box>
|
198
|
-
{diff.wordChanges && (
|
199
|
-
<IconButton
|
200
|
-
onClick={() => handleToggleSegment(diff.segmentIndex!)}
|
236
|
+
<Box key={diff.path} sx={{ mb: 0.5, display: 'flex', alignItems: 'center' }}>
|
237
|
+
<Typography variant="body2" color="text.secondary" sx={{ mr: 1, minWidth: '30px' }}>
|
238
|
+
{diff.segmentIndex}:
|
239
|
+
</Typography>
|
240
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', flexGrow: 1, alignItems: 'center' }}>
|
241
|
+
{unifiedDiff.map((word, idx) => (
|
242
|
+
<Typography
|
243
|
+
key={idx}
|
244
|
+
component="span"
|
245
|
+
color={
|
246
|
+
word.type === 'unchanged' ? 'text.primary' :
|
247
|
+
word.type === 'deleted' ? 'error.main' : 'success.main'
|
248
|
+
}
|
201
249
|
sx={{
|
202
|
-
|
203
|
-
|
250
|
+
textDecoration: word.type === 'deleted' ? 'line-through' : 'none',
|
251
|
+
mr: 0.5
|
204
252
|
}}
|
205
253
|
>
|
206
|
-
|
207
|
-
</
|
208
|
-
)}
|
254
|
+
{word.text}
|
255
|
+
</Typography>
|
256
|
+
))}
|
257
|
+
<Typography variant="body2" color="text.secondary" sx={{ ml: 1 }}>
|
258
|
+
{timing}
|
259
|
+
</Typography>
|
209
260
|
</Box>
|
210
|
-
|
211
|
-
{diff.wordChanges && (
|
212
|
-
<Collapse in={isExpanded}>
|
213
|
-
<Box sx={{ mt: 2, ml: 4 }}>
|
214
|
-
{diff.wordChanges.map((wordDiff, index) => (
|
215
|
-
<Box key={index}>
|
216
|
-
<Typography color={getColor()} variant="body2">
|
217
|
-
{wordDiff.type.toUpperCase()}: {wordDiff.path}
|
218
|
-
</Typography>
|
219
|
-
{wordDiff.oldValue && (
|
220
|
-
<Typography color="error.main" variant="body2" sx={{ ml: 2 }}>
|
221
|
-
- {wordDiff.oldValue}
|
222
|
-
</Typography>
|
223
|
-
)}
|
224
|
-
{wordDiff.newValue && (
|
225
|
-
<Typography color="success.main" variant="body2" sx={{ ml: 2 }}>
|
226
|
-
+ {wordDiff.newValue}
|
227
|
-
</Typography>
|
228
|
-
)}
|
229
|
-
</Box>
|
230
|
-
))}
|
231
|
-
</Box>
|
232
|
-
</Collapse>
|
233
|
-
)}
|
234
|
-
</Paper>
|
261
|
+
</Box>
|
235
262
|
)
|
236
263
|
}
|
237
264
|
|
@@ -243,24 +270,41 @@ export default function ReviewChangesModal({
|
|
243
270
|
fullWidth
|
244
271
|
>
|
245
272
|
<DialogTitle>Review Changes</DialogTitle>
|
246
|
-
<DialogContent
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
273
|
+
<DialogContent
|
274
|
+
dividers
|
275
|
+
sx={{
|
276
|
+
p: 0, // Remove default padding
|
277
|
+
'&:first-of-type': { pt: 0 } // Remove default top padding
|
278
|
+
}}
|
279
|
+
>
|
280
|
+
<PreviewVideoSection
|
281
|
+
apiClient={apiClient}
|
282
|
+
isModalOpen={open}
|
283
|
+
updatedData={updatedData}
|
284
|
+
videoRef={videoRef} // Pass the ref to PreviewVideoSection
|
285
|
+
/>
|
286
|
+
|
287
|
+
<Box sx={{ p: 2, mt: 0 }}>
|
288
|
+
{differences.length === 0 ? (
|
289
|
+
<Box>
|
290
|
+
<Typography color="text.secondary">
|
291
|
+
No changes detected. You can still submit to continue processing.
|
292
|
+
</Typography>
|
293
|
+
<Typography variant="body2" color="text.secondary">
|
294
|
+
Total segments: {updatedData.corrected_segments.length}
|
295
|
+
</Typography>
|
296
|
+
</Box>
|
297
|
+
) : (
|
298
|
+
<Box>
|
299
|
+
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
300
|
+
{differences.length} segment{differences.length !== 1 ? 's' : ''} modified:
|
301
|
+
</Typography>
|
302
|
+
<Paper sx={{ p: 2 }}>
|
303
|
+
{differences.map(renderCompactDiff)}
|
304
|
+
</Paper>
|
305
|
+
</Box>
|
306
|
+
)}
|
307
|
+
</Box>
|
264
308
|
</DialogContent>
|
265
309
|
<DialogActions>
|
266
310
|
<Button onClick={onClose}>Cancel</Button>
|
@@ -75,7 +75,6 @@ const TimelineWord = styled(Box)(({ theme }) => ({
|
|
75
75
|
|
76
76
|
const ResizeHandle = styled(Box)(({ theme }) => ({
|
77
77
|
position: 'absolute',
|
78
|
-
right: -4,
|
79
78
|
top: 0,
|
80
79
|
width: 8,
|
81
80
|
height: '100%',
|
@@ -83,6 +82,12 @@ const ResizeHandle = styled(Box)(({ theme }) => ({
|
|
83
82
|
'&:hover': {
|
84
83
|
backgroundColor: theme.palette.primary.light,
|
85
84
|
},
|
85
|
+
'&.left': {
|
86
|
+
left: -4,
|
87
|
+
},
|
88
|
+
'&.right': {
|
89
|
+
right: -4,
|
90
|
+
}
|
86
91
|
}))
|
87
92
|
|
88
93
|
// Add new styled component for the cursor
|
@@ -101,7 +106,7 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
|
|
101
106
|
const containerRef = useRef<HTMLDivElement>(null)
|
102
107
|
const [dragState, setDragState] = useState<{
|
103
108
|
wordIndex: number
|
104
|
-
type: 'move' | 'resize'
|
109
|
+
type: 'move' | 'resize-left' | 'resize-right'
|
105
110
|
initialX: number
|
106
111
|
initialTime: number
|
107
112
|
word: Word
|
@@ -116,40 +121,22 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
|
|
116
121
|
isResize: boolean
|
117
122
|
): boolean => {
|
118
123
|
if (isResize) {
|
119
|
-
// If this is the last word, allow it to extend beyond the timeline
|
120
124
|
if (currentIndex === words.length - 1) return false;
|
121
125
|
|
122
126
|
const nextWord = words[currentIndex + 1]
|
123
|
-
if (!nextWord) return false
|
124
|
-
|
125
|
-
if (hasCollision) {
|
126
|
-
console.log('Resize collision detected:', {
|
127
|
-
proposedEnd,
|
128
|
-
nextWordStart: nextWord.start_time,
|
129
|
-
word: words[currentIndex].text,
|
130
|
-
nextWord: nextWord.text
|
131
|
-
})
|
132
|
-
}
|
133
|
-
return hasCollision
|
127
|
+
if (!nextWord || nextWord.start_time === null) return false
|
128
|
+
return proposedEnd > nextWord.start_time
|
134
129
|
}
|
135
130
|
|
136
|
-
// For move operations, check all words
|
137
131
|
return words.some((word, index) => {
|
138
132
|
if (index === currentIndex) return false
|
139
|
-
|
133
|
+
if (word.start_time === null || word.end_time === null) return false
|
134
|
+
|
135
|
+
return (
|
140
136
|
(proposedStart >= word.start_time && proposedStart <= word.end_time) ||
|
141
137
|
(proposedEnd >= word.start_time && proposedEnd <= word.end_time) ||
|
142
138
|
(proposedStart <= word.start_time && proposedEnd >= word.end_time)
|
143
139
|
)
|
144
|
-
if (overlap) {
|
145
|
-
console.log('Move collision detected:', {
|
146
|
-
movingWord: words[currentIndex].text,
|
147
|
-
collidingWord: word.text,
|
148
|
-
proposedTimes: { start: proposedStart, end: proposedEnd },
|
149
|
-
collidingTimes: { start: word.start_time, end: word.end_time }
|
150
|
-
})
|
151
|
-
}
|
152
|
-
return overlap
|
153
140
|
})
|
154
141
|
}
|
155
142
|
|
@@ -188,27 +175,22 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
|
|
188
175
|
return marks
|
189
176
|
}
|
190
177
|
|
191
|
-
const handleMouseDown = (e: React.MouseEvent, wordIndex: number, type: 'move' | 'resize') => {
|
178
|
+
const handleMouseDown = (e: React.MouseEvent, wordIndex: number, type: 'move' | 'resize-left' | 'resize-right') => {
|
192
179
|
const rect = containerRef.current?.getBoundingClientRect()
|
193
180
|
if (!rect) return
|
194
181
|
|
182
|
+
const word = words[wordIndex]
|
183
|
+
if (word.start_time === null || word.end_time === null) return
|
184
|
+
|
195
185
|
const initialX = e.clientX - rect.left
|
196
186
|
const initialTime = ((initialX / rect.width) * (endTime - startTime))
|
197
187
|
|
198
|
-
console.log('Mouse down:', {
|
199
|
-
type,
|
200
|
-
wordIndex,
|
201
|
-
initialX,
|
202
|
-
initialTime,
|
203
|
-
word: words[wordIndex]
|
204
|
-
})
|
205
|
-
|
206
188
|
setDragState({
|
207
189
|
wordIndex,
|
208
190
|
type,
|
209
191
|
initialX,
|
210
192
|
initialTime,
|
211
|
-
word
|
193
|
+
word
|
212
194
|
})
|
213
195
|
}
|
214
196
|
|
@@ -219,67 +201,58 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
|
|
219
201
|
const x = e.clientX - rect.left
|
220
202
|
const width = rect.width
|
221
203
|
|
222
|
-
|
223
|
-
|
224
|
-
|
204
|
+
const currentWord = words[dragState.wordIndex]
|
205
|
+
if (currentWord.start_time === null || currentWord.end_time === null ||
|
206
|
+
dragState.word.start_time === null || dragState.word.end_time === null) return
|
207
|
+
|
208
|
+
if (dragState.type === 'resize-right') {
|
225
209
|
const initialWordDuration = dragState.word.end_time - dragState.word.start_time
|
226
210
|
const initialWordWidth = (initialWordDuration / (endTime - startTime)) * width
|
227
|
-
|
228
|
-
// Calculate how much the mouse has moved as a percentage of the initial word width
|
229
211
|
const pixelDelta = x - dragState.initialX
|
230
212
|
const percentageMoved = pixelDelta / initialWordWidth
|
231
213
|
const timeDelta = initialWordDuration * percentageMoved
|
232
214
|
|
233
|
-
console.log('Resize calculation:', {
|
234
|
-
initialWordWidth,
|
235
|
-
initialWordDuration,
|
236
|
-
pixelDelta,
|
237
|
-
percentageMoved,
|
238
|
-
timeDelta,
|
239
|
-
currentDuration: currentWord.end_time - currentWord.start_time
|
240
|
-
})
|
241
|
-
|
242
215
|
const proposedEnd = Math.max(
|
243
216
|
currentWord.start_time + MIN_DURATION,
|
244
|
-
dragState.word.end_time + timeDelta
|
217
|
+
dragState.word.end_time + timeDelta
|
245
218
|
)
|
246
219
|
|
247
|
-
// Check for collisions
|
248
220
|
if (checkCollision(currentWord.start_time, proposedEnd, dragState.wordIndex, true)) return
|
249
221
|
|
250
|
-
// If we get here, the resize is valid
|
251
222
|
onWordUpdate(dragState.wordIndex, {
|
252
223
|
start_time: currentWord.start_time,
|
253
224
|
end_time: proposedEnd
|
254
225
|
})
|
226
|
+
} else if (dragState.type === 'resize-left') {
|
227
|
+
const initialWordDuration = dragState.word.end_time - dragState.word.start_time
|
228
|
+
const initialWordWidth = (initialWordDuration / (endTime - startTime)) * width
|
229
|
+
const pixelDelta = x - dragState.initialX
|
230
|
+
const percentageMoved = pixelDelta / initialWordWidth
|
231
|
+
const timeDelta = initialWordDuration * percentageMoved
|
232
|
+
|
233
|
+
const proposedStart = Math.min(
|
234
|
+
currentWord.end_time - MIN_DURATION,
|
235
|
+
dragState.word.start_time + timeDelta
|
236
|
+
)
|
237
|
+
|
238
|
+
if (checkCollision(proposedStart, currentWord.end_time, dragState.wordIndex, true)) return
|
239
|
+
|
240
|
+
onWordUpdate(dragState.wordIndex, {
|
241
|
+
start_time: proposedStart,
|
242
|
+
end_time: currentWord.end_time
|
243
|
+
})
|
255
244
|
} else if (dragState.type === 'move') {
|
256
|
-
// Use timeline scale for consistent movement
|
257
245
|
const pixelsPerSecond = width / (endTime - startTime)
|
258
246
|
const pixelDelta = x - dragState.initialX
|
259
247
|
const timeDelta = pixelDelta / pixelsPerSecond
|
260
248
|
|
261
|
-
const currentWord = words[dragState.wordIndex]
|
262
249
|
const wordDuration = currentWord.end_time - currentWord.start_time
|
263
|
-
|
264
|
-
console.log('Move calculation:', {
|
265
|
-
timelineWidth: width,
|
266
|
-
timelineDuration: endTime - startTime,
|
267
|
-
pixelsPerSecond,
|
268
|
-
pixelDelta,
|
269
|
-
timeDelta,
|
270
|
-
currentDuration: wordDuration
|
271
|
-
})
|
272
|
-
|
273
250
|
const proposedStart = dragState.word.start_time + timeDelta
|
274
251
|
const proposedEnd = proposedStart + wordDuration
|
275
252
|
|
276
|
-
// Ensure we stay within timeline bounds
|
277
253
|
if (proposedStart < startTime || proposedEnd > endTime) return
|
278
|
-
|
279
|
-
// Check for collisions
|
280
254
|
if (checkCollision(proposedStart, proposedEnd, dragState.wordIndex, false)) return
|
281
255
|
|
282
|
-
// If we get here, the move is valid
|
283
256
|
onWordUpdate(dragState.wordIndex, {
|
284
257
|
start_time: proposedStart,
|
285
258
|
end_time: proposedEnd
|
@@ -292,7 +265,7 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
|
|
292
265
|
}
|
293
266
|
|
294
267
|
const isWordHighlighted = (word: Word): boolean => {
|
295
|
-
if (!currentTime ||
|
268
|
+
if (!currentTime || word.start_time === null || word.end_time === null) return false
|
296
269
|
return currentTime >= word.start_time && currentTime <= word.end_time
|
297
270
|
}
|
298
271
|
|
@@ -332,12 +305,13 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
|
|
332
305
|
/>
|
333
306
|
|
334
307
|
{words.map((word, index) => {
|
308
|
+
// Skip words with null timestamps
|
309
|
+
if (word.start_time === null || word.end_time === null) return null;
|
310
|
+
|
335
311
|
const leftPosition = timeToPosition(word.start_time)
|
336
312
|
const rightPosition = timeToPosition(word.end_time)
|
337
313
|
const width = rightPosition - leftPosition
|
338
|
-
|
339
|
-
// Add visual padding only to right side (2% of total width)
|
340
|
-
const visualPadding = 2 // percentage points
|
314
|
+
const visualPadding = 2
|
341
315
|
const adjustedWidth = Math.max(0, width - visualPadding)
|
342
316
|
|
343
317
|
return (
|
@@ -345,21 +319,28 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
|
|
345
319
|
key={index}
|
346
320
|
className={isWordHighlighted(word) ? 'highlighted' : ''}
|
347
321
|
sx={{
|
348
|
-
left: `${leftPosition}%`,
|
322
|
+
left: `${leftPosition}%`,
|
349
323
|
width: `${adjustedWidth}%`,
|
350
|
-
// Ensure the last word doesn't overflow
|
351
324
|
maxWidth: `calc(${100 - leftPosition}% - 2px)`,
|
352
325
|
}}
|
353
326
|
onMouseDown={(e) => {
|
354
|
-
e.stopPropagation()
|
355
|
-
handleMouseDown(e, index, 'move')
|
327
|
+
e.stopPropagation()
|
328
|
+
handleMouseDown(e, index, 'move')
|
356
329
|
}}
|
357
330
|
>
|
331
|
+
<ResizeHandle
|
332
|
+
className="left"
|
333
|
+
onMouseDown={(e) => {
|
334
|
+
e.stopPropagation()
|
335
|
+
handleMouseDown(e, index, 'resize-left')
|
336
|
+
}}
|
337
|
+
/>
|
358
338
|
{word.text}
|
359
339
|
<ResizeHandle
|
340
|
+
className="right"
|
360
341
|
onMouseDown={(e) => {
|
361
|
-
e.stopPropagation()
|
362
|
-
handleMouseDown(e, index, 'resize')
|
342
|
+
e.stopPropagation()
|
343
|
+
handleMouseDown(e, index, 'resize-right')
|
363
344
|
}}
|
364
345
|
/>
|
365
346
|
</TimelineWord>
|