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.
Files changed (39) hide show
  1. lyrics_transcriber/core/controller.py +10 -1
  2. lyrics_transcriber/correction/corrector.py +4 -3
  3. lyrics_transcriber/frontend/dist/assets/index-CQCER5Fo.js +181 -0
  4. lyrics_transcriber/frontend/dist/index.html +1 -1
  5. lyrics_transcriber/frontend/src/App.tsx +6 -2
  6. lyrics_transcriber/frontend/src/api.ts +9 -0
  7. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +155 -0
  8. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +1 -1
  9. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +23 -191
  10. lyrics_transcriber/frontend/src/components/EditModal.tsx +407 -0
  11. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +255 -221
  12. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +39 -0
  13. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +35 -264
  14. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +232 -0
  15. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
  16. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +315 -0
  17. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +116 -138
  18. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +116 -0
  19. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +243 -0
  20. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +28 -0
  21. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +52 -0
  22. lyrics_transcriber/frontend/src/components/{constants.ts → shared/constants.ts} +1 -0
  23. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +137 -0
  24. lyrics_transcriber/frontend/src/components/{styles.ts → shared/styles.ts} +1 -1
  25. lyrics_transcriber/frontend/src/components/shared/types.ts +99 -0
  26. lyrics_transcriber/frontend/src/components/shared/utils/newlineCalculator.ts +37 -0
  27. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +76 -0
  28. lyrics_transcriber/frontend/src/types.ts +2 -43
  29. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  30. lyrics_transcriber/lyrics/spotify.py +11 -0
  31. lyrics_transcriber/output/generator.py +28 -11
  32. lyrics_transcriber/review/server.py +38 -12
  33. {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/METADATA +1 -1
  34. {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/RECORD +37 -24
  35. lyrics_transcriber/frontend/dist/assets/index-DqFgiUni.js +0 -245
  36. lyrics_transcriber/frontend/src/components/DebugPanel.tsx +0 -311
  37. {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/LICENSE +0 -0
  38. {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/WHEEL +0 -0
  39. {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 { 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, '')
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
- onDebugInfoUpdate
18
+ highlightInfo,
19
+ mode
44
20
  }: 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
- }
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
- <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>
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
+ }