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
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Lyrics Transcriber Analyzer</title>
8
- <script type="module" crossorigin src="/assets/index-DqFgiUni.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-CQCER5Fo.js"></script>
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -34,7 +34,6 @@ export default function App() {
34
34
  try {
35
35
  const client = new LiveApiClient(baseUrl)
36
36
  const data = await client.getCorrectionData()
37
- console.log('Fetched data:', data)
38
37
  setData(data)
39
38
  } catch (err) {
40
39
  const error = err as Error
@@ -168,7 +167,12 @@ export default function App() {
168
167
  }
169
168
 
170
169
  return (
171
- <Box sx={{ p: 3 }}>
170
+ <Box sx={{
171
+ p: 3,
172
+ pb: 6, // Add bottom padding to ensure content doesn't get cut off
173
+ maxWidth: '100%',
174
+ overflowX: 'hidden'
175
+ }}>
172
176
  {error && (
173
177
  <Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
174
178
  {error}
@@ -4,6 +4,7 @@ import { CorrectionData } from './types';
4
4
  export interface ApiClient {
5
5
  getCorrectionData: () => Promise<CorrectionData>;
6
6
  submitCorrections: (data: CorrectionData) => Promise<void>;
7
+ getAudioUrl: () => string;
7
8
  }
8
9
 
9
10
  // Add new interface for the minimal update payload
@@ -45,6 +46,10 @@ export class LiveApiClient implements ApiClient {
45
46
  throw new Error(`API error: ${response.statusText}`);
46
47
  }
47
48
  }
49
+
50
+ getAudioUrl(): string {
51
+ return `${this.baseUrl}/audio`
52
+ }
48
53
  }
49
54
 
50
55
  export class FileOnlyClient implements ApiClient {
@@ -56,4 +61,8 @@ export class FileOnlyClient implements ApiClient {
56
61
  async submitCorrections(_data: CorrectionData): Promise<void> {
57
62
  throw new Error('Not supported in file-only mode');
58
63
  }
64
+
65
+ getAudioUrl(): string {
66
+ throw new Error('Not supported in file-only mode');
67
+ }
59
68
  }
@@ -0,0 +1,155 @@
1
+ import { Box, IconButton, Slider, Typography } from '@mui/material'
2
+ import PlayArrowIcon from '@mui/icons-material/PlayArrow'
3
+ import PauseIcon from '@mui/icons-material/Pause'
4
+ import { useEffect, useRef, useState, useCallback } from 'react'
5
+ import { ApiClient } from '../api'
6
+
7
+ interface AudioPlayerProps {
8
+ apiClient: ApiClient | null,
9
+ onTimeUpdate?: (time: number) => void
10
+ }
11
+
12
+ export default function AudioPlayer({ apiClient, onTimeUpdate }: AudioPlayerProps) {
13
+ const [isPlaying, setIsPlaying] = useState(false)
14
+ const [currentTime, setCurrentTime] = useState(0)
15
+ const [duration, setDuration] = useState(0)
16
+ const audioRef = useRef<HTMLAudioElement | null>(null)
17
+
18
+ useEffect(() => {
19
+ if (!apiClient) return
20
+
21
+ const audio = new Audio(apiClient.getAudioUrl())
22
+ audioRef.current = audio
23
+
24
+ // Add requestAnimationFrame for smoother updates
25
+ let animationFrameId: number
26
+
27
+ const updateTime = () => {
28
+ const time = audio.currentTime
29
+ setCurrentTime(time)
30
+ onTimeUpdate?.(time)
31
+ animationFrameId = requestAnimationFrame(updateTime)
32
+ }
33
+
34
+ audio.addEventListener('play', () => {
35
+ updateTime()
36
+ })
37
+
38
+ audio.addEventListener('pause', () => {
39
+ cancelAnimationFrame(animationFrameId)
40
+ })
41
+
42
+ audio.addEventListener('ended', () => {
43
+ cancelAnimationFrame(animationFrameId)
44
+ setIsPlaying(false)
45
+ setCurrentTime(0)
46
+ })
47
+
48
+ audio.addEventListener('loadedmetadata', () => {
49
+ setDuration(audio.duration)
50
+ })
51
+
52
+ return () => {
53
+ cancelAnimationFrame(animationFrameId)
54
+ audio.pause()
55
+ audio.src = ''
56
+ audioRef.current = null
57
+ }
58
+ }, [apiClient, onTimeUpdate])
59
+
60
+ const handlePlayPause = () => {
61
+ if (!audioRef.current) return
62
+
63
+ if (isPlaying) {
64
+ audioRef.current.pause()
65
+ } else {
66
+ audioRef.current.play()
67
+ }
68
+ setIsPlaying(!isPlaying)
69
+ }
70
+
71
+ const handleSeek = (_: Event, newValue: number | number[]) => {
72
+ if (!audioRef.current) return
73
+ const time = newValue as number
74
+ audioRef.current.currentTime = time
75
+ setCurrentTime(time)
76
+ }
77
+
78
+ const formatTime = (seconds: number) => {
79
+ const mins = Math.floor(seconds / 60)
80
+ const secs = Math.floor(seconds % 60)
81
+ return `${mins}:${secs.toString().padStart(2, '0')}`
82
+ }
83
+
84
+ // Add this method to expose seeking functionality
85
+ const seekAndPlay = (time: number) => {
86
+ if (!audioRef.current) return
87
+
88
+ audioRef.current.currentTime = time
89
+ setCurrentTime(time)
90
+ audioRef.current.play()
91
+ setIsPlaying(true)
92
+ }
93
+
94
+ const togglePlayback = useCallback(() => {
95
+ if (!audioRef.current) return
96
+
97
+ if (isPlaying) {
98
+ audioRef.current.pause()
99
+ } else {
100
+ audioRef.current.play()
101
+ }
102
+ setIsPlaying(!isPlaying)
103
+ }, [isPlaying])
104
+
105
+ // Expose both methods globally
106
+ useEffect(() => {
107
+ if (!apiClient) return
108
+
109
+ const win = window as any
110
+ win.seekAndPlayAudio = seekAndPlay
111
+ win.toggleAudioPlayback = togglePlayback
112
+
113
+ return () => {
114
+ delete win.seekAndPlayAudio
115
+ delete win.toggleAudioPlayback
116
+ }
117
+ }, [apiClient, togglePlayback])
118
+
119
+ if (!apiClient) return null
120
+
121
+ return (
122
+ <Box sx={{
123
+ display: 'flex',
124
+ alignItems: 'center',
125
+ gap: 2,
126
+ p: 2,
127
+ backgroundColor: 'background.paper',
128
+ borderRadius: 1,
129
+ boxShadow: 1
130
+ }}>
131
+ <IconButton
132
+ onClick={handlePlayPause}
133
+ size="large"
134
+ >
135
+ {isPlaying ? <PauseIcon /> : <PlayArrowIcon />}
136
+ </IconButton>
137
+
138
+ <Typography sx={{ minWidth: 45 }}>
139
+ {formatTime(currentTime)}
140
+ </Typography>
141
+
142
+ <Slider
143
+ value={currentTime}
144
+ min={0}
145
+ max={duration}
146
+ onChange={handleSeek}
147
+ sx={{ mx: 2 }}
148
+ />
149
+
150
+ <Typography sx={{ minWidth: 45 }}>
151
+ {formatTime(duration)}
152
+ </Typography>
153
+ </Box>
154
+ )
155
+ }
@@ -1,5 +1,5 @@
1
1
  import { Grid, Paper, Box, Typography } from '@mui/material'
2
- import { COLORS } from './constants'
2
+ import { COLORS } from './shared/constants'
3
3
 
4
4
  interface MetricProps {
5
5
  color?: string
@@ -4,82 +4,31 @@ import {
4
4
  DialogContent,
5
5
  IconButton,
6
6
  Grid,
7
- Typography,
8
- Box,
9
- TextField,
10
- Button,
7
+ Typography
11
8
  } from '@mui/material'
12
9
  import CloseIcon from '@mui/icons-material/Close'
13
10
  import { ModalContent } from './LyricsAnalyzer'
14
- import { WordCorrection } from '../types'
15
- import { useState, useEffect } from 'react'
16
11
 
17
12
  interface DetailsModalProps {
18
13
  open: boolean
19
14
  content: ModalContent | null
20
15
  onClose: () => void
21
- onUpdateCorrection?: (position: number, updatedWords: string[]) => void
22
- isReadOnly?: boolean
23
16
  }
24
17
 
25
18
  export default function DetailsModal({
26
19
  open,
27
20
  content,
28
21
  onClose,
29
- onUpdateCorrection,
30
- isReadOnly = true
31
22
  }: 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
23
  if (!content) return null
44
24
 
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 = () => {
25
+ const getCurrentWord = () => {
74
26
  if (content.type === 'gap') {
75
- setEditedWord(content.data.word)
76
- setIsEditing(false)
27
+ return content.data.word
28
+ } else if (content.type === 'anchor') {
29
+ return content.data.word ?? content.data.words[0]
77
30
  }
78
- }
79
-
80
- const handleWordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
81
- console.log('Word changed to:', event.target.value)
82
- setEditedWord(event.target.value)
31
+ return ''
83
32
  }
84
33
 
85
34
  const renderContent = () => {
@@ -87,155 +36,41 @@ export default function DetailsModal({
87
36
  case 'anchor':
88
37
  return (
89
38
  <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
39
  <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
- }
40
+ title="Selected Word"
41
+ value={`"${getCurrentWord()}"`}
100
42
  />
101
43
  <GridItem
102
- title="Confidence"
103
- value={`${(content.data.confidence * 100).toFixed(2)}%`}
44
+ title="Full Text"
45
+ value={`"${content.data.text}"`}
104
46
  />
47
+ <GridItem title="Position" value={content.data.position} />
105
48
  <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
- )}
49
+ {/* ... rest of anchor details rendering ... */}
130
50
  </Grid>
131
51
  )
132
52
 
133
53
  case 'gap':
134
54
  return (
135
55
  <Grid container spacing={2}>
56
+ <GridItem
57
+ title="Selected Word"
58
+ value={`"${getCurrentWord()}"`}
59
+ />
136
60
  <GridItem
137
61
  title="Transcribed Text"
138
62
  value={`"${content.data.text}"`}
139
63
  />
140
64
  <GridItem
141
65
  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>
66
+ value={`"${content.data.words.map(word => {
67
+ const correction = content.data.corrections.find(
68
+ c => c.original_word === word
188
69
  )
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
- }
70
+ return correction ? correction.corrected_word : word
71
+ }).join(' ')}"`}
226
72
  />
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
- )}
73
+ {/* ... rest of gap details rendering ... */}
239
74
  </Grid>
240
75
  )
241
76
 
@@ -250,9 +85,6 @@ export default function DetailsModal({
250
85
  onClose={onClose}
251
86
  maxWidth="sm"
252
87
  fullWidth
253
- PaperProps={{
254
- sx: { position: 'relative' },
255
- }}
256
88
  >
257
89
  <IconButton
258
90
  onClick={onClose}
@@ -265,7 +97,7 @@ export default function DetailsModal({
265
97
  <CloseIcon />
266
98
  </IconButton>
267
99
  <DialogTitle>
268
- {content.type.charAt(0).toUpperCase() + content.type.slice(1)} Details
100
+ {content.type.charAt(0).toUpperCase() + content.type.slice(1)} Details - "{getCurrentWord()}"
269
101
  </DialogTitle>
270
102
  <DialogContent dividers>{renderContent()}</DialogContent>
271
103
  </Dialog>