lyrics-transcriber 0.34.2__py3-none-any.whl → 0.35.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 +10 -1
- lyrics_transcriber/correction/corrector.py +4 -3
- lyrics_transcriber/frontend/dist/assets/index-CQCER5Fo.js +181 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/App.tsx +6 -2
- lyrics_transcriber/frontend/src/api.ts +9 -0
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +155 -0
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +1 -1
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +23 -191
- lyrics_transcriber/frontend/src/components/EditModal.tsx +407 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +255 -221
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +39 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +35 -264
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +232 -0
- lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +315 -0
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +116 -138
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +116 -0
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +243 -0
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +28 -0
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +52 -0
- lyrics_transcriber/frontend/src/components/{constants.ts → shared/constants.ts} +1 -0
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +137 -0
- lyrics_transcriber/frontend/src/components/{styles.ts → shared/styles.ts} +1 -1
- lyrics_transcriber/frontend/src/components/shared/types.ts +99 -0
- lyrics_transcriber/frontend/src/components/shared/utils/newlineCalculator.ts +37 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +76 -0
- lyrics_transcriber/frontend/src/types.ts +2 -43
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/lyrics/spotify.py +11 -0
- lyrics_transcriber/output/generator.py +28 -11
- lyrics_transcriber/review/server.py +38 -12
- {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/METADATA +1 -1
- {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/RECORD +37 -24
- lyrics_transcriber/frontend/dist/assets/index-DqFgiUni.js +0 -245
- lyrics_transcriber/frontend/src/components/DebugPanel.tsx +0 -311
- {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/WHEEL +0 -0
- {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,315 @@
|
|
1
|
+
import { Box, styled } from '@mui/material'
|
2
|
+
import { Word } from '../types'
|
3
|
+
import { useRef, useState } from 'react'
|
4
|
+
|
5
|
+
interface TimelineEditorProps {
|
6
|
+
words: Word[]
|
7
|
+
startTime: number
|
8
|
+
endTime: number
|
9
|
+
onWordUpdate: (index: number, updates: Partial<Word>) => void
|
10
|
+
currentTime?: number
|
11
|
+
}
|
12
|
+
|
13
|
+
const TimelineContainer = styled(Box)(({ theme }) => ({
|
14
|
+
position: 'relative',
|
15
|
+
height: '60px',
|
16
|
+
backgroundColor: theme.palette.grey[200],
|
17
|
+
borderRadius: theme.shape.borderRadius,
|
18
|
+
margin: theme.spacing(2, 0),
|
19
|
+
padding: theme.spacing(0, 1),
|
20
|
+
}))
|
21
|
+
|
22
|
+
const TimelineRuler = styled(Box)(({ theme }) => ({
|
23
|
+
position: 'absolute',
|
24
|
+
top: 0,
|
25
|
+
left: 0,
|
26
|
+
right: 0,
|
27
|
+
height: '16px',
|
28
|
+
borderBottom: `1px solid ${theme.palette.grey[300]}`,
|
29
|
+
}))
|
30
|
+
|
31
|
+
const TimelineMark = styled(Box)(({ theme }) => ({
|
32
|
+
position: 'absolute',
|
33
|
+
top: '10px',
|
34
|
+
width: '1px',
|
35
|
+
height: '6px',
|
36
|
+
backgroundColor: theme.palette.grey[400],
|
37
|
+
}))
|
38
|
+
|
39
|
+
const TimelineLabel = styled(Box)(({ theme }) => ({
|
40
|
+
position: 'absolute',
|
41
|
+
top: 0,
|
42
|
+
transform: 'translateX(-50%)',
|
43
|
+
fontSize: '0.6rem',
|
44
|
+
color: theme.palette.grey[600],
|
45
|
+
}))
|
46
|
+
|
47
|
+
const TimelineWord = styled(Box)(({ theme }) => ({
|
48
|
+
position: 'absolute',
|
49
|
+
height: '30px',
|
50
|
+
top: '22px',
|
51
|
+
backgroundColor: theme.palette.primary.main,
|
52
|
+
borderRadius: theme.shape.borderRadius,
|
53
|
+
color: theme.palette.primary.contrastText,
|
54
|
+
padding: theme.spacing(0.5, 1),
|
55
|
+
cursor: 'move',
|
56
|
+
userSelect: 'none',
|
57
|
+
display: 'flex',
|
58
|
+
alignItems: 'center',
|
59
|
+
fontSize: '0.875rem',
|
60
|
+
transition: 'background-color 0.1s ease',
|
61
|
+
'&.highlighted': {
|
62
|
+
backgroundColor: theme.palette.secondary.main,
|
63
|
+
}
|
64
|
+
}))
|
65
|
+
|
66
|
+
const ResizeHandle = styled(Box)(({ theme }) => ({
|
67
|
+
position: 'absolute',
|
68
|
+
right: -4,
|
69
|
+
top: 0,
|
70
|
+
width: 8,
|
71
|
+
height: '100%',
|
72
|
+
cursor: 'col-resize',
|
73
|
+
'&:hover': {
|
74
|
+
backgroundColor: theme.palette.primary.light,
|
75
|
+
},
|
76
|
+
}))
|
77
|
+
|
78
|
+
export default function TimelineEditor({ words, startTime, endTime, onWordUpdate, currentTime = 0 }: TimelineEditorProps) {
|
79
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
80
|
+
const [dragState, setDragState] = useState<{
|
81
|
+
wordIndex: number
|
82
|
+
type: 'move' | 'resize'
|
83
|
+
initialX: number
|
84
|
+
initialTime: number
|
85
|
+
word: Word
|
86
|
+
} | null>(null)
|
87
|
+
|
88
|
+
const MIN_DURATION = 0.1 // Minimum word duration in seconds
|
89
|
+
|
90
|
+
const checkCollision = (
|
91
|
+
proposedStart: number,
|
92
|
+
proposedEnd: number,
|
93
|
+
currentIndex: number,
|
94
|
+
isResize: boolean
|
95
|
+
): boolean => {
|
96
|
+
if (isResize) {
|
97
|
+
// If this is the last word, allow it to extend beyond the timeline
|
98
|
+
if (currentIndex === words.length - 1) return false;
|
99
|
+
|
100
|
+
const nextWord = words[currentIndex + 1]
|
101
|
+
if (!nextWord) return false
|
102
|
+
const hasCollision = proposedEnd > nextWord.start_time
|
103
|
+
if (hasCollision) {
|
104
|
+
console.log('Resize collision detected:', {
|
105
|
+
proposedEnd,
|
106
|
+
nextWordStart: nextWord.start_time,
|
107
|
+
word: words[currentIndex].text,
|
108
|
+
nextWord: nextWord.text
|
109
|
+
})
|
110
|
+
}
|
111
|
+
return hasCollision
|
112
|
+
}
|
113
|
+
|
114
|
+
// For move operations, check all words
|
115
|
+
return words.some((word, index) => {
|
116
|
+
if (index === currentIndex) return false
|
117
|
+
const overlap = (
|
118
|
+
(proposedStart >= word.start_time && proposedStart <= word.end_time) ||
|
119
|
+
(proposedEnd >= word.start_time && proposedEnd <= word.end_time) ||
|
120
|
+
(proposedStart <= word.start_time && proposedEnd >= word.end_time)
|
121
|
+
)
|
122
|
+
if (overlap) {
|
123
|
+
console.log('Move collision detected:', {
|
124
|
+
movingWord: words[currentIndex].text,
|
125
|
+
collidingWord: word.text,
|
126
|
+
proposedTimes: { start: proposedStart, end: proposedEnd },
|
127
|
+
collidingTimes: { start: word.start_time, end: word.end_time }
|
128
|
+
})
|
129
|
+
}
|
130
|
+
return overlap
|
131
|
+
})
|
132
|
+
}
|
133
|
+
|
134
|
+
const timeToPosition = (time: number): number => {
|
135
|
+
const duration = endTime - startTime
|
136
|
+
const position = ((time - startTime) / duration) * 100
|
137
|
+
return Math.max(0, Math.min(100, position))
|
138
|
+
}
|
139
|
+
|
140
|
+
const generateTimelineMarks = () => {
|
141
|
+
const marks = []
|
142
|
+
const startSecond = Math.floor(startTime)
|
143
|
+
const endSecond = Math.ceil(endTime)
|
144
|
+
|
145
|
+
for (let time = startSecond; time <= endSecond; time++) {
|
146
|
+
if (time >= startTime && time <= endTime) {
|
147
|
+
const position = timeToPosition(time)
|
148
|
+
marks.push(
|
149
|
+
<Box key={time}>
|
150
|
+
<TimelineMark sx={{ left: `${position}%` }} />
|
151
|
+
<TimelineLabel sx={{ left: `${position}%` }}>
|
152
|
+
{time}s
|
153
|
+
</TimelineLabel>
|
154
|
+
</Box>
|
155
|
+
)
|
156
|
+
}
|
157
|
+
}
|
158
|
+
return marks
|
159
|
+
}
|
160
|
+
|
161
|
+
const handleMouseDown = (e: React.MouseEvent, wordIndex: number, type: 'move' | 'resize') => {
|
162
|
+
const rect = containerRef.current?.getBoundingClientRect()
|
163
|
+
if (!rect) return
|
164
|
+
|
165
|
+
const initialX = e.clientX - rect.left
|
166
|
+
const initialTime = ((initialX / rect.width) * (endTime - startTime))
|
167
|
+
|
168
|
+
console.log('Mouse down:', {
|
169
|
+
type,
|
170
|
+
wordIndex,
|
171
|
+
initialX,
|
172
|
+
initialTime,
|
173
|
+
word: words[wordIndex]
|
174
|
+
})
|
175
|
+
|
176
|
+
setDragState({
|
177
|
+
wordIndex,
|
178
|
+
type,
|
179
|
+
initialX,
|
180
|
+
initialTime,
|
181
|
+
word: words[wordIndex]
|
182
|
+
})
|
183
|
+
}
|
184
|
+
|
185
|
+
const handleMouseMove = (e: React.MouseEvent) => {
|
186
|
+
if (!dragState || !containerRef.current) return
|
187
|
+
|
188
|
+
const rect = containerRef.current.getBoundingClientRect()
|
189
|
+
const x = e.clientX - rect.left
|
190
|
+
const width = rect.width
|
191
|
+
|
192
|
+
if (dragState.type === 'resize') {
|
193
|
+
const currentWord = words[dragState.wordIndex]
|
194
|
+
// Use the initial word duration for consistent scaling
|
195
|
+
const initialWordDuration = dragState.word.end_time - dragState.word.start_time
|
196
|
+
const initialWordWidth = (initialWordDuration / (endTime - startTime)) * width
|
197
|
+
|
198
|
+
// Calculate how much the mouse has moved as a percentage of the initial word width
|
199
|
+
const pixelDelta = x - dragState.initialX
|
200
|
+
const percentageMoved = pixelDelta / initialWordWidth
|
201
|
+
const timeDelta = initialWordDuration * percentageMoved
|
202
|
+
|
203
|
+
console.log('Resize calculation:', {
|
204
|
+
initialWordWidth,
|
205
|
+
initialWordDuration,
|
206
|
+
pixelDelta,
|
207
|
+
percentageMoved,
|
208
|
+
timeDelta,
|
209
|
+
currentDuration: currentWord.end_time - currentWord.start_time
|
210
|
+
})
|
211
|
+
|
212
|
+
const proposedEnd = Math.max(
|
213
|
+
currentWord.start_time + MIN_DURATION,
|
214
|
+
dragState.word.end_time + timeDelta // Use initial end time as reference
|
215
|
+
)
|
216
|
+
|
217
|
+
// Check for collisions
|
218
|
+
if (checkCollision(currentWord.start_time, proposedEnd, dragState.wordIndex, true)) return
|
219
|
+
|
220
|
+
// If we get here, the resize is valid
|
221
|
+
onWordUpdate(dragState.wordIndex, {
|
222
|
+
start_time: currentWord.start_time,
|
223
|
+
end_time: proposedEnd
|
224
|
+
})
|
225
|
+
} else if (dragState.type === 'move') {
|
226
|
+
// Use timeline scale for consistent movement
|
227
|
+
const pixelsPerSecond = width / (endTime - startTime)
|
228
|
+
const pixelDelta = x - dragState.initialX
|
229
|
+
const timeDelta = pixelDelta / pixelsPerSecond
|
230
|
+
|
231
|
+
const currentWord = words[dragState.wordIndex]
|
232
|
+
const wordDuration = currentWord.end_time - currentWord.start_time
|
233
|
+
|
234
|
+
console.log('Move calculation:', {
|
235
|
+
timelineWidth: width,
|
236
|
+
timelineDuration: endTime - startTime,
|
237
|
+
pixelsPerSecond,
|
238
|
+
pixelDelta,
|
239
|
+
timeDelta,
|
240
|
+
currentDuration: wordDuration
|
241
|
+
})
|
242
|
+
|
243
|
+
const proposedStart = dragState.word.start_time + timeDelta
|
244
|
+
const proposedEnd = proposedStart + wordDuration
|
245
|
+
|
246
|
+
// Ensure we stay within timeline bounds
|
247
|
+
if (proposedStart < startTime || proposedEnd > endTime) return
|
248
|
+
|
249
|
+
// Check for collisions
|
250
|
+
if (checkCollision(proposedStart, proposedEnd, dragState.wordIndex, false)) return
|
251
|
+
|
252
|
+
// If we get here, the move is valid
|
253
|
+
onWordUpdate(dragState.wordIndex, {
|
254
|
+
start_time: proposedStart,
|
255
|
+
end_time: proposedEnd
|
256
|
+
})
|
257
|
+
}
|
258
|
+
}
|
259
|
+
|
260
|
+
const handleMouseUp = () => {
|
261
|
+
setDragState(null)
|
262
|
+
}
|
263
|
+
|
264
|
+
const isWordHighlighted = (word: Word): boolean => {
|
265
|
+
if (!currentTime || !word.start_time || !word.end_time) return false
|
266
|
+
return currentTime >= word.start_time && currentTime <= word.end_time
|
267
|
+
}
|
268
|
+
|
269
|
+
return (
|
270
|
+
<TimelineContainer
|
271
|
+
ref={containerRef}
|
272
|
+
onMouseMove={handleMouseMove}
|
273
|
+
onMouseUp={handleMouseUp}
|
274
|
+
onMouseLeave={handleMouseUp}
|
275
|
+
>
|
276
|
+
<TimelineRuler>
|
277
|
+
{generateTimelineMarks()}
|
278
|
+
</TimelineRuler>
|
279
|
+
{words.map((word, index) => {
|
280
|
+
const leftPosition = timeToPosition(word.start_time)
|
281
|
+
const rightPosition = timeToPosition(word.end_time)
|
282
|
+
const width = rightPosition - leftPosition
|
283
|
+
|
284
|
+
// Add visual padding only to right side (2% of total width)
|
285
|
+
const visualPadding = 2 // percentage points
|
286
|
+
const adjustedWidth = Math.max(0, width - visualPadding)
|
287
|
+
|
288
|
+
return (
|
289
|
+
<TimelineWord
|
290
|
+
key={index}
|
291
|
+
className={isWordHighlighted(word) ? 'highlighted' : ''}
|
292
|
+
sx={{
|
293
|
+
left: `${leftPosition}%`, // No adjustment to left position
|
294
|
+
width: `${adjustedWidth}%`,
|
295
|
+
// Ensure the last word doesn't overflow
|
296
|
+
maxWidth: `calc(${100 - leftPosition}% - 2px)`,
|
297
|
+
}}
|
298
|
+
onMouseDown={(e) => {
|
299
|
+
e.stopPropagation(); // Prevent the parent's mousedown from firing
|
300
|
+
handleMouseDown(e, index, 'move');
|
301
|
+
}}
|
302
|
+
>
|
303
|
+
{word.text}
|
304
|
+
<ResizeHandle
|
305
|
+
onMouseDown={(e) => {
|
306
|
+
e.stopPropagation(); // Prevent the parent's mousedown from firing
|
307
|
+
handleMouseDown(e, index, 'resize');
|
308
|
+
}}
|
309
|
+
/>
|
310
|
+
</TimelineWord>
|
311
|
+
)
|
312
|
+
})}
|
313
|
+
</TimelineContainer>
|
314
|
+
)
|
315
|
+
}
|
@@ -1,157 +1,135 @@
|
|
1
|
-
import {
|
2
|
-
import {
|
3
|
-
import {
|
4
|
-
import {
|
5
|
-
import {
|
1
|
+
import { useState } from 'react'
|
2
|
+
import { Paper, Typography, Box, IconButton } from '@mui/material'
|
3
|
+
import { TranscriptionViewProps } from './shared/types'
|
4
|
+
import { HighlightedText } from './shared/components/HighlightedText'
|
5
|
+
import { styled } from '@mui/material/styles'
|
6
|
+
import SegmentDetailsModal from './SegmentDetailsModal'
|
7
|
+
import { TranscriptionWordPosition } from './shared/types'
|
8
|
+
import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
|
6
9
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
const SegmentIndex = styled(Typography)(({ theme }) => ({
|
11
|
+
color: theme.palette.text.secondary,
|
12
|
+
width: '2em',
|
13
|
+
minWidth: '2em',
|
14
|
+
textAlign: 'right',
|
15
|
+
marginRight: theme.spacing(1),
|
16
|
+
userSelect: 'none',
|
17
|
+
fontFamily: 'monospace',
|
18
|
+
cursor: 'pointer',
|
19
|
+
paddingTop: '3px',
|
20
|
+
'&:hover': {
|
21
|
+
textDecoration: 'underline',
|
22
|
+
},
|
23
|
+
}))
|
13
24
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
25
|
+
const TextContainer = styled(Box)({
|
26
|
+
flex: 1,
|
27
|
+
minWidth: 0,
|
28
|
+
})
|
29
|
+
|
30
|
+
const SegmentControls = styled(Box)({
|
31
|
+
display: 'flex',
|
32
|
+
alignItems: 'center',
|
33
|
+
gap: '4px',
|
34
|
+
minWidth: '3em',
|
35
|
+
paddingTop: '3px',
|
36
|
+
paddingRight: '8px'
|
37
|
+
})
|
21
38
|
|
22
39
|
export default function TranscriptionView({
|
23
40
|
data,
|
24
41
|
onElementClick,
|
25
42
|
onWordClick,
|
26
43
|
flashingType,
|
27
|
-
highlightInfo
|
44
|
+
highlightInfo,
|
45
|
+
mode,
|
46
|
+
onPlaySegment,
|
47
|
+
currentTime = 0
|
28
48
|
}: TranscriptionViewProps) {
|
29
|
-
const
|
30
|
-
const normalizedText = data.corrected_text.replace(/\n\n+/g, '\n')
|
31
|
-
const words = normalizedText.split(/(\s+)/)
|
32
|
-
let wordIndex = 0 // Track actual word positions, ignoring whitespace
|
33
|
-
|
34
|
-
// Build a map of original positions to their corrections
|
35
|
-
const correctionMap = new Map()
|
36
|
-
data.gap_sequences.forEach(gap => {
|
37
|
-
gap.corrections.forEach(c => {
|
38
|
-
correctionMap.set(c.original_position, {
|
39
|
-
original: c.original_word,
|
40
|
-
corrected: c.corrected_word,
|
41
|
-
is_deletion: c.is_deletion,
|
42
|
-
split_total: c.split_total
|
43
|
-
})
|
44
|
-
})
|
45
|
-
})
|
46
|
-
|
47
|
-
return words.map((word, index) => {
|
48
|
-
if (/^\s+$/.test(word)) {
|
49
|
-
return word
|
50
|
-
}
|
51
|
-
|
52
|
-
const currentWordIndex = wordIndex
|
53
|
-
const anchor = data.anchor_sequences.find(anchor => {
|
54
|
-
const start = anchor.transcription_position
|
55
|
-
const end = start + anchor.length
|
56
|
-
return currentWordIndex >= start && currentWordIndex < end
|
57
|
-
})
|
58
|
-
|
59
|
-
const gap = data.gap_sequences.find(g => {
|
60
|
-
const start = g.transcription_position
|
61
|
-
const end = start + g.length
|
62
|
-
return currentWordIndex >= start && currentWordIndex < end
|
63
|
-
})
|
64
|
-
|
65
|
-
const hasCorrections = gap ? gap.corrections.length > 0 : false
|
66
|
-
|
67
|
-
const shouldFlash = Boolean(
|
68
|
-
(flashingType === 'anchor' && anchor) ||
|
69
|
-
(flashingType === 'corrected' && hasCorrections) ||
|
70
|
-
(flashingType === 'uncorrected' && gap && !hasCorrections) ||
|
71
|
-
(flashingType === 'word' && highlightInfo?.type === 'anchor' && anchor && (
|
72
|
-
anchor.transcription_position === highlightInfo.transcriptionIndex &&
|
73
|
-
currentWordIndex >= anchor.transcription_position &&
|
74
|
-
currentWordIndex < anchor.transcription_position + anchor.length
|
75
|
-
))
|
76
|
-
)
|
77
|
-
|
78
|
-
const wordElement = (
|
79
|
-
<HighlightedWord
|
80
|
-
key={`${word}-${index}-${shouldFlash}`}
|
81
|
-
shouldFlash={shouldFlash}
|
82
|
-
style={{
|
83
|
-
backgroundColor: anchor
|
84
|
-
? COLORS.anchor
|
85
|
-
: hasCorrections
|
86
|
-
? COLORS.corrected
|
87
|
-
: gap
|
88
|
-
? COLORS.uncorrectedGap
|
89
|
-
: 'transparent',
|
90
|
-
padding: anchor || gap ? '2px 4px' : '0',
|
91
|
-
borderRadius: '3px',
|
92
|
-
cursor: 'pointer',
|
93
|
-
}}
|
94
|
-
onClick={(e) => {
|
95
|
-
if (e.detail === 1) {
|
96
|
-
setTimeout(() => {
|
97
|
-
if (!e.defaultPrevented) {
|
98
|
-
onWordClick?.({
|
99
|
-
wordIndex: currentWordIndex,
|
100
|
-
type: anchor ? 'anchor' : gap ? 'gap' : 'other',
|
101
|
-
anchor,
|
102
|
-
gap
|
103
|
-
})
|
104
|
-
}
|
105
|
-
}, 200)
|
106
|
-
}
|
107
|
-
}}
|
108
|
-
onDoubleClick={(e) => {
|
109
|
-
e.preventDefault() // Prevent single-click from firing
|
110
|
-
if (anchor) {
|
111
|
-
onElementClick({
|
112
|
-
type: 'anchor',
|
113
|
-
data: {
|
114
|
-
...anchor,
|
115
|
-
position: currentWordIndex
|
116
|
-
}
|
117
|
-
})
|
118
|
-
} else if (gap) {
|
119
|
-
onElementClick({
|
120
|
-
type: 'gap',
|
121
|
-
data: {
|
122
|
-
...gap,
|
123
|
-
position: currentWordIndex,
|
124
|
-
word: word
|
125
|
-
}
|
126
|
-
})
|
127
|
-
}
|
128
|
-
}}
|
129
|
-
>
|
130
|
-
{word}
|
131
|
-
</HighlightedWord>
|
132
|
-
)
|
49
|
+
const [selectedSegmentIndex, setSelectedSegmentIndex] = useState<number | null>(null)
|
133
50
|
|
134
|
-
|
135
|
-
|
136
|
-
})
|
137
|
-
}
|
51
|
+
// Keep track of global word position
|
52
|
+
let globalWordPosition = 0
|
138
53
|
|
139
54
|
return (
|
140
55
|
<Paper sx={{ p: 2 }}>
|
141
56
|
<Typography variant="h6" gutterBottom>
|
142
57
|
Corrected Transcription
|
143
58
|
</Typography>
|
144
|
-
<
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
59
|
+
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
60
|
+
{data.corrected_segments.map((segment, segmentIndex) => {
|
61
|
+
// Convert segment words to TranscriptionWordPosition format
|
62
|
+
const segmentWords: TranscriptionWordPosition[] = segment.words.map((word, idx) => {
|
63
|
+
const position = globalWordPosition + idx
|
64
|
+
const anchor = data.anchor_sequences.find(a =>
|
65
|
+
position >= a.transcription_position &&
|
66
|
+
position < a.transcription_position + a.length
|
67
|
+
)
|
68
|
+
const gap = !anchor ? data.gap_sequences.find(g =>
|
69
|
+
position >= g.transcription_position &&
|
70
|
+
position < g.transcription_position + g.length
|
71
|
+
) : undefined
|
72
|
+
|
73
|
+
return {
|
74
|
+
word: {
|
75
|
+
text: word.text,
|
76
|
+
start_time: word.start_time,
|
77
|
+
end_time: word.end_time
|
78
|
+
},
|
79
|
+
position,
|
80
|
+
type: anchor ? 'anchor' : gap ? 'gap' : 'other',
|
81
|
+
sequence: anchor || gap,
|
82
|
+
isInRange: true
|
83
|
+
}
|
84
|
+
})
|
85
|
+
|
86
|
+
// Update global position counter for next segment
|
87
|
+
globalWordPosition += segment.words.length
|
88
|
+
|
89
|
+
return (
|
90
|
+
<Box key={segmentIndex} sx={{ display: 'flex', alignItems: 'flex-start', width: '100%' }}>
|
91
|
+
<SegmentControls>
|
92
|
+
<SegmentIndex
|
93
|
+
variant="body2"
|
94
|
+
onClick={() => setSelectedSegmentIndex(segmentIndex)}
|
95
|
+
>
|
96
|
+
{segmentIndex}
|
97
|
+
</SegmentIndex>
|
98
|
+
{segment.start_time !== undefined && (
|
99
|
+
<IconButton
|
100
|
+
size="small"
|
101
|
+
onClick={() => onPlaySegment?.(segment.start_time)}
|
102
|
+
sx={{ padding: '2px' }}
|
103
|
+
>
|
104
|
+
<PlayCircleOutlineIcon fontSize="small" />
|
105
|
+
</IconButton>
|
106
|
+
)}
|
107
|
+
</SegmentControls>
|
108
|
+
<TextContainer>
|
109
|
+
<HighlightedText
|
110
|
+
wordPositions={segmentWords}
|
111
|
+
anchors={data.anchor_sequences}
|
112
|
+
gaps={data.gap_sequences}
|
113
|
+
onElementClick={onElementClick}
|
114
|
+
onWordClick={onWordClick}
|
115
|
+
flashingType={flashingType}
|
116
|
+
highlightInfo={highlightInfo}
|
117
|
+
mode={mode}
|
118
|
+
preserveSegments={true}
|
119
|
+
currentTime={currentTime}
|
120
|
+
/>
|
121
|
+
</TextContainer>
|
122
|
+
</Box>
|
123
|
+
)
|
124
|
+
})}
|
125
|
+
</Box>
|
126
|
+
|
127
|
+
<SegmentDetailsModal
|
128
|
+
open={selectedSegmentIndex !== null}
|
129
|
+
onClose={() => setSelectedSegmentIndex(null)}
|
130
|
+
segment={selectedSegmentIndex !== null ? data.corrected_segments[selectedSegmentIndex] : null}
|
131
|
+
segmentIndex={selectedSegmentIndex}
|
132
|
+
/>
|
155
133
|
</Paper>
|
156
134
|
)
|
157
135
|
}
|