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
@@ -1,33 +1,9 @@
|
|
1
|
-
import {
|
2
|
-
import { Paper, Typography, Box
|
3
|
-
import {
|
4
|
-
import {
|
5
|
-
import {
|
6
|
-
import {
|
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, '')
|
1
|
+
import { useMemo } from 'react'
|
2
|
+
import { Paper, Typography, Box } from '@mui/material'
|
3
|
+
import { ReferenceViewProps } from './shared/types'
|
4
|
+
import { calculateReferenceLinePositions } from './shared/utils/referenceLineCalculator'
|
5
|
+
import { SourceSelector } from './shared/components/SourceSelector'
|
6
|
+
import { HighlightedText } from './shared/components/HighlightedText'
|
31
7
|
|
32
8
|
export default function ReferenceView({
|
33
9
|
referenceTexts,
|
@@ -37,215 +13,19 @@ export default function ReferenceView({
|
|
37
13
|
onWordClick,
|
38
14
|
flashingType,
|
39
15
|
corrected_segments,
|
40
|
-
highlightedWordIndex,
|
41
16
|
currentSource,
|
42
17
|
onSourceChange,
|
43
|
-
|
18
|
+
highlightInfo,
|
19
|
+
mode
|
44
20
|
}: ReferenceViewProps) {
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
}
|
21
|
+
const { linePositions } = useMemo(() =>
|
22
|
+
calculateReferenceLinePositions(
|
23
|
+
corrected_segments,
|
24
|
+
anchors,
|
25
|
+
currentSource
|
26
|
+
),
|
27
|
+
[corrected_segments, anchors, currentSource]
|
28
|
+
)
|
249
29
|
|
250
30
|
return (
|
251
31
|
<Paper sx={{ p: 2 }}>
|
@@ -253,35 +33,26 @@ export default function ReferenceView({
|
|
253
33
|
<Typography variant="h6">
|
254
34
|
Reference Text
|
255
35
|
</Typography>
|
256
|
-
<
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
36
|
+
<SourceSelector
|
37
|
+
currentSource={currentSource}
|
38
|
+
onSourceChange={onSourceChange}
|
39
|
+
/>
|
40
|
+
</Box>
|
41
|
+
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
42
|
+
<HighlightedText
|
43
|
+
text={referenceTexts[currentSource]}
|
44
|
+
anchors={anchors}
|
45
|
+
gaps={gaps}
|
46
|
+
onElementClick={onElementClick}
|
47
|
+
onWordClick={onWordClick}
|
48
|
+
flashingType={flashingType}
|
49
|
+
highlightInfo={highlightInfo}
|
50
|
+
mode={mode}
|
51
|
+
isReference={true}
|
52
|
+
currentSource={currentSource}
|
53
|
+
linePositions={linePositions}
|
54
|
+
/>
|
273
55
|
</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
56
|
</Paper>
|
286
57
|
)
|
287
58
|
}
|
@@ -0,0 +1,232 @@
|
|
1
|
+
import {
|
2
|
+
Dialog,
|
3
|
+
DialogTitle,
|
4
|
+
DialogContent,
|
5
|
+
DialogActions,
|
6
|
+
Button,
|
7
|
+
Box,
|
8
|
+
Typography,
|
9
|
+
Paper,
|
10
|
+
Collapse,
|
11
|
+
IconButton,
|
12
|
+
} from '@mui/material'
|
13
|
+
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
14
|
+
import { CorrectionData } from '../types'
|
15
|
+
import { useMemo, useState } from 'react'
|
16
|
+
|
17
|
+
interface ReviewChangesModalProps {
|
18
|
+
open: boolean
|
19
|
+
onClose: () => void
|
20
|
+
originalData: CorrectionData
|
21
|
+
updatedData: CorrectionData
|
22
|
+
onSubmit: () => void
|
23
|
+
}
|
24
|
+
|
25
|
+
interface DiffResult {
|
26
|
+
type: 'added' | 'removed' | 'modified'
|
27
|
+
path: string
|
28
|
+
segmentIndex?: number
|
29
|
+
oldValue?: any
|
30
|
+
newValue?: any
|
31
|
+
wordChanges?: DiffResult[]
|
32
|
+
}
|
33
|
+
|
34
|
+
export default function ReviewChangesModal({
|
35
|
+
open,
|
36
|
+
onClose,
|
37
|
+
originalData,
|
38
|
+
updatedData,
|
39
|
+
onSubmit
|
40
|
+
}: ReviewChangesModalProps) {
|
41
|
+
const [expandedSegments, setExpandedSegments] = useState<number[]>([])
|
42
|
+
|
43
|
+
const differences = useMemo(() => {
|
44
|
+
const diffs: DiffResult[] = []
|
45
|
+
|
46
|
+
// Compare corrected segments
|
47
|
+
originalData.corrected_segments.forEach((segment, index) => {
|
48
|
+
const updatedSegment = updatedData.corrected_segments[index]
|
49
|
+
if (!updatedSegment) {
|
50
|
+
diffs.push({
|
51
|
+
type: 'removed',
|
52
|
+
path: `Segment ${index}`,
|
53
|
+
segmentIndex: index,
|
54
|
+
oldValue: segment.text
|
55
|
+
})
|
56
|
+
return
|
57
|
+
}
|
58
|
+
|
59
|
+
const wordChanges: DiffResult[] = []
|
60
|
+
|
61
|
+
// Compare word-level changes
|
62
|
+
segment.words.forEach((word, wordIndex) => {
|
63
|
+
const updatedWord = updatedSegment.words[wordIndex]
|
64
|
+
if (!updatedWord) {
|
65
|
+
wordChanges.push({
|
66
|
+
type: 'removed',
|
67
|
+
path: `Word ${wordIndex}`,
|
68
|
+
oldValue: `"${word.text}" (${word.start_time.toFixed(4)} - ${word.end_time.toFixed(4)})`
|
69
|
+
})
|
70
|
+
return
|
71
|
+
}
|
72
|
+
|
73
|
+
if (word.text !== updatedWord.text ||
|
74
|
+
Math.abs(word.start_time - updatedWord.start_time) > 0.0001 ||
|
75
|
+
Math.abs(word.end_time - updatedWord.end_time) > 0.0001) {
|
76
|
+
wordChanges.push({
|
77
|
+
type: 'modified',
|
78
|
+
path: `Word ${wordIndex}`,
|
79
|
+
oldValue: `"${word.text}" (${word.start_time.toFixed(4)} - ${word.end_time.toFixed(4)})`,
|
80
|
+
newValue: `"${updatedWord.text}" (${updatedWord.start_time.toFixed(4)} - ${updatedWord.end_time.toFixed(4)})`
|
81
|
+
})
|
82
|
+
}
|
83
|
+
})
|
84
|
+
|
85
|
+
// Check for added words
|
86
|
+
if (updatedSegment.words.length > segment.words.length) {
|
87
|
+
for (let i = segment.words.length; i < updatedSegment.words.length; i++) {
|
88
|
+
const word = updatedSegment.words[i]
|
89
|
+
wordChanges.push({
|
90
|
+
type: 'added',
|
91
|
+
path: `Word ${i}`,
|
92
|
+
newValue: `"${word.text}" (${word.start_time.toFixed(4)} - ${word.end_time.toFixed(4)})`
|
93
|
+
})
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
if (segment.text !== updatedSegment.text ||
|
98
|
+
segment.start_time !== updatedSegment.start_time ||
|
99
|
+
segment.end_time !== updatedSegment.end_time ||
|
100
|
+
wordChanges.length > 0) {
|
101
|
+
diffs.push({
|
102
|
+
type: 'modified',
|
103
|
+
path: `Segment ${index}`,
|
104
|
+
segmentIndex: index,
|
105
|
+
oldValue: `"${segment.text}" (${segment.start_time.toFixed(4)} - ${segment.end_time.toFixed(4)})`,
|
106
|
+
newValue: `"${updatedSegment.text}" (${updatedSegment.start_time.toFixed(4)} - ${updatedSegment.end_time.toFixed(4)})`,
|
107
|
+
wordChanges: wordChanges.length > 0 ? wordChanges : undefined
|
108
|
+
})
|
109
|
+
}
|
110
|
+
})
|
111
|
+
|
112
|
+
return diffs
|
113
|
+
}, [originalData, updatedData])
|
114
|
+
|
115
|
+
const handleToggleSegment = (segmentIndex: number) => {
|
116
|
+
setExpandedSegments(prev =>
|
117
|
+
prev.includes(segmentIndex)
|
118
|
+
? prev.filter(i => i !== segmentIndex)
|
119
|
+
: [...prev, segmentIndex]
|
120
|
+
)
|
121
|
+
}
|
122
|
+
|
123
|
+
const renderDiff = (diff: DiffResult) => {
|
124
|
+
const getColor = () => {
|
125
|
+
switch (diff.type) {
|
126
|
+
case 'added': return 'success.main'
|
127
|
+
case 'removed': return 'error.main'
|
128
|
+
case 'modified': return 'warning.main'
|
129
|
+
default: return 'text.primary'
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
const isExpanded = diff.segmentIndex !== undefined &&
|
134
|
+
expandedSegments.includes(diff.segmentIndex)
|
135
|
+
|
136
|
+
return (
|
137
|
+
<Paper key={diff.path} sx={{ p: 2, mb: 1 }}>
|
138
|
+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
139
|
+
<Box>
|
140
|
+
<Typography color={getColor()} sx={{ fontWeight: 'bold' }}>
|
141
|
+
{diff.type.toUpperCase()}: {diff.path}
|
142
|
+
</Typography>
|
143
|
+
{diff.oldValue && (
|
144
|
+
<Typography color="error.main" sx={{ ml: 2 }}>
|
145
|
+
- {diff.oldValue}
|
146
|
+
</Typography>
|
147
|
+
)}
|
148
|
+
{diff.newValue && (
|
149
|
+
<Typography color="success.main" sx={{ ml: 2 }}>
|
150
|
+
+ {diff.newValue}
|
151
|
+
</Typography>
|
152
|
+
)}
|
153
|
+
</Box>
|
154
|
+
{diff.wordChanges && (
|
155
|
+
<IconButton
|
156
|
+
onClick={() => handleToggleSegment(diff.segmentIndex!)}
|
157
|
+
sx={{
|
158
|
+
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
|
159
|
+
transition: 'transform 0.2s'
|
160
|
+
}}
|
161
|
+
>
|
162
|
+
<ExpandMoreIcon />
|
163
|
+
</IconButton>
|
164
|
+
)}
|
165
|
+
</Box>
|
166
|
+
|
167
|
+
{diff.wordChanges && (
|
168
|
+
<Collapse in={isExpanded}>
|
169
|
+
<Box sx={{ mt: 2, ml: 4 }}>
|
170
|
+
{diff.wordChanges.map((wordDiff, index) => (
|
171
|
+
<Box key={index}>
|
172
|
+
<Typography color={getColor()} variant="body2">
|
173
|
+
{wordDiff.type.toUpperCase()}: {wordDiff.path}
|
174
|
+
</Typography>
|
175
|
+
{wordDiff.oldValue && (
|
176
|
+
<Typography color="error.main" variant="body2" sx={{ ml: 2 }}>
|
177
|
+
- {wordDiff.oldValue}
|
178
|
+
</Typography>
|
179
|
+
)}
|
180
|
+
{wordDiff.newValue && (
|
181
|
+
<Typography color="success.main" variant="body2" sx={{ ml: 2 }}>
|
182
|
+
+ {wordDiff.newValue}
|
183
|
+
</Typography>
|
184
|
+
)}
|
185
|
+
</Box>
|
186
|
+
))}
|
187
|
+
</Box>
|
188
|
+
</Collapse>
|
189
|
+
)}
|
190
|
+
</Paper>
|
191
|
+
)
|
192
|
+
}
|
193
|
+
|
194
|
+
return (
|
195
|
+
<Dialog
|
196
|
+
open={open}
|
197
|
+
onClose={onClose}
|
198
|
+
maxWidth="md"
|
199
|
+
fullWidth
|
200
|
+
>
|
201
|
+
<DialogTitle>Review Changes</DialogTitle>
|
202
|
+
<DialogContent dividers>
|
203
|
+
{differences.length === 0 ? (
|
204
|
+
<Box>
|
205
|
+
<Typography color="text.secondary" sx={{ mb: 2 }}>
|
206
|
+
No changes detected. You can still submit to continue processing.
|
207
|
+
</Typography>
|
208
|
+
<Typography variant="body2" color="text.secondary">
|
209
|
+
Total segments: {updatedData.corrected_segments.length}
|
210
|
+
</Typography>
|
211
|
+
</Box>
|
212
|
+
) : (
|
213
|
+
<Box>
|
214
|
+
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
215
|
+
{differences.length} change{differences.length !== 1 ? 's' : ''} detected:
|
216
|
+
</Typography>
|
217
|
+
{differences.map(renderDiff)}
|
218
|
+
</Box>
|
219
|
+
)}
|
220
|
+
</DialogContent>
|
221
|
+
<DialogActions>
|
222
|
+
<Button onClick={onClose}>Cancel</Button>
|
223
|
+
<Button
|
224
|
+
onClick={onSubmit}
|
225
|
+
variant="contained"
|
226
|
+
>
|
227
|
+
Submit to Server
|
228
|
+
</Button>
|
229
|
+
</DialogActions>
|
230
|
+
</Dialog>
|
231
|
+
)
|
232
|
+
}
|
@@ -0,0 +1,64 @@
|
|
1
|
+
import {
|
2
|
+
Dialog,
|
3
|
+
DialogTitle,
|
4
|
+
DialogContent,
|
5
|
+
IconButton, Box
|
6
|
+
} from '@mui/material'
|
7
|
+
import CloseIcon from '@mui/icons-material/Close'
|
8
|
+
import { LyricsSegment } from '../types'
|
9
|
+
|
10
|
+
interface SegmentDetailsModalProps {
|
11
|
+
open: boolean
|
12
|
+
onClose: () => void
|
13
|
+
segment: LyricsSegment | null
|
14
|
+
segmentIndex: number | null
|
15
|
+
}
|
16
|
+
|
17
|
+
export default function SegmentDetailsModal({
|
18
|
+
open,
|
19
|
+
onClose,
|
20
|
+
segment,
|
21
|
+
segmentIndex
|
22
|
+
}: SegmentDetailsModalProps) {
|
23
|
+
if (!segment || segmentIndex === null) return null
|
24
|
+
|
25
|
+
return (
|
26
|
+
<Dialog
|
27
|
+
open={open}
|
28
|
+
onClose={onClose}
|
29
|
+
maxWidth="sm"
|
30
|
+
fullWidth
|
31
|
+
PaperProps={{
|
32
|
+
sx: { position: 'relative' },
|
33
|
+
}}
|
34
|
+
>
|
35
|
+
<IconButton
|
36
|
+
onClick={onClose}
|
37
|
+
sx={{
|
38
|
+
position: 'absolute',
|
39
|
+
right: 8,
|
40
|
+
top: 8,
|
41
|
+
}}
|
42
|
+
>
|
43
|
+
<CloseIcon />
|
44
|
+
</IconButton>
|
45
|
+
<DialogTitle>
|
46
|
+
Segment {segmentIndex} Details
|
47
|
+
</DialogTitle>
|
48
|
+
<DialogContent dividers>
|
49
|
+
<Box
|
50
|
+
component="pre"
|
51
|
+
sx={{
|
52
|
+
margin: 0,
|
53
|
+
fontFamily: 'monospace',
|
54
|
+
fontSize: '0.875rem',
|
55
|
+
whiteSpace: 'pre-wrap',
|
56
|
+
wordBreak: 'break-word'
|
57
|
+
}}
|
58
|
+
>
|
59
|
+
{JSON.stringify(segment, null, 2)}
|
60
|
+
</Box>
|
61
|
+
</DialogContent>
|
62
|
+
</Dialog>
|
63
|
+
)
|
64
|
+
}
|