lyrics-transcriber 0.34.0__py3-none-any.whl → 0.34.2__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/correction/handlers/syllables_match.py +22 -2
- lyrics_transcriber/frontend/.gitignore +23 -0
- lyrics_transcriber/frontend/README.md +50 -0
- lyrics_transcriber/frontend/dist/assets/index-DqFgiUni.js +245 -0
- lyrics_transcriber/frontend/dist/index.html +13 -0
- lyrics_transcriber/frontend/dist/vite.svg +1 -0
- lyrics_transcriber/frontend/eslint.config.js +28 -0
- lyrics_transcriber/frontend/index.html +13 -0
- lyrics_transcriber/frontend/package-lock.json +4260 -0
- lyrics_transcriber/frontend/package.json +37 -0
- lyrics_transcriber/frontend/public/vite.svg +1 -0
- lyrics_transcriber/frontend/src/App.tsx +192 -0
- lyrics_transcriber/frontend/src/api.ts +59 -0
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +155 -0
- lyrics_transcriber/frontend/src/components/DebugPanel.tsx +311 -0
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +297 -0
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +450 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +287 -0
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +157 -0
- lyrics_transcriber/frontend/src/components/constants.ts +19 -0
- lyrics_transcriber/frontend/src/components/styles.ts +13 -0
- lyrics_transcriber/frontend/src/main.tsx +6 -0
- lyrics_transcriber/frontend/src/types.ts +158 -0
- lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
- lyrics_transcriber/frontend/tsconfig.app.json +26 -0
- lyrics_transcriber/frontend/tsconfig.json +25 -0
- lyrics_transcriber/frontend/tsconfig.node.json +23 -0
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
- lyrics_transcriber/frontend/vite.config.d.ts +2 -0
- lyrics_transcriber/frontend/vite.config.js +6 -0
- lyrics_transcriber/frontend/vite.config.ts +7 -0
- lyrics_transcriber/review/server.py +18 -29
- {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/METADATA +1 -1
- {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/RECORD +38 -7
- {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/WHEEL +0 -0
- {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,287 @@
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'react'
|
2
|
+
import { Paper, Typography, Box, Button } from '@mui/material'
|
3
|
+
import { AnchorMatchInfo, LyricsData, LyricsSegment } from '../types'
|
4
|
+
import { FlashType, ModalContent } from './LyricsAnalyzer'
|
5
|
+
import { COLORS } from './constants'
|
6
|
+
import { HighlightedWord } from './styles'
|
7
|
+
|
8
|
+
interface WordClickInfo {
|
9
|
+
wordIndex: number
|
10
|
+
type: 'anchor' | 'gap' | 'other'
|
11
|
+
anchor?: LyricsData['anchor_sequences'][0]
|
12
|
+
gap?: LyricsData['gap_sequences'][0]
|
13
|
+
}
|
14
|
+
|
15
|
+
interface ReferenceViewProps {
|
16
|
+
referenceTexts: Record<string, string>
|
17
|
+
anchors: LyricsData['anchor_sequences']
|
18
|
+
gaps: LyricsData['gap_sequences']
|
19
|
+
onElementClick: (content: ModalContent) => void
|
20
|
+
onWordClick?: (info: WordClickInfo) => void
|
21
|
+
flashingType: FlashType
|
22
|
+
corrected_segments: LyricsSegment[]
|
23
|
+
highlightedWordIndex?: number
|
24
|
+
currentSource: 'genius' | 'spotify'
|
25
|
+
onSourceChange: (source: 'genius' | 'spotify') => void
|
26
|
+
onDebugInfoUpdate?: (info: AnchorMatchInfo[]) => void
|
27
|
+
}
|
28
|
+
|
29
|
+
const normalizeWord = (word: string): string =>
|
30
|
+
word.toLowerCase().replace(/[.,!?']/g, '')
|
31
|
+
|
32
|
+
export default function ReferenceView({
|
33
|
+
referenceTexts,
|
34
|
+
anchors,
|
35
|
+
gaps,
|
36
|
+
onElementClick,
|
37
|
+
onWordClick,
|
38
|
+
flashingType,
|
39
|
+
corrected_segments,
|
40
|
+
highlightedWordIndex,
|
41
|
+
currentSource,
|
42
|
+
onSourceChange,
|
43
|
+
onDebugInfoUpdate
|
44
|
+
}: ReferenceViewProps) {
|
45
|
+
// Create a ref to store debug info to avoid dependency cycles
|
46
|
+
const debugInfoRef = useRef<AnchorMatchInfo[]>([])
|
47
|
+
|
48
|
+
const { newlineIndices } = useMemo(() => {
|
49
|
+
debugInfoRef.current = corrected_segments.map(segment => ({
|
50
|
+
segment: segment.text.trim(),
|
51
|
+
lastWord: '',
|
52
|
+
normalizedLastWord: '',
|
53
|
+
overlappingAnchors: [],
|
54
|
+
matchingGap: null,
|
55
|
+
debugLog: []
|
56
|
+
}));
|
57
|
+
|
58
|
+
const newlineIndices = new Set(
|
59
|
+
corrected_segments.slice(0, -1).map((segment, segmentIndex) => {
|
60
|
+
const segmentText = segment.text.trim()
|
61
|
+
const segmentWords = segmentText.split(/\s+/)
|
62
|
+
const lastWord = segmentWords[segmentWords.length - 1]
|
63
|
+
const normalizedLastWord = normalizeWord(lastWord)
|
64
|
+
|
65
|
+
debugInfoRef.current[segmentIndex].debugLog?.push(
|
66
|
+
`Processing segment: "${segmentText}"\n` +
|
67
|
+
` Words: ${segmentWords.join('|')}\n` +
|
68
|
+
` Last word: "${lastWord}"\n` +
|
69
|
+
` Normalized last word: "${normalizedLastWord}"`
|
70
|
+
)
|
71
|
+
|
72
|
+
// Calculate word position
|
73
|
+
const segmentStartWord = corrected_segments
|
74
|
+
.slice(0, segmentIndex)
|
75
|
+
.reduce((acc, s) => acc + s.text.trim().split(/\s+/).length, 0)
|
76
|
+
const lastWordPosition = segmentStartWord + segmentWords.length - 1
|
77
|
+
|
78
|
+
// Try to find the anchor containing this word
|
79
|
+
const matchingAnchor = anchors.find(a => {
|
80
|
+
const start = a.transcription_position
|
81
|
+
const end = start + a.length - 1
|
82
|
+
const isMatch = lastWordPosition >= start && lastWordPosition <= end
|
83
|
+
|
84
|
+
debugInfoRef.current[segmentIndex].debugLog?.push(
|
85
|
+
`Checking anchor: "${a.text}"\n` +
|
86
|
+
` Position range: ${start}-${end}\n` +
|
87
|
+
` Last word position: ${lastWordPosition}\n` +
|
88
|
+
` Is in range: ${isMatch}\n` +
|
89
|
+
` Words: ${a.words.join('|')}`
|
90
|
+
)
|
91
|
+
|
92
|
+
return isMatch
|
93
|
+
})
|
94
|
+
|
95
|
+
if (matchingAnchor?.reference_positions[currentSource] !== undefined) {
|
96
|
+
const anchorWords = matchingAnchor.words
|
97
|
+
const wordIndex = anchorWords.findIndex(w => {
|
98
|
+
const normalizedAnchorWord = normalizeWord(w)
|
99
|
+
const matches = normalizedAnchorWord === normalizedLastWord
|
100
|
+
|
101
|
+
debugInfoRef.current[segmentIndex].debugLog?.push(
|
102
|
+
`Comparing words:\n` +
|
103
|
+
` Anchor word: "${w}" (normalized: "${normalizedAnchorWord}")\n` +
|
104
|
+
` Segment word: "${lastWord}" (normalized: "${normalizedLastWord}")\n` +
|
105
|
+
` Matches: ${matches}`
|
106
|
+
)
|
107
|
+
|
108
|
+
return matches
|
109
|
+
})
|
110
|
+
|
111
|
+
if (wordIndex !== -1) {
|
112
|
+
const position = matchingAnchor.reference_positions[currentSource] + wordIndex
|
113
|
+
|
114
|
+
debugInfoRef.current[segmentIndex].debugLog?.push(
|
115
|
+
`Found match:\n` +
|
116
|
+
` Word index in anchor: ${wordIndex}\n` +
|
117
|
+
` Reference position: ${matchingAnchor.reference_positions[currentSource]}\n` +
|
118
|
+
` Final position: ${position}`
|
119
|
+
)
|
120
|
+
|
121
|
+
// Update debug info with word matching details
|
122
|
+
debugInfoRef.current[segmentIndex] = {
|
123
|
+
...debugInfoRef.current[segmentIndex],
|
124
|
+
lastWord,
|
125
|
+
normalizedLastWord,
|
126
|
+
overlappingAnchors: [{
|
127
|
+
text: matchingAnchor.text,
|
128
|
+
range: [matchingAnchor.transcription_position, matchingAnchor.transcription_position + matchingAnchor.length - 1],
|
129
|
+
words: anchorWords,
|
130
|
+
hasMatchingWord: true
|
131
|
+
}],
|
132
|
+
wordPositionDebug: {
|
133
|
+
anchorWords,
|
134
|
+
wordIndex,
|
135
|
+
referencePosition: matchingAnchor.reference_positions[currentSource],
|
136
|
+
finalPosition: position,
|
137
|
+
normalizedWords: {
|
138
|
+
anchor: normalizeWord(anchorWords[wordIndex]),
|
139
|
+
segment: normalizedLastWord
|
140
|
+
}
|
141
|
+
}
|
142
|
+
}
|
143
|
+
|
144
|
+
return position
|
145
|
+
}
|
146
|
+
}
|
147
|
+
|
148
|
+
return null
|
149
|
+
}).filter((pos): pos is number => pos !== null && pos >= 0)
|
150
|
+
)
|
151
|
+
return { newlineIndices }
|
152
|
+
}, [corrected_segments, anchors, currentSource])
|
153
|
+
|
154
|
+
// Update debug info whenever it changes
|
155
|
+
useEffect(() => {
|
156
|
+
onDebugInfoUpdate?.(debugInfoRef.current)
|
157
|
+
}, [onDebugInfoUpdate])
|
158
|
+
|
159
|
+
const renderHighlightedText = () => {
|
160
|
+
const elements: React.ReactNode[] = []
|
161
|
+
const words = referenceTexts[currentSource].split(/\s+/)
|
162
|
+
let currentIndex = 0
|
163
|
+
|
164
|
+
words.forEach((word, index) => {
|
165
|
+
// Add the word element
|
166
|
+
const thisWordIndex = currentIndex
|
167
|
+
const anchor = anchors.find(a => {
|
168
|
+
const position = a.reference_positions[currentSource]
|
169
|
+
if (position === undefined) return false
|
170
|
+
return thisWordIndex >= position && thisWordIndex < position + a.length
|
171
|
+
})
|
172
|
+
const correctedGap = gaps.find(g => {
|
173
|
+
if (!g.corrections.length) return false
|
174
|
+
const correction = g.corrections[0]
|
175
|
+
const position = correction.reference_positions?.[currentSource]
|
176
|
+
if (position === undefined) return false
|
177
|
+
return thisWordIndex >= position && thisWordIndex < position + correction.length
|
178
|
+
})
|
179
|
+
|
180
|
+
elements.push(
|
181
|
+
<HighlightedWord
|
182
|
+
key={`${word}-${index}`}
|
183
|
+
shouldFlash={flashingType === 'word' && highlightedWordIndex === thisWordIndex}
|
184
|
+
style={{
|
185
|
+
backgroundColor: flashingType === 'word' && highlightedWordIndex === thisWordIndex
|
186
|
+
? COLORS.highlighted
|
187
|
+
: anchor
|
188
|
+
? COLORS.anchor
|
189
|
+
: correctedGap
|
190
|
+
? COLORS.corrected
|
191
|
+
: 'transparent',
|
192
|
+
padding: (anchor || correctedGap) ? '2px 4px' : '0',
|
193
|
+
borderRadius: '3px',
|
194
|
+
cursor: 'pointer',
|
195
|
+
}}
|
196
|
+
onClick={(e) => {
|
197
|
+
if (e.detail === 1) {
|
198
|
+
setTimeout(() => {
|
199
|
+
if (!e.defaultPrevented) {
|
200
|
+
onWordClick?.({
|
201
|
+
wordIndex: thisWordIndex,
|
202
|
+
type: anchor ? 'anchor' : correctedGap ? 'gap' : 'other',
|
203
|
+
anchor,
|
204
|
+
gap: correctedGap
|
205
|
+
})
|
206
|
+
}
|
207
|
+
}, 200)
|
208
|
+
}
|
209
|
+
}}
|
210
|
+
onDoubleClick={(e) => {
|
211
|
+
e.preventDefault() // Prevent single-click from firing
|
212
|
+
if (anchor) {
|
213
|
+
onElementClick({
|
214
|
+
type: 'anchor',
|
215
|
+
data: {
|
216
|
+
...anchor,
|
217
|
+
position: thisWordIndex
|
218
|
+
}
|
219
|
+
})
|
220
|
+
} else if (correctedGap) {
|
221
|
+
onElementClick({
|
222
|
+
type: 'gap',
|
223
|
+
data: {
|
224
|
+
...correctedGap,
|
225
|
+
position: thisWordIndex,
|
226
|
+
word: word
|
227
|
+
}
|
228
|
+
})
|
229
|
+
}
|
230
|
+
}}
|
231
|
+
>
|
232
|
+
{word}
|
233
|
+
</HighlightedWord>
|
234
|
+
)
|
235
|
+
|
236
|
+
// Check if we need to add a newline after this word
|
237
|
+
if (newlineIndices.has(thisWordIndex)) {
|
238
|
+
elements.push(<br key={`br-${index}`} />)
|
239
|
+
} else {
|
240
|
+
// Only add space if not adding newline
|
241
|
+
elements.push(' ')
|
242
|
+
}
|
243
|
+
|
244
|
+
currentIndex++
|
245
|
+
})
|
246
|
+
|
247
|
+
return elements
|
248
|
+
}
|
249
|
+
|
250
|
+
return (
|
251
|
+
<Paper sx={{ p: 2 }}>
|
252
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
253
|
+
<Typography variant="h6">
|
254
|
+
Reference Text
|
255
|
+
</Typography>
|
256
|
+
<Box>
|
257
|
+
<Button
|
258
|
+
size="small"
|
259
|
+
variant={currentSource === 'genius' ? 'contained' : 'outlined'}
|
260
|
+
onClick={() => onSourceChange('genius')}
|
261
|
+
sx={{ mr: 1 }}
|
262
|
+
>
|
263
|
+
Genius
|
264
|
+
</Button>
|
265
|
+
<Button
|
266
|
+
size="small"
|
267
|
+
variant={currentSource === 'spotify' ? 'contained' : 'outlined'}
|
268
|
+
onClick={() => onSourceChange('spotify')}
|
269
|
+
>
|
270
|
+
Spotify
|
271
|
+
</Button>
|
272
|
+
</Box>
|
273
|
+
</Box>
|
274
|
+
<Typography
|
275
|
+
component="pre"
|
276
|
+
sx={{
|
277
|
+
fontFamily: 'monospace',
|
278
|
+
whiteSpace: 'pre-wrap',
|
279
|
+
margin: 0,
|
280
|
+
lineHeight: 1.5,
|
281
|
+
}}
|
282
|
+
>
|
283
|
+
{renderHighlightedText()}
|
284
|
+
</Typography>
|
285
|
+
</Paper>
|
286
|
+
)
|
287
|
+
}
|
@@ -0,0 +1,157 @@
|
|
1
|
+
import { Paper, Typography } from '@mui/material'
|
2
|
+
import { CorrectionData, AnchorSequence, HighlightInfo } from '../types'
|
3
|
+
import { FlashType, ModalContent } from './LyricsAnalyzer'
|
4
|
+
import { COLORS } from './constants'
|
5
|
+
import { HighlightedWord } from './styles'
|
6
|
+
|
7
|
+
interface WordClickInfo {
|
8
|
+
wordIndex: number
|
9
|
+
type: 'anchor' | 'gap' | 'other'
|
10
|
+
anchor?: AnchorSequence
|
11
|
+
gap?: CorrectionData['gap_sequences'][0]
|
12
|
+
}
|
13
|
+
|
14
|
+
interface TranscriptionViewProps {
|
15
|
+
data: CorrectionData
|
16
|
+
onElementClick: (content: ModalContent) => void
|
17
|
+
onWordClick?: (info: WordClickInfo) => void
|
18
|
+
flashingType: FlashType
|
19
|
+
highlightInfo: HighlightInfo | null
|
20
|
+
}
|
21
|
+
|
22
|
+
export default function TranscriptionView({
|
23
|
+
data,
|
24
|
+
onElementClick,
|
25
|
+
onWordClick,
|
26
|
+
flashingType,
|
27
|
+
highlightInfo
|
28
|
+
}: TranscriptionViewProps) {
|
29
|
+
const renderHighlightedText = () => {
|
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
|
+
)
|
133
|
+
|
134
|
+
wordIndex++
|
135
|
+
return wordElement
|
136
|
+
})
|
137
|
+
}
|
138
|
+
|
139
|
+
return (
|
140
|
+
<Paper sx={{ p: 2 }}>
|
141
|
+
<Typography variant="h6" gutterBottom>
|
142
|
+
Corrected Transcription
|
143
|
+
</Typography>
|
144
|
+
<Typography
|
145
|
+
component="pre"
|
146
|
+
sx={{
|
147
|
+
fontFamily: 'monospace',
|
148
|
+
whiteSpace: 'pre-wrap',
|
149
|
+
margin: 0,
|
150
|
+
lineHeight: 1.5,
|
151
|
+
}}
|
152
|
+
>
|
153
|
+
{renderHighlightedText()}
|
154
|
+
</Typography>
|
155
|
+
</Paper>
|
156
|
+
)
|
157
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import { keyframes } from '@mui/system'
|
2
|
+
|
3
|
+
export const COLORS = {
|
4
|
+
anchor: '#e3f2fd', // Pale blue
|
5
|
+
corrected: '#e8f5e9', // Pale green
|
6
|
+
uncorrectedGap: '#fff3e0', // Pale orange
|
7
|
+
highlighted: '#ffeb3b', // or any color you prefer for highlighting
|
8
|
+
} as const
|
9
|
+
|
10
|
+
export const flashAnimation = keyframes`
|
11
|
+
0%, 100% {
|
12
|
+
opacity: 1;
|
13
|
+
background-color: inherit;
|
14
|
+
}
|
15
|
+
50% {
|
16
|
+
opacity: 0.6;
|
17
|
+
background-color: ${COLORS.highlighted};
|
18
|
+
}
|
19
|
+
`
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import { styled } from '@mui/system'
|
2
|
+
import { flashAnimation } from './constants'
|
3
|
+
|
4
|
+
export const HighlightedWord = styled('span')<{ shouldFlash: boolean }>(
|
5
|
+
({ shouldFlash }) => ({
|
6
|
+
display: 'inline-block',
|
7
|
+
marginRight: '0.25em',
|
8
|
+
transition: 'background-color 0.2s ease',
|
9
|
+
...(shouldFlash && {
|
10
|
+
animation: `${flashAnimation} 0.4s ease-in-out 3`,
|
11
|
+
}),
|
12
|
+
})
|
13
|
+
)
|
@@ -0,0 +1,158 @@
|
|
1
|
+
export interface Word {
|
2
|
+
text: string
|
3
|
+
start_time: number
|
4
|
+
end_time: number
|
5
|
+
confidence?: number
|
6
|
+
}
|
7
|
+
|
8
|
+
export interface LyricsSegment {
|
9
|
+
text: string
|
10
|
+
words: Word[]
|
11
|
+
start_time: number
|
12
|
+
end_time: number
|
13
|
+
}
|
14
|
+
|
15
|
+
export interface WordCorrection {
|
16
|
+
original_word: string
|
17
|
+
corrected_word: string
|
18
|
+
segment_index: number
|
19
|
+
original_position: number
|
20
|
+
source: string
|
21
|
+
confidence: number
|
22
|
+
reason: string
|
23
|
+
alternatives: Record<string, number>
|
24
|
+
is_deletion: boolean
|
25
|
+
split_index?: number
|
26
|
+
split_total?: number
|
27
|
+
reference_positions?: Record<string, number>
|
28
|
+
length: number
|
29
|
+
}
|
30
|
+
|
31
|
+
export interface PhraseScore {
|
32
|
+
phrase_type: string
|
33
|
+
natural_break_score: number
|
34
|
+
length_score: number
|
35
|
+
total_score: number
|
36
|
+
}
|
37
|
+
|
38
|
+
export interface AnchorSequence {
|
39
|
+
words: string[]
|
40
|
+
text: string
|
41
|
+
length: number
|
42
|
+
transcription_position: number
|
43
|
+
reference_positions: Record<string, number>
|
44
|
+
confidence: number
|
45
|
+
phrase_score: PhraseScore
|
46
|
+
total_score: number
|
47
|
+
}
|
48
|
+
|
49
|
+
export interface GapSequence {
|
50
|
+
words: string[]
|
51
|
+
text: string
|
52
|
+
length: number
|
53
|
+
transcription_position: number
|
54
|
+
preceding_anchor: AnchorSequence | null
|
55
|
+
following_anchor: AnchorSequence | null
|
56
|
+
reference_words: {
|
57
|
+
spotify?: string[]
|
58
|
+
genius?: string[]
|
59
|
+
}
|
60
|
+
reference_words_original?: {
|
61
|
+
spotify?: string[]
|
62
|
+
genius?: string[]
|
63
|
+
}
|
64
|
+
corrections: WordCorrection[]
|
65
|
+
}
|
66
|
+
|
67
|
+
export interface LyricsData {
|
68
|
+
transcribed_text: string
|
69
|
+
corrected_text: string
|
70
|
+
original_segments: LyricsSegment[]
|
71
|
+
metadata: {
|
72
|
+
anchor_sequences_count: number
|
73
|
+
gap_sequences_count: number
|
74
|
+
total_words: number
|
75
|
+
correction_ratio: number
|
76
|
+
}
|
77
|
+
anchor_sequences: AnchorSequence[]
|
78
|
+
gap_sequences: GapSequence[]
|
79
|
+
corrected_segments: LyricsSegment[]
|
80
|
+
corrections_made: number
|
81
|
+
confidence: number
|
82
|
+
corrections: WordCorrection[]
|
83
|
+
reference_texts: Record<string, string>
|
84
|
+
}
|
85
|
+
|
86
|
+
export interface CorrectionData {
|
87
|
+
transcribed_text: string
|
88
|
+
original_segments: LyricsSegment[]
|
89
|
+
reference_texts: Record<string, string>
|
90
|
+
anchor_sequences: AnchorSequence[]
|
91
|
+
gap_sequences: GapSequence[]
|
92
|
+
resized_segments: LyricsSegment[]
|
93
|
+
corrected_text: string
|
94
|
+
corrections_made: number
|
95
|
+
confidence: number
|
96
|
+
corrections: WordCorrection[]
|
97
|
+
corrected_segments: LyricsSegment[]
|
98
|
+
metadata: {
|
99
|
+
anchor_sequences_count: number
|
100
|
+
gap_sequences_count: number
|
101
|
+
total_words: number
|
102
|
+
correction_ratio: number
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
106
|
+
export interface HighlightInfo {
|
107
|
+
transcriptionIndex?: number
|
108
|
+
transcriptionLength?: number
|
109
|
+
referenceIndices: {
|
110
|
+
genius?: number
|
111
|
+
spotify?: number
|
112
|
+
}
|
113
|
+
referenceLength?: number
|
114
|
+
type: 'single' | 'gap' | 'anchor'
|
115
|
+
}
|
116
|
+
|
117
|
+
export interface AnchorMatchInfo {
|
118
|
+
segment: string
|
119
|
+
lastWord: string
|
120
|
+
normalizedLastWord: string
|
121
|
+
overlappingAnchors: Array<{
|
122
|
+
text: string
|
123
|
+
range: [number, number]
|
124
|
+
words: string[]
|
125
|
+
hasMatchingWord: boolean
|
126
|
+
}>
|
127
|
+
matchingGap: {
|
128
|
+
text: string
|
129
|
+
position: number
|
130
|
+
length: number
|
131
|
+
corrections: Array<{
|
132
|
+
word: string
|
133
|
+
referencePosition: number | undefined
|
134
|
+
}>
|
135
|
+
followingAnchor: {
|
136
|
+
text: string
|
137
|
+
position: number | undefined
|
138
|
+
} | null
|
139
|
+
} | null
|
140
|
+
highlightDebug?: Array<{
|
141
|
+
wordIndex: number
|
142
|
+
refPos: number | undefined
|
143
|
+
highlightPos: number | undefined
|
144
|
+
anchorLength: number
|
145
|
+
isInRange: boolean
|
146
|
+
}>
|
147
|
+
wordPositionDebug?: {
|
148
|
+
anchorWords: string[]
|
149
|
+
wordIndex: number
|
150
|
+
referencePosition: number
|
151
|
+
finalPosition: number
|
152
|
+
normalizedWords: {
|
153
|
+
anchor: string
|
154
|
+
segment: string
|
155
|
+
}
|
156
|
+
}
|
157
|
+
debugLog?: string[]
|
158
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
/// <reference types="vite/client" />
|
@@ -0,0 +1,26 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
4
|
+
"target": "ES2020",
|
5
|
+
"useDefineForClassFields": true,
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
7
|
+
"module": "ESNext",
|
8
|
+
"skipLibCheck": true,
|
9
|
+
|
10
|
+
/* Bundler mode */
|
11
|
+
"moduleResolution": "bundler",
|
12
|
+
"allowImportingTsExtensions": true,
|
13
|
+
"isolatedModules": true,
|
14
|
+
"moduleDetection": "force",
|
15
|
+
"noEmit": true,
|
16
|
+
"jsx": "react-jsx",
|
17
|
+
|
18
|
+
/* Linting */
|
19
|
+
"strict": true,
|
20
|
+
"noUnusedLocals": true,
|
21
|
+
"noUnusedParameters": true,
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
23
|
+
"noUncheckedSideEffectImports": true
|
24
|
+
},
|
25
|
+
"include": ["src"]
|
26
|
+
}
|