lyrics-transcriber 0.34.2__py3-none-any.whl → 0.35.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lyrics_transcriber/core/controller.py +10 -1
- lyrics_transcriber/correction/corrector.py +4 -3
- lyrics_transcriber/frontend/dist/assets/index-CQCER5Fo.js +181 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/App.tsx +6 -2
- lyrics_transcriber/frontend/src/api.ts +9 -0
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +155 -0
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +1 -1
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +23 -191
- lyrics_transcriber/frontend/src/components/EditModal.tsx +407 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +255 -221
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +39 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +35 -264
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +232 -0
- lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +315 -0
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +116 -138
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +116 -0
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +243 -0
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +28 -0
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +52 -0
- lyrics_transcriber/frontend/src/components/{constants.ts → shared/constants.ts} +1 -0
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +137 -0
- lyrics_transcriber/frontend/src/components/{styles.ts → shared/styles.ts} +1 -1
- lyrics_transcriber/frontend/src/components/shared/types.ts +99 -0
- lyrics_transcriber/frontend/src/components/shared/utils/newlineCalculator.ts +37 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +76 -0
- lyrics_transcriber/frontend/src/types.ts +2 -43
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/lyrics/spotify.py +11 -0
- lyrics_transcriber/output/generator.py +28 -11
- lyrics_transcriber/review/server.py +38 -12
- {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/METADATA +1 -1
- {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/RECORD +37 -24
- lyrics_transcriber/frontend/dist/assets/index-DqFgiUni.js +0 -245
- lyrics_transcriber/frontend/src/components/DebugPanel.tsx +0 -311
- {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/WHEEL +0 -0
- {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,116 @@
|
|
1
|
+
import { Button, Box, TextField } from '@mui/material'
|
2
|
+
import DeleteIcon from '@mui/icons-material/Delete'
|
3
|
+
import { useState, useEffect } from 'react'
|
4
|
+
import { ModalContent } from './LyricsAnalyzer'
|
5
|
+
|
6
|
+
interface WordEditControlsProps {
|
7
|
+
content: ModalContent
|
8
|
+
onUpdateCorrection?: (position: number, updatedWords: string[]) => void
|
9
|
+
onClose: () => void
|
10
|
+
}
|
11
|
+
|
12
|
+
export function useWordEdit(content: ModalContent | null) {
|
13
|
+
const [editedWord, setEditedWord] = useState('')
|
14
|
+
const [isEditing, setIsEditing] = useState(false)
|
15
|
+
|
16
|
+
useEffect(() => {
|
17
|
+
if (content) {
|
18
|
+
setEditedWord(content.type === 'gap' ? content.data.word : content.type === 'anchor' ? content.data.words[0] : '')
|
19
|
+
setIsEditing(false)
|
20
|
+
}
|
21
|
+
}, [content])
|
22
|
+
|
23
|
+
return {
|
24
|
+
editedWord,
|
25
|
+
setEditedWord,
|
26
|
+
isEditing,
|
27
|
+
setIsEditing
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
export default function WordEditControls({ content, onUpdateCorrection, onClose }: WordEditControlsProps) {
|
32
|
+
const {
|
33
|
+
editedWord,
|
34
|
+
setEditedWord,
|
35
|
+
isEditing,
|
36
|
+
setIsEditing
|
37
|
+
} = useWordEdit(content)
|
38
|
+
|
39
|
+
const handleStartEdit = () => {
|
40
|
+
if (content.type === 'gap') {
|
41
|
+
setEditedWord(content.data.word)
|
42
|
+
} else if (content.type === 'anchor') {
|
43
|
+
setEditedWord(content.data.words[0])
|
44
|
+
}
|
45
|
+
setIsEditing(true)
|
46
|
+
}
|
47
|
+
|
48
|
+
const handleDelete = () => {
|
49
|
+
if (!onUpdateCorrection) return
|
50
|
+
onUpdateCorrection(content.data.position, [])
|
51
|
+
onClose()
|
52
|
+
}
|
53
|
+
|
54
|
+
const handleSaveEdit = () => {
|
55
|
+
if (onUpdateCorrection) {
|
56
|
+
onUpdateCorrection(content.data.position, [editedWord])
|
57
|
+
}
|
58
|
+
onClose()
|
59
|
+
}
|
60
|
+
|
61
|
+
const handleCancelEdit = () => {
|
62
|
+
if (content.type === 'gap') {
|
63
|
+
setEditedWord(content.data.word)
|
64
|
+
setIsEditing(false)
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
const handleWordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
69
|
+
setEditedWord(event.target.value)
|
70
|
+
}
|
71
|
+
|
72
|
+
return isEditing ? (
|
73
|
+
<Box>
|
74
|
+
<TextField
|
75
|
+
value={editedWord}
|
76
|
+
onChange={handleWordChange}
|
77
|
+
fullWidth
|
78
|
+
label="Edit word"
|
79
|
+
variant="outlined"
|
80
|
+
size="small"
|
81
|
+
sx={{ mb: 1 }}
|
82
|
+
/>
|
83
|
+
<Box sx={{ display: 'flex', gap: 1 }}>
|
84
|
+
<Button variant="contained" onClick={handleSaveEdit}>
|
85
|
+
Save Changes
|
86
|
+
</Button>
|
87
|
+
<Button variant="outlined" onClick={handleCancelEdit}>
|
88
|
+
Cancel
|
89
|
+
</Button>
|
90
|
+
<Button
|
91
|
+
variant="outlined"
|
92
|
+
color="error"
|
93
|
+
startIcon={<DeleteIcon />}
|
94
|
+
onClick={handleDelete}
|
95
|
+
>
|
96
|
+
Delete
|
97
|
+
</Button>
|
98
|
+
</Box>
|
99
|
+
</Box>
|
100
|
+
) : (
|
101
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
102
|
+
<Button variant="outlined" size="small" onClick={handleStartEdit}>
|
103
|
+
Edit
|
104
|
+
</Button>
|
105
|
+
<Button
|
106
|
+
variant="outlined"
|
107
|
+
size="small"
|
108
|
+
color="error"
|
109
|
+
startIcon={<DeleteIcon />}
|
110
|
+
onClick={handleDelete}
|
111
|
+
>
|
112
|
+
Delete
|
113
|
+
</Button>
|
114
|
+
</Box>
|
115
|
+
)
|
116
|
+
}
|
@@ -0,0 +1,243 @@
|
|
1
|
+
import { Typography, Box } from '@mui/material'
|
2
|
+
import { Word } from './Word'
|
3
|
+
import { useWordClick } from '../hooks/useWordClick'
|
4
|
+
import { AnchorSequence, GapSequence, HighlightInfo, InteractionMode } from '../../../types'
|
5
|
+
import { ModalContent } from '../../LyricsAnalyzer'
|
6
|
+
import { WordClickInfo, TranscriptionWordPosition, FlashType, LinePosition } from '../types'
|
7
|
+
import React from 'react'
|
8
|
+
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
9
|
+
import IconButton from '@mui/material/IconButton';
|
10
|
+
|
11
|
+
interface HighlightedTextProps {
|
12
|
+
// Input can be either raw text or pre-processed word positions
|
13
|
+
text?: string
|
14
|
+
wordPositions?: TranscriptionWordPosition[]
|
15
|
+
// Common props
|
16
|
+
anchors: AnchorSequence[]
|
17
|
+
gaps: GapSequence[]
|
18
|
+
highlightInfo: HighlightInfo | null
|
19
|
+
mode: InteractionMode
|
20
|
+
onElementClick: (content: ModalContent) => void
|
21
|
+
onWordClick?: (info: WordClickInfo) => void
|
22
|
+
flashingType: FlashType
|
23
|
+
// Reference-specific props
|
24
|
+
isReference?: boolean
|
25
|
+
currentSource?: 'genius' | 'spotify'
|
26
|
+
preserveSegments?: boolean
|
27
|
+
linePositions?: LinePosition[]
|
28
|
+
currentTime?: number
|
29
|
+
}
|
30
|
+
|
31
|
+
export function HighlightedText({
|
32
|
+
text,
|
33
|
+
wordPositions,
|
34
|
+
anchors,
|
35
|
+
highlightInfo,
|
36
|
+
mode,
|
37
|
+
onElementClick,
|
38
|
+
onWordClick,
|
39
|
+
flashingType,
|
40
|
+
isReference,
|
41
|
+
currentSource,
|
42
|
+
preserveSegments = false,
|
43
|
+
linePositions = [],
|
44
|
+
currentTime = 0
|
45
|
+
}: HighlightedTextProps) {
|
46
|
+
const { handleWordClick } = useWordClick({
|
47
|
+
mode,
|
48
|
+
onElementClick,
|
49
|
+
onWordClick,
|
50
|
+
isReference,
|
51
|
+
currentSource
|
52
|
+
})
|
53
|
+
|
54
|
+
const shouldWordFlash = (wordPos: TranscriptionWordPosition | { word: string; index: number }): boolean => {
|
55
|
+
if (!flashingType) return false
|
56
|
+
|
57
|
+
if ('type' in wordPos) {
|
58
|
+
// Handle TranscriptionWordPosition
|
59
|
+
const hasCorrections = wordPos.type === 'gap' &&
|
60
|
+
Boolean((wordPos.sequence as GapSequence)?.corrections?.length)
|
61
|
+
|
62
|
+
return Boolean(
|
63
|
+
(flashingType === 'anchor' && wordPos.type === 'anchor') ||
|
64
|
+
(flashingType === 'corrected' && hasCorrections) ||
|
65
|
+
(flashingType === 'uncorrected' && wordPos.type === 'gap' && !hasCorrections) ||
|
66
|
+
(flashingType === 'word' && highlightInfo?.type === 'anchor' &&
|
67
|
+
wordPos.type === 'anchor' && wordPos.sequence && (
|
68
|
+
(wordPos.sequence as AnchorSequence).transcription_position === highlightInfo.transcriptionIndex ||
|
69
|
+
(isReference && currentSource &&
|
70
|
+
(wordPos.sequence as AnchorSequence).reference_positions[currentSource] === highlightInfo.referenceIndices?.[currentSource])
|
71
|
+
))
|
72
|
+
)
|
73
|
+
} else {
|
74
|
+
// Handle reference word
|
75
|
+
const thisWordIndex = wordPos.index
|
76
|
+
const anchor = anchors.find(a => {
|
77
|
+
const position = isReference
|
78
|
+
? a.reference_positions[currentSource!]
|
79
|
+
: a.transcription_position
|
80
|
+
if (position === undefined) return false
|
81
|
+
return thisWordIndex >= position && thisWordIndex < position + a.length
|
82
|
+
})
|
83
|
+
|
84
|
+
return Boolean(
|
85
|
+
(flashingType === 'anchor' && anchor) ||
|
86
|
+
(flashingType === 'word' && highlightInfo?.type === 'anchor' && anchor && (
|
87
|
+
anchor.transcription_position === highlightInfo.transcriptionIndex ||
|
88
|
+
(isReference && currentSource && anchor.reference_positions[currentSource] === highlightInfo.referenceIndices?.[currentSource])
|
89
|
+
))
|
90
|
+
)
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
const shouldHighlightWord = (wordPos: TranscriptionWordPosition): boolean => {
|
95
|
+
if (!currentTime || !wordPos.word.start_time || !wordPos.word.end_time) return false
|
96
|
+
return currentTime >= wordPos.word.start_time && currentTime <= wordPos.word.end_time
|
97
|
+
}
|
98
|
+
|
99
|
+
const handleCopyLine = (text: string) => {
|
100
|
+
navigator.clipboard.writeText(text);
|
101
|
+
};
|
102
|
+
|
103
|
+
const renderContent = () => {
|
104
|
+
if (wordPositions) {
|
105
|
+
return wordPositions.map((wordPos, index) => (
|
106
|
+
<React.Fragment key={`${wordPos.word.text}-${index}`}>
|
107
|
+
<Word
|
108
|
+
word={wordPos.word.text}
|
109
|
+
shouldFlash={shouldWordFlash(wordPos)}
|
110
|
+
isCurrentlyPlaying={shouldHighlightWord(wordPos)}
|
111
|
+
isAnchor={wordPos.type === 'anchor'}
|
112
|
+
isCorrectedGap={wordPos.type === 'gap' && Boolean((wordPos.sequence as GapSequence)?.corrections?.length)}
|
113
|
+
isUncorrectedGap={wordPos.type === 'gap' && !(wordPos.sequence as GapSequence)?.corrections?.length}
|
114
|
+
onClick={() => handleWordClick(
|
115
|
+
wordPos.word.text,
|
116
|
+
wordPos.position,
|
117
|
+
wordPos.type === 'anchor' ? wordPos.sequence as AnchorSequence : undefined,
|
118
|
+
wordPos.type === 'gap' ? wordPos.sequence as GapSequence : undefined
|
119
|
+
)}
|
120
|
+
/>
|
121
|
+
{index < wordPositions.length - 1 && ' '}
|
122
|
+
</React.Fragment>
|
123
|
+
))
|
124
|
+
} else if (text) {
|
125
|
+
const lines = text.split('\n')
|
126
|
+
let globalWordIndex = 0
|
127
|
+
|
128
|
+
return lines.map((line, lineIndex) => {
|
129
|
+
const currentLinePosition = linePositions?.find((pos: LinePosition) => pos.position === globalWordIndex)
|
130
|
+
if (currentLinePosition?.isEmpty) {
|
131
|
+
globalWordIndex++
|
132
|
+
return (
|
133
|
+
<Box key={`empty-${lineIndex}`} sx={{ display: 'flex', alignItems: 'flex-start' }}>
|
134
|
+
<Typography
|
135
|
+
component="span"
|
136
|
+
sx={{
|
137
|
+
color: 'text.secondary',
|
138
|
+
width: '2em',
|
139
|
+
minWidth: '2em',
|
140
|
+
textAlign: 'right',
|
141
|
+
marginRight: 1,
|
142
|
+
userSelect: 'none',
|
143
|
+
fontFamily: 'monospace',
|
144
|
+
paddingTop: '4px',
|
145
|
+
}}
|
146
|
+
>
|
147
|
+
{currentLinePosition.lineNumber}
|
148
|
+
</Typography>
|
149
|
+
<Box sx={{ width: '28px' }} /> {/* Space for copy button */}
|
150
|
+
<Box sx={{ flex: 1, height: '1.5em' }} />
|
151
|
+
</Box>
|
152
|
+
)
|
153
|
+
}
|
154
|
+
|
155
|
+
const lineContent = line.split(/(\s+)/)
|
156
|
+
return (
|
157
|
+
<Box key={`line-${lineIndex}`} sx={{ display: 'flex', alignItems: 'flex-start' }}>
|
158
|
+
<Typography
|
159
|
+
component="span"
|
160
|
+
sx={{
|
161
|
+
color: 'text.secondary',
|
162
|
+
width: '2em',
|
163
|
+
minWidth: '2em',
|
164
|
+
textAlign: 'right',
|
165
|
+
marginRight: 1,
|
166
|
+
userSelect: 'none',
|
167
|
+
fontFamily: 'monospace',
|
168
|
+
paddingTop: '4px',
|
169
|
+
}}
|
170
|
+
>
|
171
|
+
{lineIndex}
|
172
|
+
</Typography>
|
173
|
+
<IconButton
|
174
|
+
size="small"
|
175
|
+
onClick={() => handleCopyLine(line)}
|
176
|
+
sx={{
|
177
|
+
padding: '2px',
|
178
|
+
marginRight: 1,
|
179
|
+
height: '24px',
|
180
|
+
width: '24px'
|
181
|
+
}}
|
182
|
+
>
|
183
|
+
<ContentCopyIcon sx={{ fontSize: '1rem' }} />
|
184
|
+
</IconButton>
|
185
|
+
<Box sx={{ flex: 1 }}>
|
186
|
+
{lineContent.map((word, wordIndex) => {
|
187
|
+
if (word === '') return null
|
188
|
+
if (/^\s+$/.test(word)) {
|
189
|
+
return <span key={`space-${lineIndex}-${wordIndex}`}> </span>
|
190
|
+
}
|
191
|
+
|
192
|
+
const position = globalWordIndex++
|
193
|
+
const anchor = anchors.find(a => {
|
194
|
+
const refPos = a.reference_positions[currentSource!]
|
195
|
+
if (refPos === undefined) return false
|
196
|
+
return position >= refPos && position < refPos + a.length
|
197
|
+
})
|
198
|
+
|
199
|
+
// Create a mock TranscriptionWordPosition for highlighting
|
200
|
+
const wordPos: TranscriptionWordPosition = {
|
201
|
+
word: { text: word },
|
202
|
+
position,
|
203
|
+
type: anchor ? 'anchor' : 'other',
|
204
|
+
sequence: anchor,
|
205
|
+
isInRange: true
|
206
|
+
}
|
207
|
+
|
208
|
+
return (
|
209
|
+
<Word
|
210
|
+
key={`${word}-${lineIndex}-${wordIndex}`}
|
211
|
+
word={word}
|
212
|
+
shouldFlash={shouldWordFlash({ word, index: position })}
|
213
|
+
isCurrentlyPlaying={shouldHighlightWord(wordPos)}
|
214
|
+
isAnchor={Boolean(anchor)}
|
215
|
+
isCorrectedGap={false}
|
216
|
+
isUncorrectedGap={false}
|
217
|
+
onClick={() => handleWordClick(word, position, anchor, undefined)}
|
218
|
+
/>
|
219
|
+
)
|
220
|
+
})}
|
221
|
+
</Box>
|
222
|
+
</Box>
|
223
|
+
)
|
224
|
+
})
|
225
|
+
}
|
226
|
+
|
227
|
+
return null
|
228
|
+
}
|
229
|
+
|
230
|
+
return (
|
231
|
+
<Typography
|
232
|
+
component="div"
|
233
|
+
sx={{
|
234
|
+
fontFamily: 'monospace',
|
235
|
+
whiteSpace: preserveSegments ? 'normal' : 'pre-wrap',
|
236
|
+
margin: 0,
|
237
|
+
lineHeight: 1.5
|
238
|
+
}}
|
239
|
+
>
|
240
|
+
{renderContent()}
|
241
|
+
</Typography>
|
242
|
+
)
|
243
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import { Box, Button } from '@mui/material'
|
2
|
+
|
3
|
+
interface SourceSelectorProps {
|
4
|
+
currentSource: 'genius' | 'spotify'
|
5
|
+
onSourceChange: (source: 'genius' | 'spotify') => void
|
6
|
+
}
|
7
|
+
|
8
|
+
export function SourceSelector({ currentSource, onSourceChange }: SourceSelectorProps) {
|
9
|
+
return (
|
10
|
+
<Box>
|
11
|
+
<Button
|
12
|
+
size="small"
|
13
|
+
variant={currentSource === 'genius' ? 'contained' : 'outlined'}
|
14
|
+
onClick={() => onSourceChange('genius')}
|
15
|
+
sx={{ mr: 1 }}
|
16
|
+
>
|
17
|
+
Genius
|
18
|
+
</Button>
|
19
|
+
<Button
|
20
|
+
size="small"
|
21
|
+
variant={currentSource === 'spotify' ? 'contained' : 'outlined'}
|
22
|
+
onClick={() => onSourceChange('spotify')}
|
23
|
+
>
|
24
|
+
Spotify
|
25
|
+
</Button>
|
26
|
+
</Box>
|
27
|
+
)
|
28
|
+
}
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import { COLORS } from '../constants'
|
3
|
+
import { HighlightedWord } from '../styles'
|
4
|
+
import { WordProps } from '../types'
|
5
|
+
|
6
|
+
export const Word = React.memo(function Word({
|
7
|
+
word,
|
8
|
+
shouldFlash,
|
9
|
+
isAnchor,
|
10
|
+
isCorrectedGap,
|
11
|
+
isUncorrectedGap,
|
12
|
+
isCurrentlyPlaying,
|
13
|
+
padding = '2px 4px',
|
14
|
+
onClick,
|
15
|
+
}: WordProps) {
|
16
|
+
if (/^\s+$/.test(word)) {
|
17
|
+
return word
|
18
|
+
}
|
19
|
+
|
20
|
+
const backgroundColor = isCurrentlyPlaying
|
21
|
+
? COLORS.playing
|
22
|
+
: shouldFlash
|
23
|
+
? COLORS.highlighted
|
24
|
+
: isAnchor
|
25
|
+
? COLORS.anchor
|
26
|
+
: isCorrectedGap
|
27
|
+
? COLORS.corrected
|
28
|
+
: isUncorrectedGap
|
29
|
+
? COLORS.uncorrectedGap
|
30
|
+
: 'transparent'
|
31
|
+
|
32
|
+
return (
|
33
|
+
<HighlightedWord
|
34
|
+
shouldFlash={shouldFlash}
|
35
|
+
style={{
|
36
|
+
backgroundColor,
|
37
|
+
padding,
|
38
|
+
cursor: 'pointer',
|
39
|
+
borderRadius: '3px',
|
40
|
+
color: isCurrentlyPlaying ? '#ffffff' : 'inherit',
|
41
|
+
}}
|
42
|
+
sx={{
|
43
|
+
'&:hover': {
|
44
|
+
backgroundColor: '#e0e0e0'
|
45
|
+
}
|
46
|
+
}}
|
47
|
+
onClick={onClick}
|
48
|
+
>
|
49
|
+
{word}
|
50
|
+
</HighlightedWord>
|
51
|
+
)
|
52
|
+
})
|
@@ -0,0 +1,137 @@
|
|
1
|
+
import { useCallback } from 'react'
|
2
|
+
import { AnchorSequence, GapSequence, InteractionMode } from '../../../types'
|
3
|
+
import { ModalContent } from '../../LyricsAnalyzer'
|
4
|
+
import { WordClickInfo } from '../types'
|
5
|
+
|
6
|
+
interface UseWordClickProps {
|
7
|
+
mode: InteractionMode
|
8
|
+
onElementClick: (content: ModalContent) => void
|
9
|
+
onWordClick?: (info: WordClickInfo) => void
|
10
|
+
isReference?: boolean
|
11
|
+
currentSource?: 'genius' | 'spotify'
|
12
|
+
}
|
13
|
+
|
14
|
+
export function useWordClick({
|
15
|
+
mode,
|
16
|
+
onElementClick,
|
17
|
+
onWordClick,
|
18
|
+
isReference,
|
19
|
+
currentSource
|
20
|
+
}: UseWordClickProps) {
|
21
|
+
const handleWordClick = useCallback((
|
22
|
+
word: string,
|
23
|
+
position: number,
|
24
|
+
anchor?: AnchorSequence,
|
25
|
+
gap?: GapSequence,
|
26
|
+
debugInfo?: any
|
27
|
+
) => {
|
28
|
+
console.log(JSON.stringify({
|
29
|
+
debug: {
|
30
|
+
clickedWord: word,
|
31
|
+
position,
|
32
|
+
isReference,
|
33
|
+
currentSource,
|
34
|
+
wordInfo: debugInfo?.wordSplitInfo,
|
35
|
+
nearbyAnchors: debugInfo?.nearbyAnchors,
|
36
|
+
anchorInfo: anchor && {
|
37
|
+
transcriptionPos: anchor.transcription_position,
|
38
|
+
length: anchor.length,
|
39
|
+
words: anchor.words,
|
40
|
+
refPositions: anchor.reference_positions
|
41
|
+
},
|
42
|
+
gapInfo: gap && {
|
43
|
+
transcriptionPos: gap.transcription_position,
|
44
|
+
length: gap.length,
|
45
|
+
words: gap.words,
|
46
|
+
corrections: gap.corrections.map(c => ({
|
47
|
+
length: c.length,
|
48
|
+
refPositions: c.reference_positions
|
49
|
+
}))
|
50
|
+
},
|
51
|
+
belongsToAnchor: anchor && (
|
52
|
+
isReference
|
53
|
+
? position >= (anchor.reference_positions[currentSource!] ?? -1) &&
|
54
|
+
position < ((anchor.reference_positions[currentSource!] ?? -1) + anchor.length)
|
55
|
+
: position >= anchor.transcription_position &&
|
56
|
+
position < (anchor.transcription_position + anchor.length)
|
57
|
+
),
|
58
|
+
belongsToGap: gap && (
|
59
|
+
isReference
|
60
|
+
? gap.corrections[0]?.reference_positions?.[currentSource!] !== undefined &&
|
61
|
+
position >= (gap.corrections[0].reference_positions![currentSource!]) &&
|
62
|
+
position < (gap.corrections[0].reference_positions![currentSource!] + gap.corrections[0].length)
|
63
|
+
: position >= gap.transcription_position &&
|
64
|
+
position < (gap.transcription_position + gap.length)
|
65
|
+
)
|
66
|
+
}
|
67
|
+
}, null, 2))
|
68
|
+
|
69
|
+
const belongsToAnchor = anchor && (
|
70
|
+
isReference
|
71
|
+
? position >= (anchor.reference_positions[currentSource!] ?? -1) &&
|
72
|
+
position < ((anchor.reference_positions[currentSource!] ?? -1) + anchor.length)
|
73
|
+
: position >= anchor.transcription_position &&
|
74
|
+
position < (anchor.transcription_position + anchor.length)
|
75
|
+
)
|
76
|
+
|
77
|
+
const belongsToGap = gap && (
|
78
|
+
isReference
|
79
|
+
? gap.corrections[0]?.reference_positions?.[currentSource!] !== undefined &&
|
80
|
+
position >= (gap.corrections[0].reference_positions![currentSource!]) &&
|
81
|
+
position < (gap.corrections[0].reference_positions![currentSource!] + gap.corrections[0].length)
|
82
|
+
: position >= gap.transcription_position &&
|
83
|
+
position < (gap.transcription_position + gap.length)
|
84
|
+
)
|
85
|
+
|
86
|
+
if (mode === 'highlight' || mode === 'edit') {
|
87
|
+
onWordClick?.({
|
88
|
+
wordIndex: position,
|
89
|
+
type: belongsToAnchor ? 'anchor' : belongsToGap ? 'gap' : 'other',
|
90
|
+
anchor: belongsToAnchor ? anchor : undefined,
|
91
|
+
gap: belongsToGap ? gap : undefined
|
92
|
+
})
|
93
|
+
} else if (mode === 'details') {
|
94
|
+
if (belongsToAnchor && anchor) {
|
95
|
+
onElementClick({
|
96
|
+
type: 'anchor',
|
97
|
+
data: {
|
98
|
+
...anchor,
|
99
|
+
position,
|
100
|
+
word
|
101
|
+
}
|
102
|
+
})
|
103
|
+
} else if (belongsToGap && gap) {
|
104
|
+
onElementClick({
|
105
|
+
type: 'gap',
|
106
|
+
data: {
|
107
|
+
...gap,
|
108
|
+
position,
|
109
|
+
word
|
110
|
+
}
|
111
|
+
})
|
112
|
+
} else if (!isReference) {
|
113
|
+
// Create synthetic gap for non-sequence words (transcription view only)
|
114
|
+
const syntheticGap: GapSequence = {
|
115
|
+
text: word,
|
116
|
+
words: [word],
|
117
|
+
transcription_position: position,
|
118
|
+
length: 1,
|
119
|
+
corrections: [],
|
120
|
+
preceding_anchor: null,
|
121
|
+
following_anchor: null,
|
122
|
+
reference_words: {}
|
123
|
+
}
|
124
|
+
onElementClick({
|
125
|
+
type: 'gap',
|
126
|
+
data: {
|
127
|
+
...syntheticGap,
|
128
|
+
position: 0,
|
129
|
+
word
|
130
|
+
}
|
131
|
+
})
|
132
|
+
}
|
133
|
+
}
|
134
|
+
}, [mode, onWordClick, onElementClick, isReference, currentSource])
|
135
|
+
|
136
|
+
return { handleWordClick }
|
137
|
+
}
|
@@ -0,0 +1,99 @@
|
|
1
|
+
import { AnchorSequence, GapSequence, HighlightInfo, InteractionMode, LyricsData, LyricsSegment } from '../../types'
|
2
|
+
import { ModalContent } from '../LyricsAnalyzer'
|
3
|
+
|
4
|
+
// Add FlashType definition directly in shared types
|
5
|
+
export type FlashType = 'anchor' | 'corrected' | 'uncorrected' | 'word' | null
|
6
|
+
|
7
|
+
// Common word click handling
|
8
|
+
export interface WordClickInfo {
|
9
|
+
wordIndex: number
|
10
|
+
type: 'anchor' | 'gap' | 'other'
|
11
|
+
anchor?: AnchorSequence
|
12
|
+
gap?: GapSequence
|
13
|
+
}
|
14
|
+
|
15
|
+
// Base props shared between components
|
16
|
+
export interface BaseViewProps {
|
17
|
+
onElementClick: (content: ModalContent) => void
|
18
|
+
onWordClick?: (info: WordClickInfo) => void
|
19
|
+
flashingType: FlashType
|
20
|
+
highlightInfo: HighlightInfo | null
|
21
|
+
mode: InteractionMode
|
22
|
+
}
|
23
|
+
|
24
|
+
// Base word position interface - remove the word property from here
|
25
|
+
export interface BaseWordPosition {
|
26
|
+
type: 'anchor' | 'gap' | 'other'
|
27
|
+
sequence?: AnchorSequence | GapSequence
|
28
|
+
}
|
29
|
+
|
30
|
+
// Transcription-specific word position with timing info
|
31
|
+
export interface TranscriptionWordPosition extends BaseWordPosition {
|
32
|
+
position: number
|
33
|
+
isInRange: boolean
|
34
|
+
word: {
|
35
|
+
text: string
|
36
|
+
start_time?: number
|
37
|
+
end_time?: number
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
// Reference-specific word position with simple string word
|
42
|
+
export interface ReferenceWordPosition extends BaseWordPosition {
|
43
|
+
index: number
|
44
|
+
isHighlighted: boolean
|
45
|
+
word: string // Simple string word for reference view
|
46
|
+
}
|
47
|
+
|
48
|
+
// Word component props
|
49
|
+
export interface WordProps {
|
50
|
+
word: string
|
51
|
+
shouldFlash: boolean
|
52
|
+
isAnchor?: boolean
|
53
|
+
isCorrectedGap?: boolean
|
54
|
+
isUncorrectedGap?: boolean
|
55
|
+
isCurrentlyPlaying?: boolean
|
56
|
+
padding?: string
|
57
|
+
onClick?: () => void
|
58
|
+
}
|
59
|
+
|
60
|
+
// Text segment props
|
61
|
+
export interface TextSegmentProps extends BaseViewProps {
|
62
|
+
wordPositions: TranscriptionWordPosition[] | ReferenceWordPosition[]
|
63
|
+
}
|
64
|
+
|
65
|
+
// View-specific props
|
66
|
+
export interface TranscriptionViewProps extends BaseViewProps {
|
67
|
+
data: LyricsData
|
68
|
+
onPlaySegment?: (startTime: number) => void
|
69
|
+
currentTime?: number
|
70
|
+
}
|
71
|
+
|
72
|
+
// Add LinePosition type here since it's used in multiple places
|
73
|
+
export interface LinePosition {
|
74
|
+
position: number
|
75
|
+
lineNumber: number
|
76
|
+
isEmpty?: boolean
|
77
|
+
}
|
78
|
+
|
79
|
+
// Reference-specific props
|
80
|
+
export interface ReferenceViewProps extends BaseViewProps {
|
81
|
+
referenceTexts: Record<string, string>
|
82
|
+
anchors: LyricsData['anchor_sequences']
|
83
|
+
gaps: LyricsData['gap_sequences']
|
84
|
+
currentSource: 'genius' | 'spotify'
|
85
|
+
onSourceChange: (source: 'genius' | 'spotify') => void
|
86
|
+
corrected_segments: LyricsSegment[]
|
87
|
+
}
|
88
|
+
|
89
|
+
// Update HighlightedTextProps to include linePositions
|
90
|
+
export interface HighlightedTextProps extends BaseViewProps {
|
91
|
+
text?: string
|
92
|
+
wordPositions?: TranscriptionWordPosition[]
|
93
|
+
anchors: AnchorSequence[]
|
94
|
+
gaps: GapSequence[]
|
95
|
+
isReference?: boolean
|
96
|
+
currentSource?: 'genius' | 'spotify'
|
97
|
+
preserveSegments?: boolean
|
98
|
+
linePositions?: LinePosition[]
|
99
|
+
}
|