lyrics-transcriber 0.40.0__py3-none-any.whl → 0.42.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/cli/cli_main.py +7 -0
- lyrics_transcriber/core/config.py +1 -0
- 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-coH8y7gV.js} +16284 -9032
- lyrics_transcriber/frontend/dist/assets/index-coH8y7gV.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 +7 -6
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +86 -59
- lyrics_transcriber/frontend/src/components/EditModal.tsx +93 -43
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +2 -2
- lyrics_transcriber/frontend/src/components/Header.tsx +251 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +303 -265
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +117 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +125 -40
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +129 -115
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +59 -78
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +40 -16
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +4 -10
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +137 -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 +35 -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.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 +8 -8
- lyrics_transcriber/output/generator.py +29 -14
- lyrics_transcriber/output/plain_text.py +15 -10
- lyrics_transcriber/output/segment_resizer.py +16 -3
- lyrics_transcriber/output/subtitles.py +56 -2
- 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.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/METADATA +3 -1
- {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/RECORD +76 -63
- {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.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.40.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.40.0.dist-info → lyrics_transcriber-0.42.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 } 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,7 @@ interface ReviewChangesModalProps {
|
|
20
19
|
originalData: CorrectionData
|
21
20
|
updatedData: CorrectionData
|
22
21
|
onSubmit: () => void
|
22
|
+
apiClient: ApiClient | null
|
23
23
|
}
|
24
24
|
|
25
25
|
interface DiffResult {
|
@@ -34,29 +34,29 @@ interface DiffResult {
|
|
34
34
|
// Add interfaces for the word and segment structures
|
35
35
|
interface Word {
|
36
36
|
text: string
|
37
|
-
start_time: number
|
38
|
-
end_time: number
|
37
|
+
start_time: number | null
|
38
|
+
end_time: number | null
|
39
39
|
id?: string
|
40
40
|
}
|
41
41
|
|
42
42
|
interface Segment {
|
43
43
|
text: string
|
44
|
-
start_time: number
|
45
|
-
end_time: number
|
44
|
+
start_time: number | null
|
45
|
+
end_time: number | null
|
46
46
|
words: Word[]
|
47
47
|
id?: string
|
48
48
|
}
|
49
49
|
|
50
50
|
const normalizeWordForComparison = (word: Word): Omit<Word, 'id'> => ({
|
51
51
|
text: word.text,
|
52
|
-
start_time: word.start_time,
|
53
|
-
end_time: word.end_time
|
52
|
+
start_time: word.start_time ?? 0, // Default to 0 for comparison
|
53
|
+
end_time: word.end_time ?? 0 // Default to 0 for comparison
|
54
54
|
})
|
55
55
|
|
56
56
|
const normalizeSegmentForComparison = (segment: Segment): Omit<Segment, 'id'> => ({
|
57
57
|
text: segment.text,
|
58
|
-
start_time: segment.start_time,
|
59
|
-
end_time: segment.end_time,
|
58
|
+
start_time: segment.start_time ?? 0, // Default to 0 for comparison
|
59
|
+
end_time: segment.end_time ?? 0, // Default to 0 for comparison
|
60
60
|
words: segment.words.map(normalizeWordForComparison)
|
61
61
|
})
|
62
62
|
|
@@ -65,14 +65,12 @@ export default function ReviewChangesModal({
|
|
65
65
|
onClose,
|
66
66
|
originalData,
|
67
67
|
updatedData,
|
68
|
-
onSubmit
|
68
|
+
onSubmit,
|
69
|
+
apiClient
|
69
70
|
}: ReviewChangesModalProps) {
|
70
|
-
const [expandedSegments, setExpandedSegments] = useState<number[]>([])
|
71
|
-
|
72
71
|
const differences = useMemo(() => {
|
73
72
|
const diffs: DiffResult[] = []
|
74
73
|
|
75
|
-
// Compare corrected segments
|
76
74
|
originalData.corrected_segments.forEach((originalSegment, index) => {
|
77
75
|
const updatedSegment = updatedData.corrected_segments[index]
|
78
76
|
if (!updatedSegment) {
|
@@ -89,26 +87,26 @@ export default function ReviewChangesModal({
|
|
89
87
|
const normalizedUpdated = normalizeSegmentForComparison(updatedSegment)
|
90
88
|
const wordChanges: DiffResult[] = []
|
91
89
|
|
92
|
-
// Compare word-level changes
|
93
|
-
normalizedOriginal.words.forEach((word
|
90
|
+
// Compare word-level changes
|
91
|
+
normalizedOriginal.words.forEach((word, wordIndex) => {
|
94
92
|
const updatedWord = normalizedUpdated.words[wordIndex]
|
95
93
|
if (!updatedWord) {
|
96
94
|
wordChanges.push({
|
97
95
|
type: 'removed',
|
98
96
|
path: `Word ${wordIndex}`,
|
99
|
-
oldValue: `"${word.text}" (${word.start_time
|
97
|
+
oldValue: `"${word.text}" (${word.start_time?.toFixed(4) ?? 'N/A'} - ${word.end_time?.toFixed(4) ?? 'N/A'})`
|
100
98
|
})
|
101
99
|
return
|
102
100
|
}
|
103
101
|
|
104
102
|
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) {
|
103
|
+
Math.abs((word.start_time ?? 0) - (updatedWord.start_time ?? 0)) > 0.0001 ||
|
104
|
+
Math.abs((word.end_time ?? 0) - (updatedWord.end_time ?? 0)) > 0.0001) {
|
107
105
|
wordChanges.push({
|
108
106
|
type: 'modified',
|
109
107
|
path: `Word ${wordIndex}`,
|
110
|
-
oldValue: `"${word.text}" (${word.start_time
|
111
|
-
newValue: `"${updatedWord.text}" (${updatedWord.start_time
|
108
|
+
oldValue: `"${word.text}" (${word.start_time?.toFixed(4) ?? 'N/A'} - ${word.end_time?.toFixed(4) ?? 'N/A'})`,
|
109
|
+
newValue: `"${updatedWord.text}" (${updatedWord.start_time?.toFixed(4) ?? 'N/A'} - ${updatedWord.end_time?.toFixed(4) ?? 'N/A'})`
|
112
110
|
})
|
113
111
|
}
|
114
112
|
})
|
@@ -120,21 +118,22 @@ export default function ReviewChangesModal({
|
|
120
118
|
wordChanges.push({
|
121
119
|
type: 'added',
|
122
120
|
path: `Word ${i}`,
|
123
|
-
newValue: `"${word.text}" (${word.start_time
|
121
|
+
newValue: `"${word.text}" (${word.start_time?.toFixed(4) ?? 'N/A'} - ${word.end_time?.toFixed(4) ?? 'N/A'})`
|
124
122
|
})
|
125
123
|
}
|
126
124
|
}
|
127
125
|
|
126
|
+
// Compare segment-level changes
|
128
127
|
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 ||
|
128
|
+
Math.abs((normalizedOriginal.start_time ?? 0) - (normalizedUpdated.start_time ?? 0)) > 0.0001 ||
|
129
|
+
Math.abs((normalizedOriginal.end_time ?? 0) - (normalizedUpdated.end_time ?? 0)) > 0.0001 ||
|
131
130
|
wordChanges.length > 0) {
|
132
131
|
diffs.push({
|
133
132
|
type: 'modified',
|
134
133
|
path: `Segment ${index}`,
|
135
134
|
segmentIndex: index,
|
136
|
-
oldValue: `"${normalizedOriginal.text}" (${normalizedOriginal.start_time
|
137
|
-
newValue: `"${normalizedUpdated.text}" (${normalizedUpdated.start_time
|
135
|
+
oldValue: `"${normalizedOriginal.text}" (${normalizedOriginal.start_time?.toFixed(4) ?? 'N/A'} - ${normalizedOriginal.end_time?.toFixed(4) ?? 'N/A'})`,
|
136
|
+
newValue: `"${normalizedUpdated.text}" (${normalizedUpdated.start_time?.toFixed(4) ?? 'N/A'} - ${normalizedUpdated.end_time?.toFixed(4) ?? 'N/A'})`,
|
138
137
|
wordChanges: wordChanges.length > 0 ? wordChanges : undefined
|
139
138
|
})
|
140
139
|
}
|
@@ -148,7 +147,7 @@ export default function ReviewChangesModal({
|
|
148
147
|
type: 'added',
|
149
148
|
path: `Segment ${i}`,
|
150
149
|
segmentIndex: i,
|
151
|
-
newValue: `"${segment.text}" (${segment.start_time
|
150
|
+
newValue: `"${segment.text}" (${segment.start_time?.toFixed(4) ?? 'N/A'} - ${segment.end_time?.toFixed(4) ?? 'N/A'})`
|
152
151
|
})
|
153
152
|
}
|
154
153
|
}
|
@@ -156,82 +155,81 @@ export default function ReviewChangesModal({
|
|
156
155
|
return diffs
|
157
156
|
}, [originalData, updatedData])
|
158
157
|
|
159
|
-
const
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
158
|
+
const renderCompactDiff = (diff: DiffResult) => {
|
159
|
+
if (diff.type !== 'modified') {
|
160
|
+
// For added/removed segments, show them as before but in a single line
|
161
|
+
return (
|
162
|
+
<Typography
|
163
|
+
key={diff.path}
|
164
|
+
color={diff.type === 'added' ? 'success.main' : 'error.main'}
|
165
|
+
sx={{ mb: 0.5 }}
|
166
|
+
>
|
167
|
+
{diff.segmentIndex}: {diff.type === 'added' ? '+ ' : '- '}
|
168
|
+
{diff.type === 'added' ? diff.newValue : diff.oldValue}
|
169
|
+
</Typography>
|
170
|
+
)
|
171
|
+
}
|
172
|
+
|
173
|
+
// For modified segments, create a unified inline diff view
|
174
|
+
const oldText = diff.oldValue?.split('"')[1] || ''
|
175
|
+
const newText = diff.newValue?.split('"')[1] || ''
|
176
|
+
const oldWords = oldText.split(' ')
|
177
|
+
const newWords = newText.split(' ')
|
178
|
+
|
179
|
+
// Extract timing info and format with 2 decimal places
|
180
|
+
const timingMatch = diff.newValue?.match(/\(([\d.]+) - ([\d.]+)\)/)
|
181
|
+
const timing = timingMatch ?
|
182
|
+
`(${parseFloat(timingMatch[1]).toFixed(2)} - ${parseFloat(timingMatch[2]).toFixed(2)})` :
|
183
|
+
''
|
166
184
|
|
167
|
-
|
168
|
-
const
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
185
|
+
// Create unified diff of words
|
186
|
+
const unifiedDiff = []
|
187
|
+
let i = 0, j = 0
|
188
|
+
|
189
|
+
while (i < oldWords.length || j < newWords.length) {
|
190
|
+
if (i < oldWords.length && j < newWords.length && oldWords[i] === newWords[j]) {
|
191
|
+
// Unchanged word
|
192
|
+
unifiedDiff.push({ type: 'unchanged', text: oldWords[i] })
|
193
|
+
i++
|
194
|
+
j++
|
195
|
+
} else if (i < oldWords.length && (!newWords[j] || oldWords[i] !== newWords[j])) {
|
196
|
+
// Deleted word
|
197
|
+
unifiedDiff.push({ type: 'deleted', text: oldWords[i] })
|
198
|
+
i++
|
199
|
+
} else if (j < newWords.length) {
|
200
|
+
// Added word
|
201
|
+
unifiedDiff.push({ type: 'added', text: newWords[j] })
|
202
|
+
j++
|
174
203
|
}
|
175
204
|
}
|
176
205
|
|
177
|
-
const isExpanded = diff.segmentIndex !== undefined &&
|
178
|
-
expandedSegments.includes(diff.segmentIndex)
|
179
|
-
|
180
206
|
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!)}
|
207
|
+
<Box key={diff.path} sx={{ mb: 0.5, display: 'flex', alignItems: 'center' }}>
|
208
|
+
<Typography variant="body2" color="text.secondary" sx={{ mr: 1, minWidth: '30px' }}>
|
209
|
+
{diff.segmentIndex}:
|
210
|
+
</Typography>
|
211
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', flexGrow: 1, alignItems: 'center' }}>
|
212
|
+
{unifiedDiff.map((word, idx) => (
|
213
|
+
<Typography
|
214
|
+
key={idx}
|
215
|
+
component="span"
|
216
|
+
color={
|
217
|
+
word.type === 'unchanged' ? 'text.primary' :
|
218
|
+
word.type === 'deleted' ? 'error.main' : 'success.main'
|
219
|
+
}
|
201
220
|
sx={{
|
202
|
-
|
203
|
-
|
221
|
+
textDecoration: word.type === 'deleted' ? 'line-through' : 'none',
|
222
|
+
mr: 0.5
|
204
223
|
}}
|
205
224
|
>
|
206
|
-
|
207
|
-
</
|
208
|
-
)}
|
225
|
+
{word.text}
|
226
|
+
</Typography>
|
227
|
+
))}
|
228
|
+
<Typography variant="body2" color="text.secondary" sx={{ ml: 1 }}>
|
229
|
+
{timing}
|
230
|
+
</Typography>
|
209
231
|
</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>
|
232
|
+
</Box>
|
235
233
|
)
|
236
234
|
}
|
237
235
|
|
@@ -243,24 +241,40 @@ export default function ReviewChangesModal({
|
|
243
241
|
fullWidth
|
244
242
|
>
|
245
243
|
<DialogTitle>Review Changes</DialogTitle>
|
246
|
-
<DialogContent
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
244
|
+
<DialogContent
|
245
|
+
dividers
|
246
|
+
sx={{
|
247
|
+
p: 0, // Remove default padding
|
248
|
+
'&:first-of-type': { pt: 0 } // Remove default top padding
|
249
|
+
}}
|
250
|
+
>
|
251
|
+
<PreviewVideoSection
|
252
|
+
apiClient={apiClient}
|
253
|
+
isModalOpen={open}
|
254
|
+
updatedData={updatedData}
|
255
|
+
/>
|
256
|
+
|
257
|
+
<Box sx={{ p: 2, mt: 0 }}>
|
258
|
+
{differences.length === 0 ? (
|
259
|
+
<Box>
|
260
|
+
<Typography color="text.secondary">
|
261
|
+
No changes detected. You can still submit to continue processing.
|
262
|
+
</Typography>
|
263
|
+
<Typography variant="body2" color="text.secondary">
|
264
|
+
Total segments: {updatedData.corrected_segments.length}
|
265
|
+
</Typography>
|
266
|
+
</Box>
|
267
|
+
) : (
|
268
|
+
<Box>
|
269
|
+
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
270
|
+
{differences.length} segment{differences.length !== 1 ? 's' : ''} modified:
|
271
|
+
</Typography>
|
272
|
+
<Paper sx={{ p: 2 }}>
|
273
|
+
{differences.map(renderCompactDiff)}
|
274
|
+
</Paper>
|
275
|
+
</Box>
|
276
|
+
)}
|
277
|
+
</Box>
|
264
278
|
</DialogContent>
|
265
279
|
<DialogActions>
|
266
280
|
<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>
|