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,311 @@
|
|
1
|
+
import { Box, Typography, Accordion, AccordionSummary, AccordionDetails } from '@mui/material'
|
2
|
+
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
3
|
+
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
4
|
+
import { CorrectionData } from '../types'
|
5
|
+
import { useMemo, useRef, useState } from 'react'
|
6
|
+
|
7
|
+
export interface AnchorMatchInfo {
|
8
|
+
segment: string
|
9
|
+
lastWord: string
|
10
|
+
normalizedLastWord: string
|
11
|
+
overlappingAnchors: Array<{
|
12
|
+
text: string
|
13
|
+
range: [number, number]
|
14
|
+
words: string[]
|
15
|
+
hasMatchingWord: boolean
|
16
|
+
}>
|
17
|
+
matchingGap: {
|
18
|
+
text: string
|
19
|
+
position: number
|
20
|
+
length: number
|
21
|
+
corrections: Array<{
|
22
|
+
word: string
|
23
|
+
referencePosition: number | undefined
|
24
|
+
}>
|
25
|
+
followingAnchor: {
|
26
|
+
text: string
|
27
|
+
position: number | undefined
|
28
|
+
} | null
|
29
|
+
} | null
|
30
|
+
highlightDebug?: Array<{
|
31
|
+
wordIndex: number
|
32
|
+
refPos: number | undefined
|
33
|
+
highlightPos: number | undefined
|
34
|
+
anchorLength: number
|
35
|
+
isInRange: boolean
|
36
|
+
}>
|
37
|
+
wordPositionDebug?: {
|
38
|
+
anchorWords: string[]
|
39
|
+
wordIndex: number
|
40
|
+
referencePosition: number
|
41
|
+
finalPosition: number
|
42
|
+
}
|
43
|
+
debugLog?: string[]
|
44
|
+
}
|
45
|
+
|
46
|
+
interface DebugPanelProps {
|
47
|
+
data: CorrectionData
|
48
|
+
currentSource: 'genius' | 'spotify'
|
49
|
+
anchorMatchInfo: AnchorMatchInfo[]
|
50
|
+
}
|
51
|
+
|
52
|
+
export default function DebugPanel({ data, currentSource, anchorMatchInfo }: DebugPanelProps) {
|
53
|
+
// Create a ref to hold the content div
|
54
|
+
const contentRef = useRef<HTMLDivElement>(null)
|
55
|
+
const [expanded, setExpanded] = useState(false)
|
56
|
+
|
57
|
+
// Calculate newline positions for reference text using useMemo
|
58
|
+
const { newlineInfo, newlineIndices } = useMemo(() => {
|
59
|
+
const newlineInfo = new Map<number, string>()
|
60
|
+
const newlineIndices = new Set(
|
61
|
+
data.corrected_segments.slice(0, -1).map((segment, segmentIndex) => {
|
62
|
+
const segmentWords = segment.text.trim().split(/\s+/)
|
63
|
+
const lastWord = segmentWords[segmentWords.length - 1]
|
64
|
+
|
65
|
+
const matchingScoredAnchor = data.anchor_sequences.find(anchor => {
|
66
|
+
const transcriptionStart = anchor.transcription_position
|
67
|
+
const transcriptionEnd = transcriptionStart + anchor.length - 1
|
68
|
+
const lastWordPosition = data.corrected_segments
|
69
|
+
.slice(0, segmentIndex)
|
70
|
+
.reduce((acc, s) => acc + s.text.trim().split(/\s+/).length, 0) + segmentWords.length - 1
|
71
|
+
|
72
|
+
return lastWordPosition >= transcriptionStart && lastWordPosition <= transcriptionEnd
|
73
|
+
})
|
74
|
+
|
75
|
+
if (!matchingScoredAnchor) {
|
76
|
+
console.warn(`Could not find anchor for segment end: "${segment.text.trim()}"`)
|
77
|
+
return null
|
78
|
+
}
|
79
|
+
|
80
|
+
const refPosition = matchingScoredAnchor.reference_positions[currentSource]
|
81
|
+
if (refPosition === undefined) return null
|
82
|
+
|
83
|
+
const wordOffsetInAnchor = matchingScoredAnchor.words.indexOf(lastWord)
|
84
|
+
const finalPosition = refPosition + wordOffsetInAnchor
|
85
|
+
|
86
|
+
newlineInfo.set(finalPosition, segment.text.trim())
|
87
|
+
return finalPosition
|
88
|
+
}).filter((pos): pos is number => pos !== null)
|
89
|
+
)
|
90
|
+
return { newlineInfo, newlineIndices }
|
91
|
+
}, [data.corrected_segments, data.anchor_sequences, currentSource])
|
92
|
+
|
93
|
+
// Memoize the first 5 segments data
|
94
|
+
const firstFiveSegmentsData = useMemo(() =>
|
95
|
+
data.corrected_segments.slice(0, 5).map((segment, i) => {
|
96
|
+
const segmentWords = segment.text.trim().split(/\s+/)
|
97
|
+
const previousWords = data.corrected_segments
|
98
|
+
.slice(0, i)
|
99
|
+
.reduce((acc, s) => acc + s.text.trim().split(/\s+/).length, 0)
|
100
|
+
const lastWordPosition = previousWords + segmentWords.length - 1
|
101
|
+
|
102
|
+
const matchingScoredAnchor = data.anchor_sequences.find(anchor => {
|
103
|
+
const start = anchor.transcription_position
|
104
|
+
const end = start + anchor.length
|
105
|
+
return lastWordPosition >= start && lastWordPosition < end
|
106
|
+
})
|
107
|
+
|
108
|
+
return {
|
109
|
+
segment,
|
110
|
+
segmentWords,
|
111
|
+
previousWords,
|
112
|
+
lastWordPosition,
|
113
|
+
matchingAnchor: matchingScoredAnchor
|
114
|
+
}
|
115
|
+
}), [data.corrected_segments, data.anchor_sequences])
|
116
|
+
|
117
|
+
// Memoize relevant anchors
|
118
|
+
const relevantAnchors = useMemo(() =>
|
119
|
+
data.anchor_sequences
|
120
|
+
.filter(anchor => anchor.transcription_position < 50),
|
121
|
+
[data.anchor_sequences]
|
122
|
+
)
|
123
|
+
|
124
|
+
// Memoize relevant gaps
|
125
|
+
const relevantGaps = useMemo(() =>
|
126
|
+
data.gap_sequences.filter(g => g.transcription_position < 50),
|
127
|
+
[data.gap_sequences]
|
128
|
+
)
|
129
|
+
|
130
|
+
const handleCopy = (e: React.MouseEvent) => {
|
131
|
+
e.stopPropagation() // Prevent accordion from toggling
|
132
|
+
|
133
|
+
// Temporarily expand to get content
|
134
|
+
setExpanded(true)
|
135
|
+
|
136
|
+
// Use setTimeout to allow the content to render
|
137
|
+
setTimeout(() => {
|
138
|
+
if (contentRef.current) {
|
139
|
+
const debugText = contentRef.current.innerText
|
140
|
+
navigator.clipboard.writeText(debugText)
|
141
|
+
|
142
|
+
// Restore previous state if it was collapsed
|
143
|
+
setExpanded(false)
|
144
|
+
}
|
145
|
+
}, 100)
|
146
|
+
}
|
147
|
+
|
148
|
+
return (
|
149
|
+
<Box sx={{ mb: 3 }}>
|
150
|
+
<Accordion
|
151
|
+
expanded={expanded}
|
152
|
+
onChange={(_, isExpanded) => setExpanded(isExpanded)}
|
153
|
+
>
|
154
|
+
<AccordionSummary
|
155
|
+
expandIcon={<ExpandMoreIcon />}
|
156
|
+
sx={{
|
157
|
+
'& .MuiAccordionSummary-content': {
|
158
|
+
display: 'flex',
|
159
|
+
justifyContent: 'space-between',
|
160
|
+
alignItems: 'center',
|
161
|
+
width: '100%'
|
162
|
+
}
|
163
|
+
}}
|
164
|
+
>
|
165
|
+
<Typography>Debug Information</Typography>
|
166
|
+
<Box
|
167
|
+
onClick={(e) => e.stopPropagation()} // Prevent accordion toggle
|
168
|
+
sx={{ display: 'flex', alignItems: 'center', mr: 2 }}
|
169
|
+
>
|
170
|
+
<Typography
|
171
|
+
component="span"
|
172
|
+
variant="body2"
|
173
|
+
sx={{
|
174
|
+
display: 'flex',
|
175
|
+
alignItems: 'center',
|
176
|
+
cursor: 'pointer',
|
177
|
+
'&:hover': { opacity: 0.7 }
|
178
|
+
}}
|
179
|
+
onClick={handleCopy}
|
180
|
+
>
|
181
|
+
<ContentCopyIcon sx={{ mr: 0.5, fontSize: '1rem' }} />
|
182
|
+
Copy All
|
183
|
+
</Typography>
|
184
|
+
</Box>
|
185
|
+
</AccordionSummary>
|
186
|
+
<AccordionDetails>
|
187
|
+
<Box ref={contentRef} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
188
|
+
{/* Debug Logs */}
|
189
|
+
<Box>
|
190
|
+
<Typography variant="h6" gutterBottom>Debug Logs (first 5 segments)</Typography>
|
191
|
+
<Typography component="pre" sx={{
|
192
|
+
fontSize: '0.75rem',
|
193
|
+
whiteSpace: 'pre-wrap',
|
194
|
+
backgroundColor: '#f5f5f5',
|
195
|
+
padding: 2,
|
196
|
+
borderRadius: 1
|
197
|
+
}}>
|
198
|
+
{anchorMatchInfo.slice(0, 5).map((info, i) =>
|
199
|
+
`Segment ${i + 1}: "${info.segment}"\n` +
|
200
|
+
(info.debugLog ? info.debugLog.map(log => ` ${log}`).join('\n') : ' No debug logs\n')
|
201
|
+
).join('\n')}
|
202
|
+
</Typography>
|
203
|
+
</Box>
|
204
|
+
|
205
|
+
{/* First 5 Segments */}
|
206
|
+
<Box>
|
207
|
+
<Typography variant="h6" gutterBottom>First 5 Segments (with position details)</Typography>
|
208
|
+
<Typography component="pre" sx={{ fontSize: '0.75rem', whiteSpace: 'pre-wrap' }}>
|
209
|
+
{firstFiveSegmentsData.map(({ segment, segmentWords, previousWords, lastWordPosition, matchingAnchor }, index) => {
|
210
|
+
return `Segment ${index + 1}: "${segment.text.trim()}"\n` +
|
211
|
+
` Words: ${segment.words.length} (${segmentWords.length} after trimming)\n` +
|
212
|
+
` Word count before segment: ${previousWords}\n` +
|
213
|
+
` Last word position: ${lastWordPosition}\n` +
|
214
|
+
` Matching anchor: ${matchingAnchor ?
|
215
|
+
`"${matchingAnchor.text}"\n Position: ${matchingAnchor.transcription_position}\n` +
|
216
|
+
` Length: ${matchingAnchor.length}\n` +
|
217
|
+
` Reference positions: genius=${matchingAnchor.reference_positions.genius}, spotify=${matchingAnchor.reference_positions.spotify}`
|
218
|
+
: 'None'}\n`
|
219
|
+
}).join('\n')}
|
220
|
+
</Typography>
|
221
|
+
</Box>
|
222
|
+
|
223
|
+
{/* Relevant Anchors */}
|
224
|
+
<Box>
|
225
|
+
<Typography variant="h6" gutterBottom>Relevant Anchors</Typography>
|
226
|
+
<Typography component="pre" sx={{ fontSize: '0.75rem', whiteSpace: 'pre-wrap' }}>
|
227
|
+
{relevantAnchors.map((anchor, i) => {
|
228
|
+
return `Anchor ${i}: "${anchor.text}"\n` +
|
229
|
+
` Position: ${anchor.transcription_position}\n` +
|
230
|
+
` Length: ${anchor.length}\n` +
|
231
|
+
` Words: ${anchor.words.join(' ')}\n` +
|
232
|
+
` Reference Positions: genius=${anchor.reference_positions.genius}, spotify=${anchor.reference_positions.spotify}\n`
|
233
|
+
}).join('\n')}
|
234
|
+
</Typography>
|
235
|
+
</Box>
|
236
|
+
|
237
|
+
{/* Relevant Gaps */}
|
238
|
+
<Box>
|
239
|
+
<Typography variant="h6" gutterBottom>Relevant Gaps</Typography>
|
240
|
+
<Typography component="pre" sx={{ fontSize: '0.75rem', whiteSpace: 'pre-wrap' }}>
|
241
|
+
{relevantGaps.map((gap, i) => {
|
242
|
+
return `Gap ${i}: "${gap.text}"\n` +
|
243
|
+
` Position: ${gap.transcription_position}\n` +
|
244
|
+
` Length: ${gap.length}\n` +
|
245
|
+
` Words: ${gap.words.join(' ')}\n` +
|
246
|
+
` Corrections: ${gap.corrections.length}\n`
|
247
|
+
}).join('\n')}
|
248
|
+
</Typography>
|
249
|
+
</Box>
|
250
|
+
|
251
|
+
{/* First 5 Newlines */}
|
252
|
+
<Box>
|
253
|
+
<Typography variant="h6" gutterBottom>First 5 Newlines (with detailed anchor matching)</Typography>
|
254
|
+
<Typography component="pre" sx={{ fontSize: '0.75rem', whiteSpace: 'pre-wrap' }}>
|
255
|
+
{Array.from(newlineIndices).sort((a, b) => a - b).slice(0, 5).map(pos => {
|
256
|
+
const matchingAnchor = data.anchor_sequences.find(anchor => {
|
257
|
+
const start = anchor.reference_positions[currentSource]
|
258
|
+
const end = start + anchor.length
|
259
|
+
return pos >= start && pos < end
|
260
|
+
})
|
261
|
+
|
262
|
+
const matchingSegment = data.corrected_segments.find(segment =>
|
263
|
+
newlineInfo.get(pos) === segment.text.trim()
|
264
|
+
)
|
265
|
+
|
266
|
+
const segmentIndex = matchingSegment ? data.corrected_segments.indexOf(matchingSegment) : -1
|
267
|
+
const lastWord = matchingSegment?.text.trim().split(/\s+/).pop()
|
268
|
+
const wordIndex = matchingAnchor?.words.indexOf(lastWord ?? '') ?? -1
|
269
|
+
const expectedPosition = matchingAnchor && wordIndex !== -1 ?
|
270
|
+
matchingAnchor.reference_positions[currentSource] + wordIndex :
|
271
|
+
'Unknown'
|
272
|
+
|
273
|
+
return `Position ${pos}: "${newlineInfo.get(pos)}"\n` +
|
274
|
+
` In Anchor: ${matchingAnchor ? `"${matchingAnchor.text}"` : 'None'}\n` +
|
275
|
+
` Anchor Position: ${matchingAnchor?.reference_positions[currentSource]}\n` +
|
276
|
+
` Matching Segment Index: ${segmentIndex}\n` +
|
277
|
+
` Expected Position in Reference: ${expectedPosition}\n`
|
278
|
+
}).join('\n')}
|
279
|
+
</Typography>
|
280
|
+
</Box>
|
281
|
+
|
282
|
+
{/* Anchor Matching Debug section */}
|
283
|
+
<Box>
|
284
|
+
<Typography variant="h6" gutterBottom>Anchor Matching Debug (first 5 segments)</Typography>
|
285
|
+
<Typography component="pre" sx={{ fontSize: '0.75rem', whiteSpace: 'pre-wrap' }}>
|
286
|
+
{anchorMatchInfo.slice(0, 5).map((info, i) => `
|
287
|
+
Segment ${i}: "${info.segment}"
|
288
|
+
Last word: "${info.lastWord}" (normalized: "${info.normalizedLastWord}")
|
289
|
+
Debug Log:
|
290
|
+
${info.debugLog ? info.debugLog.map(log => ` ${log}`).join('\n') : ' none'}
|
291
|
+
Overlapping anchors:
|
292
|
+
${info.overlappingAnchors.map(anchor => ` "${anchor.text}"
|
293
|
+
Range: ${anchor.range[0]}-${anchor.range[1]}
|
294
|
+
Words: ${anchor.words.join(', ')}
|
295
|
+
Has matching word: ${anchor.hasMatchingWord}
|
296
|
+
`).join('\n')}
|
297
|
+
Word Position Debug: ${info.wordPositionDebug ?
|
298
|
+
`\n Anchor words: ${info.wordPositionDebug.anchorWords.join(', ')}
|
299
|
+
Word index in anchor: ${info.wordPositionDebug.wordIndex}
|
300
|
+
Reference position: ${info.wordPositionDebug.referencePosition}
|
301
|
+
Final position: ${info.wordPositionDebug.finalPosition}`
|
302
|
+
: 'none'}
|
303
|
+
`).join('\n')}
|
304
|
+
</Typography>
|
305
|
+
</Box>
|
306
|
+
</Box>
|
307
|
+
</AccordionDetails>
|
308
|
+
</Accordion>
|
309
|
+
</Box>
|
310
|
+
)
|
311
|
+
}
|
@@ -0,0 +1,297 @@
|
|
1
|
+
import {
|
2
|
+
Dialog,
|
3
|
+
DialogTitle,
|
4
|
+
DialogContent,
|
5
|
+
IconButton,
|
6
|
+
Grid,
|
7
|
+
Typography,
|
8
|
+
Box,
|
9
|
+
TextField,
|
10
|
+
Button,
|
11
|
+
} from '@mui/material'
|
12
|
+
import CloseIcon from '@mui/icons-material/Close'
|
13
|
+
import { ModalContent } from './LyricsAnalyzer'
|
14
|
+
import { WordCorrection } from '../types'
|
15
|
+
import { useState, useEffect } from 'react'
|
16
|
+
|
17
|
+
interface DetailsModalProps {
|
18
|
+
open: boolean
|
19
|
+
content: ModalContent | null
|
20
|
+
onClose: () => void
|
21
|
+
onUpdateCorrection?: (position: number, updatedWords: string[]) => void
|
22
|
+
isReadOnly?: boolean
|
23
|
+
}
|
24
|
+
|
25
|
+
export default function DetailsModal({
|
26
|
+
open,
|
27
|
+
content,
|
28
|
+
onClose,
|
29
|
+
onUpdateCorrection,
|
30
|
+
isReadOnly = true
|
31
|
+
}: DetailsModalProps) {
|
32
|
+
const [editedWord, setEditedWord] = useState('')
|
33
|
+
const [isEditing, setIsEditing] = useState(false)
|
34
|
+
|
35
|
+
useEffect(() => {
|
36
|
+
// Reset editing state when modal content changes
|
37
|
+
if (content?.type === 'gap') {
|
38
|
+
setEditedWord(content.data.word)
|
39
|
+
setIsEditing(false)
|
40
|
+
}
|
41
|
+
}, [content])
|
42
|
+
|
43
|
+
if (!content) return null
|
44
|
+
|
45
|
+
const handleStartEdit = () => {
|
46
|
+
console.group('DetailsModal Edit Debug')
|
47
|
+
console.log('Starting edit for content:', JSON.stringify(content, null, 2))
|
48
|
+
if (content.type === 'gap') {
|
49
|
+
console.log('Setting edited word:', content.data.word)
|
50
|
+
setEditedWord(content.data.word)
|
51
|
+
}
|
52
|
+
console.groupEnd()
|
53
|
+
setIsEditing(true)
|
54
|
+
}
|
55
|
+
|
56
|
+
const handleSaveEdit = () => {
|
57
|
+
console.group('DetailsModal Save Debug')
|
58
|
+
console.log('Current content:', JSON.stringify(content, null, 2))
|
59
|
+
console.log('Edited word:', editedWord)
|
60
|
+
|
61
|
+
if (content?.type === 'gap' && onUpdateCorrection) {
|
62
|
+
// Use the editedWord state instead of the original word
|
63
|
+
console.log('Saving edit with new word:', editedWord)
|
64
|
+
onUpdateCorrection(
|
65
|
+
content.data.position,
|
66
|
+
[editedWord] // Use the edited word here
|
67
|
+
)
|
68
|
+
}
|
69
|
+
console.groupEnd()
|
70
|
+
onClose()
|
71
|
+
}
|
72
|
+
|
73
|
+
const handleCancelEdit = () => {
|
74
|
+
if (content.type === 'gap') {
|
75
|
+
setEditedWord(content.data.word)
|
76
|
+
setIsEditing(false)
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
const handleWordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
81
|
+
console.log('Word changed to:', event.target.value)
|
82
|
+
setEditedWord(event.target.value)
|
83
|
+
}
|
84
|
+
|
85
|
+
const renderContent = () => {
|
86
|
+
switch (content.type) {
|
87
|
+
case 'anchor':
|
88
|
+
return (
|
89
|
+
<Grid container spacing={2}>
|
90
|
+
<GridItem title="Text" value={`"${content.data.text}"`} />
|
91
|
+
<GridItem title="Words" value={content.data.words.join(' ')} />
|
92
|
+
<GridItem title="Position" value={content.data.position} />
|
93
|
+
<GridItem
|
94
|
+
title="Reference Positions"
|
95
|
+
value={
|
96
|
+
<Box component="pre" sx={{ margin: 0, fontSize: '0.875rem' }}>
|
97
|
+
{JSON.stringify(content.data.reference_positions, null, 2)}
|
98
|
+
</Box>
|
99
|
+
}
|
100
|
+
/>
|
101
|
+
<GridItem
|
102
|
+
title="Confidence"
|
103
|
+
value={`${(content.data.confidence * 100).toFixed(2)}%`}
|
104
|
+
/>
|
105
|
+
<GridItem title="Length" value={`${content.data.length} words`} />
|
106
|
+
{content.data.phrase_score && (
|
107
|
+
<>
|
108
|
+
<GridItem title="Phrase Type" value={content.data.phrase_score.phrase_type} />
|
109
|
+
<GridItem
|
110
|
+
title="Scores"
|
111
|
+
value={
|
112
|
+
<Box sx={{ pl: 2 }}>
|
113
|
+
<Typography>
|
114
|
+
Total: {content.data?.total_score?.toFixed(2) ?? 'N/A'}
|
115
|
+
</Typography>
|
116
|
+
<Typography>
|
117
|
+
Natural Break: {content.data?.phrase_score?.natural_break_score?.toFixed(2) ?? 'N/A'}
|
118
|
+
</Typography>
|
119
|
+
<Typography>
|
120
|
+
Length: {content.data.phrase_score.length_score.toFixed(2)}
|
121
|
+
</Typography>
|
122
|
+
<Typography>
|
123
|
+
Phrase: {content.data.phrase_score.total_score.toFixed(2)}
|
124
|
+
</Typography>
|
125
|
+
</Box>
|
126
|
+
}
|
127
|
+
/>
|
128
|
+
</>
|
129
|
+
)}
|
130
|
+
</Grid>
|
131
|
+
)
|
132
|
+
|
133
|
+
case 'gap':
|
134
|
+
return (
|
135
|
+
<Grid container spacing={2}>
|
136
|
+
<GridItem
|
137
|
+
title="Transcribed Text"
|
138
|
+
value={`"${content.data.text}"`}
|
139
|
+
/>
|
140
|
+
<GridItem
|
141
|
+
title="Current Text"
|
142
|
+
value={
|
143
|
+
isEditing ? (
|
144
|
+
<Box>
|
145
|
+
<TextField
|
146
|
+
value={editedWord}
|
147
|
+
onChange={handleWordChange}
|
148
|
+
fullWidth
|
149
|
+
label="Edit word"
|
150
|
+
variant="outlined"
|
151
|
+
size="small"
|
152
|
+
/>
|
153
|
+
<Box sx={{ display: 'flex', gap: 1 }}>
|
154
|
+
<Button
|
155
|
+
variant="contained"
|
156
|
+
onClick={handleSaveEdit}
|
157
|
+
>
|
158
|
+
Save Changes
|
159
|
+
</Button>
|
160
|
+
<Button
|
161
|
+
variant="outlined"
|
162
|
+
onClick={handleCancelEdit}
|
163
|
+
>
|
164
|
+
Cancel
|
165
|
+
</Button>
|
166
|
+
</Box>
|
167
|
+
</Box>
|
168
|
+
) : (
|
169
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
170
|
+
<Typography>
|
171
|
+
"{content.data.words.map(word => {
|
172
|
+
const correction = content.data.corrections.find(
|
173
|
+
c => c.original_word === word
|
174
|
+
);
|
175
|
+
return correction ? correction.corrected_word : word;
|
176
|
+
}).join(' ')}"
|
177
|
+
</Typography>
|
178
|
+
{!isReadOnly && (
|
179
|
+
<Button
|
180
|
+
variant="outlined"
|
181
|
+
size="small"
|
182
|
+
onClick={handleStartEdit}
|
183
|
+
>
|
184
|
+
Edit
|
185
|
+
</Button>
|
186
|
+
)}
|
187
|
+
</Box>
|
188
|
+
)
|
189
|
+
}
|
190
|
+
/>
|
191
|
+
<GridItem title="Position" value={content.data.position} />
|
192
|
+
<GridItem title="Length" value={`${content.data.length} words`} />
|
193
|
+
{content.data.corrections.length > 0 && (
|
194
|
+
<GridItem
|
195
|
+
title="Corrections"
|
196
|
+
value={
|
197
|
+
<Box sx={{ pl: 2 }}>
|
198
|
+
{content.data.corrections.map((correction: WordCorrection, i: number) => (
|
199
|
+
<Box key={i} sx={{ mb: 2 }}>
|
200
|
+
<Typography>
|
201
|
+
"{correction.original_word}" → "{correction.corrected_word}"
|
202
|
+
</Typography>
|
203
|
+
<Typography>
|
204
|
+
Confidence: {(correction.confidence * 100).toFixed(2)}%
|
205
|
+
</Typography>
|
206
|
+
<Typography>Source: {correction.source}</Typography>
|
207
|
+
<Typography>Reason: {correction.reason}</Typography>
|
208
|
+
{Object.keys(correction.alternatives).length > 0 && (
|
209
|
+
<Typography component="pre" sx={{ fontSize: '0.875rem' }}>
|
210
|
+
Alternatives: {JSON.stringify(correction.alternatives, null, 2)}
|
211
|
+
</Typography>
|
212
|
+
)}
|
213
|
+
</Box>
|
214
|
+
))}
|
215
|
+
</Box>
|
216
|
+
}
|
217
|
+
/>
|
218
|
+
)}
|
219
|
+
<GridItem
|
220
|
+
title="Reference Words"
|
221
|
+
value={
|
222
|
+
<Box component="pre" sx={{ margin: 0, fontSize: '0.875rem' }}>
|
223
|
+
{JSON.stringify(content.data.reference_words, null, 2)}
|
224
|
+
</Box>
|
225
|
+
}
|
226
|
+
/>
|
227
|
+
{content.data.preceding_anchor && (
|
228
|
+
<GridItem
|
229
|
+
title="Preceding Anchor"
|
230
|
+
value={`"${content.data.preceding_anchor.text}"`}
|
231
|
+
/>
|
232
|
+
)}
|
233
|
+
{content.data.following_anchor && (
|
234
|
+
<GridItem
|
235
|
+
title="Following Anchor"
|
236
|
+
value={`"${content.data.following_anchor.text}"`}
|
237
|
+
/>
|
238
|
+
)}
|
239
|
+
</Grid>
|
240
|
+
)
|
241
|
+
|
242
|
+
default:
|
243
|
+
return null
|
244
|
+
}
|
245
|
+
}
|
246
|
+
|
247
|
+
return (
|
248
|
+
<Dialog
|
249
|
+
open={open}
|
250
|
+
onClose={onClose}
|
251
|
+
maxWidth="sm"
|
252
|
+
fullWidth
|
253
|
+
PaperProps={{
|
254
|
+
sx: { position: 'relative' },
|
255
|
+
}}
|
256
|
+
>
|
257
|
+
<IconButton
|
258
|
+
onClick={onClose}
|
259
|
+
sx={{
|
260
|
+
position: 'absolute',
|
261
|
+
right: 8,
|
262
|
+
top: 8,
|
263
|
+
}}
|
264
|
+
>
|
265
|
+
<CloseIcon />
|
266
|
+
</IconButton>
|
267
|
+
<DialogTitle>
|
268
|
+
{content.type.charAt(0).toUpperCase() + content.type.slice(1)} Details
|
269
|
+
</DialogTitle>
|
270
|
+
<DialogContent dividers>{renderContent()}</DialogContent>
|
271
|
+
</Dialog>
|
272
|
+
)
|
273
|
+
}
|
274
|
+
|
275
|
+
interface GridItemProps {
|
276
|
+
title: string
|
277
|
+
value: string | number | React.ReactNode
|
278
|
+
}
|
279
|
+
|
280
|
+
function GridItem({ title, value }: GridItemProps) {
|
281
|
+
return (
|
282
|
+
<>
|
283
|
+
<Grid item xs={4}>
|
284
|
+
<Typography variant="subtitle1" fontWeight="bold">
|
285
|
+
{title}
|
286
|
+
</Typography>
|
287
|
+
</Grid>
|
288
|
+
<Grid item xs={8}>
|
289
|
+
{typeof value === 'string' || typeof value === 'number' ? (
|
290
|
+
<Typography>{value}</Typography>
|
291
|
+
) : (
|
292
|
+
value
|
293
|
+
)}
|
294
|
+
</Grid>
|
295
|
+
</>
|
296
|
+
)
|
297
|
+
}
|
@@ -0,0 +1,77 @@
|
|
1
|
+
import { ChangeEvent, DragEvent, useState } from 'react'
|
2
|
+
import { Paper, Typography } from '@mui/material'
|
3
|
+
import CloudUploadIcon from '@mui/icons-material/CloudUpload'
|
4
|
+
import { LyricsData } from '../types'
|
5
|
+
|
6
|
+
interface FileUploadProps {
|
7
|
+
onUpload: (data: LyricsData) => void
|
8
|
+
}
|
9
|
+
|
10
|
+
export default function FileUpload({ onUpload }: FileUploadProps) {
|
11
|
+
const [isDragging, setIsDragging] = useState(false)
|
12
|
+
|
13
|
+
const handleDragOver = (e: DragEvent) => {
|
14
|
+
e.preventDefault()
|
15
|
+
setIsDragging(true)
|
16
|
+
}
|
17
|
+
|
18
|
+
const handleDragLeave = () => {
|
19
|
+
setIsDragging(false)
|
20
|
+
}
|
21
|
+
|
22
|
+
const handleDrop = async (e: DragEvent) => {
|
23
|
+
e.preventDefault()
|
24
|
+
setIsDragging(false)
|
25
|
+
const file = e.dataTransfer.files[0]
|
26
|
+
await processFile(file)
|
27
|
+
}
|
28
|
+
|
29
|
+
const handleFileInput = async (e: ChangeEvent<HTMLInputElement>) => {
|
30
|
+
if (e.target.files?.length) {
|
31
|
+
await processFile(e.target.files[0])
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
const processFile = async (file: File) => {
|
36
|
+
try {
|
37
|
+
const text = await file.text()
|
38
|
+
const data = JSON.parse(text)
|
39
|
+
onUpload(data)
|
40
|
+
} catch (error) {
|
41
|
+
console.error('Error processing file:', error)
|
42
|
+
// TODO: Add error handling UI
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
return (
|
47
|
+
<Paper
|
48
|
+
sx={{
|
49
|
+
p: 4,
|
50
|
+
display: 'flex',
|
51
|
+
flexDirection: 'column',
|
52
|
+
alignItems: 'center',
|
53
|
+
backgroundColor: isDragging ? 'action.hover' : 'background.paper',
|
54
|
+
cursor: 'pointer',
|
55
|
+
}}
|
56
|
+
onDragOver={handleDragOver}
|
57
|
+
onDragLeave={handleDragLeave}
|
58
|
+
onDrop={handleDrop}
|
59
|
+
onClick={() => document.getElementById('file-input')?.click()}
|
60
|
+
>
|
61
|
+
<input
|
62
|
+
type="file"
|
63
|
+
id="file-input"
|
64
|
+
style={{ display: 'none' }}
|
65
|
+
accept="application/json"
|
66
|
+
onChange={handleFileInput}
|
67
|
+
/>
|
68
|
+
<CloudUploadIcon sx={{ fontSize: 48, mb: 2 }} />
|
69
|
+
<Typography variant="h6" gutterBottom>
|
70
|
+
Upload Lyrics Correction Review JSON
|
71
|
+
</Typography>
|
72
|
+
<Typography variant="body2" color="text.secondary">
|
73
|
+
Drag and drop a file here, or click to select
|
74
|
+
</Typography>
|
75
|
+
</Paper>
|
76
|
+
)
|
77
|
+
}
|