lyrics-transcriber 0.35.1__py3-none-any.whl → 0.37.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 (41) hide show
  1. lyrics_transcriber/cli/cli_main.py +2 -0
  2. lyrics_transcriber/core/config.py +1 -1
  3. lyrics_transcriber/core/controller.py +35 -2
  4. lyrics_transcriber/correction/corrector.py +8 -8
  5. lyrics_transcriber/correction/handlers/base.py +4 -0
  6. lyrics_transcriber/correction/handlers/extend_anchor.py +9 -0
  7. lyrics_transcriber/correction/handlers/no_space_punct_match.py +21 -10
  8. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +21 -11
  9. lyrics_transcriber/correction/handlers/syllables_match.py +4 -4
  10. lyrics_transcriber/correction/handlers/word_count_match.py +19 -10
  11. lyrics_transcriber/frontend/dist/assets/index-BNNbsbVN.js +182 -0
  12. lyrics_transcriber/frontend/dist/index.html +1 -1
  13. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +18 -7
  14. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +28 -27
  15. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +108 -12
  16. lyrics_transcriber/frontend/src/components/EditModal.tsx +10 -2
  17. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +145 -141
  18. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +7 -2
  19. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +24 -12
  20. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +8 -15
  21. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +3 -3
  22. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +36 -51
  23. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +17 -19
  24. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +41 -33
  25. lyrics_transcriber/frontend/src/components/shared/types.ts +6 -6
  26. lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +146 -0
  27. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +24 -25
  28. lyrics_transcriber/frontend/src/types.ts +24 -23
  29. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  30. lyrics_transcriber/lyrics/base_lyrics_provider.py +1 -0
  31. lyrics_transcriber/lyrics/file_provider.py +89 -0
  32. lyrics_transcriber/output/cdg.py +32 -6
  33. lyrics_transcriber/output/video.py +17 -7
  34. lyrics_transcriber/review/server.py +24 -8
  35. {lyrics_transcriber-0.35.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/METADATA +1 -1
  36. {lyrics_transcriber-0.35.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/RECORD +39 -38
  37. {lyrics_transcriber-0.35.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/entry_points.txt +1 -0
  38. lyrics_transcriber/frontend/dist/assets/index-CQCER5Fo.js +0 -181
  39. lyrics_transcriber/frontend/src/components/shared/utils/newlineCalculator.ts +0 -37
  40. {lyrics_transcriber-0.35.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/LICENSE +0 -0
  41. {lyrics_transcriber-0.35.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/WHEEL +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-CQCER5Fo.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-BNNbsbVN.js"></script>
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -122,20 +122,23 @@ export default function AudioPlayer({ apiClient, onTimeUpdate }: AudioPlayerProp
122
122
  <Box sx={{
123
123
  display: 'flex',
124
124
  alignItems: 'center',
125
- gap: 2,
126
- p: 2,
125
+ gap: 1,
127
126
  backgroundColor: 'background.paper',
128
127
  borderRadius: 1,
129
- boxShadow: 1
128
+ height: 40, // Match ToggleButtonGroup height
130
129
  }}>
130
+ <Typography variant="body2" color="text.secondary" sx={{ mr: 1 }}>
131
+ Playback:
132
+ </Typography>
133
+
131
134
  <IconButton
132
135
  onClick={handlePlayPause}
133
- size="large"
136
+ size="small"
134
137
  >
135
138
  {isPlaying ? <PauseIcon /> : <PlayArrowIcon />}
136
139
  </IconButton>
137
140
 
138
- <Typography sx={{ minWidth: 45 }}>
141
+ <Typography variant="body2" sx={{ minWidth: 40 }}>
139
142
  {formatTime(currentTime)}
140
143
  </Typography>
141
144
 
@@ -144,10 +147,18 @@ export default function AudioPlayer({ apiClient, onTimeUpdate }: AudioPlayerProp
144
147
  min={0}
145
148
  max={duration}
146
149
  onChange={handleSeek}
147
- sx={{ mx: 2 }}
150
+ size="small"
151
+ sx={{
152
+ width: 200,
153
+ mx: 1,
154
+ '& .MuiSlider-thumb': {
155
+ width: 12,
156
+ height: 12,
157
+ }
158
+ }}
148
159
  />
149
160
 
150
- <Typography sx={{ minWidth: 45 }}>
161
+ <Typography variant="body2" sx={{ minWidth: 40 }}>
151
162
  {formatTime(duration)}
152
163
  </Typography>
153
164
  </Box>
@@ -6,7 +6,7 @@ interface MetricProps {
6
6
  label: string
7
7
  value: string | number
8
8
  description: string
9
- details?: Array<{ label: string, value: number }>
9
+ details?: Array<{ label: string, value: string | number }>
10
10
  onClick?: () => void
11
11
  }
12
12
 
@@ -38,9 +38,11 @@ function Metric({ color, label, value, description, details, onClick }: MetricPr
38
38
  {label}
39
39
  </Typography>
40
40
  </Box>
41
- <Typography variant="h6">
42
- {value}
43
- </Typography>
41
+ <Box sx={{ display: 'flex', alignItems: 'baseline', gap: 1, mb: 0.5 }}>
42
+ <Typography variant="h6">
43
+ {value}
44
+ </Typography>
45
+ </Box>
44
46
  <Typography variant="caption" color="text.secondary">
45
47
  {description}
46
48
  </Typography>
@@ -66,21 +68,20 @@ interface CorrectionMetricsProps {
66
68
  // Anchor metrics
67
69
  anchorCount?: number
68
70
  multiSourceAnchors?: number
69
- singleSourceMatches?: {
70
- spotify: number
71
- genius: number
72
- }
71
+ anchorWordCount?: number
73
72
  // Gap metrics
74
73
  correctedGapCount?: number
75
74
  uncorrectedGapCount?: number
76
75
  uncorrectedGaps?: Array<{
77
- position: number
76
+ position: string
78
77
  length: number
79
78
  }>
80
79
  // Correction details
81
80
  replacedCount?: number
82
81
  addedCount?: number
83
82
  deletedCount?: number
83
+ // Add total words count
84
+ totalWords?: number
84
85
  onMetricClick?: {
85
86
  anchor?: () => void
86
87
  corrected?: () => void
@@ -91,21 +92,23 @@ interface CorrectionMetricsProps {
91
92
  export default function CorrectionMetrics({
92
93
  anchorCount,
93
94
  multiSourceAnchors = 0,
94
- singleSourceMatches = { spotify: 0, genius: 0 },
95
+ anchorWordCount = 0,
95
96
  correctedGapCount = 0,
96
97
  uncorrectedGapCount = 0,
97
98
  uncorrectedGaps = [],
98
99
  replacedCount = 0,
99
100
  addedCount = 0,
100
101
  deletedCount = 0,
102
+ totalWords = 0,
101
103
  onMetricClick
102
104
  }: CorrectionMetricsProps) {
103
- const formatPositionLabel = (position: number, length: number) => {
104
- if (length === 1) {
105
- return `Position ${position}`;
106
- }
107
- return `Positions ${position}-${position + length - 1}`;
108
- };
105
+ // Calculate percentages based on word counts
106
+ const anchorPercentage = totalWords > 0 ? Math.round((anchorWordCount / totalWords) * 100) : 0
107
+ const uncorrectedWordCount = uncorrectedGaps?.reduce((sum, gap) => sum + gap.length, 0) ?? 0
108
+ const uncorrectedPercentage = totalWords > 0 ? Math.round((uncorrectedWordCount / totalWords) * 100) : 0
109
+ const correctedWordCount = replacedCount + addedCount
110
+ const correctedPercentage = totalWords > 0 ?
111
+ Math.round((correctedWordCount / totalWords) * 100) : 0
109
112
 
110
113
  return (
111
114
  <Grid container spacing={2}>
@@ -113,12 +116,11 @@ export default function CorrectionMetrics({
113
116
  <Metric
114
117
  color={COLORS.anchor}
115
118
  label="Anchor Sequences"
116
- value={anchorCount ?? '-'}
119
+ value={`${anchorCount ?? '-'} (${anchorPercentage}%)`}
117
120
  description="Matched sections between transcription and reference"
118
121
  details={[
122
+ { label: "Words in Anchors", value: anchorWordCount },
119
123
  { label: "Multi-source Matches", value: multiSourceAnchors },
120
- { label: "Spotify Only", value: singleSourceMatches.spotify },
121
- { label: "Genius Only", value: singleSourceMatches.genius },
122
124
  ]}
123
125
  onClick={onMetricClick?.anchor}
124
126
  />
@@ -127,12 +129,11 @@ export default function CorrectionMetrics({
127
129
  <Metric
128
130
  color={COLORS.corrected}
129
131
  label="Corrected Gaps"
130
- value={correctedGapCount ?? '-'}
132
+ value={`${correctedGapCount} (${correctedPercentage}%)`}
131
133
  description="Successfully corrected sections"
132
134
  details={[
133
135
  { label: "Words Replaced", value: replacedCount },
134
- { label: "Words Added", value: addedCount },
135
- { label: "Words Deleted", value: deletedCount },
136
+ { label: "Words Added / Deleted", value: `+${addedCount} / -${deletedCount}` },
136
137
  ]}
137
138
  onClick={onMetricClick?.corrected}
138
139
  />
@@ -141,12 +142,12 @@ export default function CorrectionMetrics({
141
142
  <Metric
142
143
  color={COLORS.uncorrectedGap}
143
144
  label="Uncorrected Gaps"
144
- value={uncorrectedGapCount}
145
+ value={`${uncorrectedGapCount} (${uncorrectedPercentage}%)`}
145
146
  description="Sections that may need manual review"
146
- details={uncorrectedGaps.map(gap => ({
147
- label: formatPositionLabel(gap.position, gap.length),
148
- value: gap.length
149
- }))}
147
+ details={[
148
+ { label: "Words Uncorrected", value: uncorrectedWordCount },
149
+ { label: "Number of Gaps", value: uncorrectedGapCount },
150
+ ]}
150
151
  onClick={onMetricClick?.uncorrected}
151
152
  />
152
153
  </Grid>
@@ -4,7 +4,8 @@ import {
4
4
  DialogContent,
5
5
  IconButton,
6
6
  Grid,
7
- Typography
7
+ Typography,
8
+ Box
8
9
  } from '@mui/material'
9
10
  import CloseIcon from '@mui/icons-material/Close'
10
11
  import { ModalContent } from './LyricsAnalyzer'
@@ -31,6 +32,8 @@ export default function DetailsModal({
31
32
  return ''
32
33
  }
33
34
 
35
+ const isCorrected = content.type === 'gap' && content.data.corrections?.length > 0
36
+
34
37
  const renderContent = () => {
35
38
  switch (content.type) {
36
39
  case 'anchor':
@@ -44,9 +47,31 @@ export default function DetailsModal({
44
47
  title="Full Text"
45
48
  value={`"${content.data.text}"`}
46
49
  />
47
- <GridItem title="Position" value={content.data.position} />
50
+ <GridItem title="Word ID" value={content.data.wordId} />
48
51
  <GridItem title="Length" value={`${content.data.length} words`} />
49
- {/* ... rest of anchor details rendering ... */}
52
+ <GridItem
53
+ title="Reference Word IDs"
54
+ value={content.data.reference_word_ids ?
55
+ Object.entries(content.data.reference_word_ids).map(([source, ids]) => (
56
+ `${source}: ${ids?.join(', ') ?? 'No IDs'}`
57
+ )).join('\n') : 'No reference word IDs'
58
+ }
59
+ />
60
+ <GridItem title="Confidence" value={content.data.confidence?.toFixed(3) ?? 'N/A'} />
61
+ {content.data.phrase_score && (
62
+ <GridItem
63
+ title="Phrase Score Details"
64
+ value={
65
+ <>
66
+ <Typography>Type: {content.data.phrase_score.phrase_type}</Typography>
67
+ <Typography>Natural Break: {content.data.phrase_score.natural_break_score?.toFixed(3) ?? 'N/A'}</Typography>
68
+ <Typography>Length: {content.data.phrase_score.length_score?.toFixed(3) ?? 'N/A'}</Typography>
69
+ <Typography>Total: {content.data.phrase_score.total_score?.toFixed(3) ?? 'N/A'}</Typography>
70
+ </>
71
+ }
72
+ />
73
+ )}
74
+ <GridItem title="Total Score" value={content.data.total_score?.toFixed(3) ?? 'N/A'} />
50
75
  </Grid>
51
76
  )
52
77
 
@@ -57,20 +82,90 @@ export default function DetailsModal({
57
82
  title="Selected Word"
58
83
  value={`"${getCurrentWord()}"`}
59
84
  />
60
- <GridItem
61
- title="Transcribed Text"
62
- value={`"${content.data.text}"`}
63
- />
64
85
  <GridItem
65
86
  title="Current Text"
66
- value={`"${content.data.words.map(word => {
67
- const correction = content.data.corrections.find(
87
+ value={`"${content.data.words?.map(word => {
88
+ const wordCorrection = content.data.corrections?.find(
68
89
  c => c.original_word === word
69
90
  )
70
- return correction ? correction.corrected_word : word
71
- }).join(' ')}"`}
91
+ return wordCorrection ? wordCorrection.corrected_word : word
92
+ }).join(' ') ?? content.data.word}"`}
93
+ />
94
+ <GridItem
95
+ title="Word ID"
96
+ value={content.data.wordId}
97
+ />
98
+ <GridItem
99
+ title="Length"
100
+ value={`${content.data.length} words`}
101
+ />
102
+ {content.data.preceding_anchor && (
103
+ <GridItem
104
+ title="Preceding Anchor"
105
+ value={`"${content.data.preceding_anchor.text}"`}
106
+ />
107
+ )}
108
+ <GridItem
109
+ title="Transcribed Text"
110
+ value={`"${content.data.text}"`}
72
111
  />
73
- {/* ... rest of gap details rendering ... */}
112
+ {content.data.following_anchor && (
113
+ <GridItem
114
+ title="Following Anchor"
115
+ value={`"${content.data.following_anchor.text}"`}
116
+ />
117
+ )}
118
+ {content.data.reference_words && (
119
+ <GridItem
120
+ title="Reference Words"
121
+ value={
122
+ <>
123
+ {content.data.reference_words.spotify && (
124
+ <Typography>Spotify: "{content.data.reference_words.spotify.join(' ')}"</Typography>
125
+ )}
126
+ {content.data.reference_words.genius && (
127
+ <Typography>Genius: "{content.data.reference_words.genius.join(' ')}"</Typography>
128
+ )}
129
+ </>
130
+ }
131
+ />
132
+ )}
133
+ {isCorrected && content.data.corrections && (
134
+ <GridItem
135
+ title="Correction Details"
136
+ value={
137
+ <>
138
+ {content.data.corrections.map((correction, index) => (
139
+ <Box key={index} sx={{ mb: 2, p: 1, border: '1px solid #ccc', borderRadius: '4px' }}>
140
+ <Typography variant="subtitle2" fontWeight="bold">Correction {index + 1}</Typography>
141
+ <Typography>Original: <strong>"{correction.original_word}"</strong></Typography>
142
+ <Typography>Corrected: <strong>"{correction.corrected_word}"</strong></Typography>
143
+ <Typography>Word ID: {correction.word_id}</Typography>
144
+ <Typography>Confidence: {correction.confidence?.toFixed(3) ?? 'N/A'}</Typography>
145
+ <Typography>Source: {correction.source}</Typography>
146
+ <Typography>Reason: {correction.reason}</Typography>
147
+ {correction.is_deletion && <Typography>Is Deletion: Yes</Typography>}
148
+ {correction.split_total && (
149
+ <Typography>Split: {correction.split_index} of {correction.split_total}</Typography>
150
+ )}
151
+ {correction.alternatives && Object.entries(correction.alternatives).length > 0 && (
152
+ <>
153
+ <Typography>Alternatives:</Typography>
154
+ <Box sx={{ pl: 2 }}>
155
+ {Object.entries(correction.alternatives).map(([word, score]) => (
156
+ <Typography key={word}>
157
+ "{word}": {score?.toFixed(3) ?? 'N/A'}
158
+ </Typography>
159
+ ))}
160
+ </Box>
161
+ </>
162
+ )}
163
+ </Box>
164
+ ))}
165
+ </>
166
+ }
167
+ />
168
+ )}
74
169
  </Grid>
75
170
  )
76
171
 
@@ -97,6 +192,7 @@ export default function DetailsModal({
97
192
  <CloseIcon />
98
193
  </IconButton>
99
194
  <DialogTitle>
195
+ {content.type === 'gap' && (isCorrected ? 'Corrected ' : 'Uncorrected ')}
100
196
  {content.type.charAt(0).toUpperCase() + content.type.slice(1)} Details - "{getCurrentWord()}"
101
197
  </DialogTitle>
102
198
  <DialogContent dividers>{renderContent()}</DialogContent>
@@ -23,6 +23,7 @@ import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
23
23
  import { LyricsSegment, Word } from '../types'
24
24
  import { useState, useEffect } from 'react'
25
25
  import TimelineEditor from './TimelineEditor'
26
+ import { nanoid } from 'nanoid'
26
27
 
27
28
  interface EditModalProps {
28
29
  open: boolean
@@ -91,6 +92,7 @@ export default function EditModal({
91
92
  // Add at end
92
93
  const lastWord = newWords[newWords.length - 1]
93
94
  newWord = {
95
+ id: nanoid(),
94
96
  text: '',
95
97
  start_time: lastWord.end_time,
96
98
  end_time: lastWord.end_time + 0.5,
@@ -106,6 +108,7 @@ export default function EditModal({
106
108
  (nextWord ? nextWord.start_time - 0.5 : 0)
107
109
 
108
110
  newWord = {
111
+ id: nanoid(),
109
112
  text: '',
110
113
  start_time: midTime - 0.25,
111
114
  end_time: midTime + 0.25,
@@ -133,12 +136,14 @@ export default function EditModal({
133
136
  const newWords = [...editedSegment.words]
134
137
  newWords.splice(index, 1,
135
138
  {
139
+ id: nanoid(),
136
140
  text: words[0],
137
141
  start_time: word.start_time,
138
142
  end_time: midTime,
139
143
  confidence: 1.0
140
144
  },
141
145
  {
146
+ id: nanoid(),
142
147
  text: words[1],
143
148
  start_time: midTime,
144
149
  end_time: word.end_time,
@@ -157,6 +162,7 @@ export default function EditModal({
157
162
  const newWords = [...editedSegment.words]
158
163
 
159
164
  newWords.splice(index, 2, {
165
+ id: nanoid(),
160
166
  text: `${word1.text} ${word2.text}`.trim(),
161
167
  start_time: word1.start_time,
162
168
  end_time: word2.end_time,
@@ -208,17 +214,19 @@ export default function EditModal({
208
214
  let updatedWords: Word[]
209
215
 
210
216
  if (newWords.length === editedSegment.words.length) {
211
- // If word count matches, keep original timestamps
217
+ // If word count matches, keep original timestamps and IDs
212
218
  updatedWords = editedSegment.words.map((word, index) => ({
219
+ id: word.id, // Keep original ID
213
220
  text: newWords[index],
214
221
  start_time: word.start_time,
215
222
  end_time: word.end_time,
216
223
  confidence: 1.0
217
224
  }))
218
225
  } else {
219
- // If word count differs, distribute time evenly
226
+ // If word count differs, distribute time evenly and generate new IDs
220
227
  const avgWordDuration = segmentDuration / newWords.length
221
228
  updatedWords = newWords.map((text, index) => ({
229
+ id: nanoid(), // Generate new ID
222
230
  text,
223
231
  start_time: editedSegment.start_time + (index * avgWordDuration),
224
232
  end_time: editedSegment.start_time + ((index + 1) * avgWordDuration),