lyrics-transcriber 0.34.0__py3-none-any.whl → 0.34.2__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 (38) hide show
  1. lyrics_transcriber/correction/handlers/syllables_match.py +22 -2
  2. lyrics_transcriber/frontend/.gitignore +23 -0
  3. lyrics_transcriber/frontend/README.md +50 -0
  4. lyrics_transcriber/frontend/dist/assets/index-DqFgiUni.js +245 -0
  5. lyrics_transcriber/frontend/dist/index.html +13 -0
  6. lyrics_transcriber/frontend/dist/vite.svg +1 -0
  7. lyrics_transcriber/frontend/eslint.config.js +28 -0
  8. lyrics_transcriber/frontend/index.html +13 -0
  9. lyrics_transcriber/frontend/package-lock.json +4260 -0
  10. lyrics_transcriber/frontend/package.json +37 -0
  11. lyrics_transcriber/frontend/public/vite.svg +1 -0
  12. lyrics_transcriber/frontend/src/App.tsx +192 -0
  13. lyrics_transcriber/frontend/src/api.ts +59 -0
  14. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +155 -0
  15. lyrics_transcriber/frontend/src/components/DebugPanel.tsx +311 -0
  16. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +297 -0
  17. lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
  18. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +450 -0
  19. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +287 -0
  20. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +157 -0
  21. lyrics_transcriber/frontend/src/components/constants.ts +19 -0
  22. lyrics_transcriber/frontend/src/components/styles.ts +13 -0
  23. lyrics_transcriber/frontend/src/main.tsx +6 -0
  24. lyrics_transcriber/frontend/src/types.ts +158 -0
  25. lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
  26. lyrics_transcriber/frontend/tsconfig.app.json +26 -0
  27. lyrics_transcriber/frontend/tsconfig.json +25 -0
  28. lyrics_transcriber/frontend/tsconfig.node.json +23 -0
  29. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
  30. lyrics_transcriber/frontend/vite.config.d.ts +2 -0
  31. lyrics_transcriber/frontend/vite.config.js +6 -0
  32. lyrics_transcriber/frontend/vite.config.ts +7 -0
  33. lyrics_transcriber/review/server.py +18 -29
  34. {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/METADATA +1 -1
  35. {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/RECORD +38 -7
  36. {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/LICENSE +0 -0
  37. {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/WHEEL +0 -0
  38. {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "lyrics-transcriber-frontend",
3
+ "private": true,
4
+ "homepage": "https://nomadkaraoke.github.io/lyrics-transcriber-frontend",
5
+ "version": "0.0.0",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "vite",
9
+ "build": "tsc -b && vite build",
10
+ "lint": "eslint .",
11
+ "preview": "vite preview",
12
+ "predeploy": "npm run build",
13
+ "deploy": "gh-pages -d dist"
14
+ },
15
+ "dependencies": {
16
+ "@emotion/react": "^11.14.0",
17
+ "@emotion/styled": "^11.14.0",
18
+ "@mui/icons-material": "^6.3.0",
19
+ "@mui/material": "^6.3.0",
20
+ "react": "^18.3.1",
21
+ "react-dom": "^18.3.1"
22
+ },
23
+ "devDependencies": {
24
+ "@eslint/js": "^9.17.0",
25
+ "@types/react": "^18.3.18",
26
+ "@types/react-dom": "^18.3.5",
27
+ "@vitejs/plugin-react": "^4.3.4",
28
+ "eslint": "^9.17.0",
29
+ "eslint-plugin-react-hooks": "^5.0.0",
30
+ "eslint-plugin-react-refresh": "^0.4.16",
31
+ "gh-pages": "^6.3.0",
32
+ "globals": "^15.14.0",
33
+ "typescript": "~5.6.2",
34
+ "typescript-eslint": "^8.18.2",
35
+ "vite": "^6.0.5"
36
+ }
37
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
@@ -0,0 +1,192 @@
1
+ import UploadFileIcon from '@mui/icons-material/UploadFile'
2
+ import { Alert, Box, Button, Modal, Typography } from '@mui/material'
3
+ import { useEffect, useState } from 'react'
4
+ import { ApiClient, FileOnlyClient, LiveApiClient } from './api'
5
+ import CorrectionMetrics from './components/CorrectionMetrics'
6
+ import LyricsAnalyzer from './components/LyricsAnalyzer'
7
+ import { CorrectionData } from './types'
8
+
9
+ export default function App() {
10
+ const [data, setData] = useState<CorrectionData | null>(null)
11
+ const [showMetadata, setShowMetadata] = useState(false)
12
+ const [error, setError] = useState<string | null>(null)
13
+ const [apiClient, setApiClient] = useState<ApiClient | null>(null)
14
+ const [isReadOnly, setIsReadOnly] = useState(true)
15
+
16
+ useEffect(() => {
17
+ // Parse query parameters
18
+ const params = new URLSearchParams(window.location.search)
19
+ const encodedApiUrl = params.get('baseApiUrl')
20
+
21
+ if (encodedApiUrl) {
22
+ const baseApiUrl = decodeURIComponent(encodedApiUrl)
23
+ setApiClient(new LiveApiClient(baseApiUrl))
24
+ setIsReadOnly(false)
25
+ // Fetch initial data
26
+ fetchData(baseApiUrl)
27
+ } else {
28
+ setApiClient(new FileOnlyClient())
29
+ setIsReadOnly(true)
30
+ }
31
+ }, [])
32
+
33
+ const fetchData = async (baseUrl: string) => {
34
+ try {
35
+ const client = new LiveApiClient(baseUrl)
36
+ const data = await client.getCorrectionData()
37
+ console.log('Fetched data:', data)
38
+ setData(data)
39
+ } catch (err) {
40
+ const error = err as Error
41
+ setError(`Failed to fetch data: ${error.message}`)
42
+ }
43
+ }
44
+
45
+ const handleFileLoad = async () => {
46
+ const input = document.createElement('input')
47
+ input.type = 'file'
48
+ input.accept = '.json'
49
+
50
+ input.onchange = async (e) => {
51
+ const file = (e.target as HTMLInputElement).files?.[0]
52
+ if (!file) return
53
+
54
+ try {
55
+ const text = await file.text()
56
+ console.log('File contents:', text.slice(0, 500) + '...') // Show first 500 chars
57
+ const parsedData = JSON.parse(text) as CorrectionData
58
+ console.log('Parsed file data:', parsedData)
59
+ setData(parsedData)
60
+ } catch (err) {
61
+ const error = err as Error
62
+ setError(`Error loading file: ${error.message}. Please make sure it is a valid JSON file.`)
63
+ }
64
+ }
65
+
66
+ input.click()
67
+ }
68
+
69
+ const renderMetadataModal = () => {
70
+ if (!data) return null
71
+
72
+ return (
73
+ <Modal
74
+ open={showMetadata}
75
+ onClose={() => setShowMetadata(false)}
76
+ aria-labelledby="metadata-modal"
77
+ >
78
+ <Box sx={{
79
+ position: 'absolute',
80
+ top: '50%',
81
+ left: '50%',
82
+ transform: 'translate(-50%, -50%)',
83
+ width: 400,
84
+ bgcolor: 'background.paper',
85
+ boxShadow: 24,
86
+ p: 4,
87
+ borderRadius: 1,
88
+ }}>
89
+ <Typography variant="h6" gutterBottom>
90
+ Correction Process Details
91
+ </Typography>
92
+ <Box sx={{ mb: 2 }}>
93
+ <Typography variant="subtitle2" color="text.secondary">
94
+ Total Words
95
+ </Typography>
96
+ <Typography>
97
+ {data.metadata.total_words}
98
+ </Typography>
99
+ </Box>
100
+ <Box sx={{ mb: 2 }}>
101
+ <Typography variant="subtitle2" color="text.secondary">
102
+ Gap Sequences
103
+ </Typography>
104
+ <Typography>
105
+ {data.metadata.gap_sequences_count}
106
+ </Typography>
107
+ </Box>
108
+ <Box sx={{ mb: 2 }}>
109
+ <Typography variant="subtitle2" color="text.secondary">
110
+ Corrections Made
111
+ </Typography>
112
+ <Typography>
113
+ {data.corrections_made}
114
+ </Typography>
115
+ </Box>
116
+ <Box sx={{ mb: 2 }}>
117
+ <Typography variant="subtitle2" color="text.secondary">
118
+ Correction Ratio
119
+ </Typography>
120
+ <Typography>
121
+ {(data.metadata.correction_ratio * 100).toFixed(1)}%
122
+ </Typography>
123
+ </Box>
124
+ {/* Add any other metadata fields that are available */}
125
+ </Box>
126
+ </Modal>
127
+ )
128
+ }
129
+
130
+ if (!data) {
131
+ return (
132
+ <Box sx={{ p: 3 }}>
133
+ {error && (
134
+ <Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
135
+ {error}
136
+ </Alert>
137
+ )}
138
+ {isReadOnly ? (
139
+ <>
140
+ <Alert severity="info" sx={{ mb: 2 }}>
141
+ Running in read-only mode. Connect to an API to enable editing.
142
+ </Alert>
143
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
144
+ <Typography variant="h4">
145
+ Lyrics Correction Review
146
+ </Typography>
147
+ <Button
148
+ variant="outlined"
149
+ startIcon={<UploadFileIcon />}
150
+ onClick={handleFileLoad}
151
+ >
152
+ Load File
153
+ </Button>
154
+ </Box>
155
+ <Box sx={{ mb: 3 }}>
156
+ <CorrectionMetrics />
157
+ </Box>
158
+ </>
159
+ ) : (
160
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
161
+ <Typography variant="h6" color="text.secondary">
162
+ Loading Lyrics Correction Review...
163
+ </Typography>
164
+ </Box>
165
+ )}
166
+ </Box>
167
+ )
168
+ }
169
+
170
+ return (
171
+ <Box sx={{ p: 3 }}>
172
+ {error && (
173
+ <Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
174
+ {error}
175
+ </Alert>
176
+ )}
177
+ {isReadOnly && (
178
+ <Alert severity="info" sx={{ mb: 2 }}>
179
+ Running in read-only mode. Connect to an API to enable editing.
180
+ </Alert>
181
+ )}
182
+ <LyricsAnalyzer
183
+ data={data}
184
+ onFileLoad={handleFileLoad}
185
+ onShowMetadata={() => setShowMetadata(true)}
186
+ apiClient={apiClient}
187
+ isReadOnly={isReadOnly}
188
+ />
189
+ {renderMetadataModal()}
190
+ </Box>
191
+ )
192
+ }
@@ -0,0 +1,59 @@
1
+ import { CorrectionData } from './types';
2
+
3
+ // New file to handle API communication
4
+ export interface ApiClient {
5
+ getCorrectionData: () => Promise<CorrectionData>;
6
+ submitCorrections: (data: CorrectionData) => Promise<void>;
7
+ }
8
+
9
+ // Add new interface for the minimal update payload
10
+ interface CorrectionUpdate {
11
+ corrections: CorrectionData['corrections'];
12
+ corrected_segments: CorrectionData['corrected_segments'];
13
+ }
14
+
15
+ export class LiveApiClient implements ApiClient {
16
+ constructor(private baseUrl: string) {
17
+ this.baseUrl = baseUrl.replace(/\/$/, '')
18
+ }
19
+
20
+ async getCorrectionData(): Promise<CorrectionData> {
21
+ const response = await fetch(`${this.baseUrl}/correction-data`);
22
+ if (!response.ok) {
23
+ throw new Error(`API error: ${response.statusText}`);
24
+ }
25
+ const data = await response.json();
26
+ return data;
27
+ }
28
+
29
+ async submitCorrections(data: CorrectionData): Promise<void> {
30
+ // Extract only the needed fields
31
+ const updatePayload: CorrectionUpdate = {
32
+ corrections: data.corrections,
33
+ corrected_segments: data.corrected_segments
34
+ };
35
+
36
+ const response = await fetch(`${this.baseUrl}/complete`, {
37
+ method: 'POST',
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ },
41
+ body: JSON.stringify(updatePayload)
42
+ });
43
+
44
+ if (!response.ok) {
45
+ throw new Error(`API error: ${response.statusText}`);
46
+ }
47
+ }
48
+ }
49
+
50
+ export class FileOnlyClient implements ApiClient {
51
+ async getCorrectionData(): Promise<CorrectionData> {
52
+ throw new Error('Not supported in file-only mode');
53
+ }
54
+
55
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
56
+ async submitCorrections(_data: CorrectionData): Promise<void> {
57
+ throw new Error('Not supported in file-only mode');
58
+ }
59
+ }
@@ -0,0 +1,155 @@
1
+ import { Grid, Paper, Box, Typography } from '@mui/material'
2
+ import { COLORS } from './constants'
3
+
4
+ interface MetricProps {
5
+ color?: string
6
+ label: string
7
+ value: string | number
8
+ description: string
9
+ details?: Array<{ label: string, value: number }>
10
+ onClick?: () => void
11
+ }
12
+
13
+ function Metric({ color, label, value, description, details, onClick }: MetricProps) {
14
+ return (
15
+ <Paper
16
+ sx={{
17
+ p: 2,
18
+ cursor: onClick ? 'pointer' : 'default',
19
+ '&:hover': onClick ? {
20
+ bgcolor: 'action.hover'
21
+ } : undefined
22
+ }}
23
+ onClick={onClick}
24
+ >
25
+ <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
26
+ {color && (
27
+ <Box
28
+ sx={{
29
+ width: 16,
30
+ height: 16,
31
+ borderRadius: 1,
32
+ bgcolor: color,
33
+ mr: 1,
34
+ }}
35
+ />
36
+ )}
37
+ <Typography variant="subtitle2" color="text.secondary">
38
+ {label}
39
+ </Typography>
40
+ </Box>
41
+ <Typography variant="h6">
42
+ {value}
43
+ </Typography>
44
+ <Typography variant="caption" color="text.secondary">
45
+ {description}
46
+ </Typography>
47
+ {details && (
48
+ <Box sx={{ mt: 1, pt: 1, borderTop: 1, borderColor: 'divider' }}>
49
+ {details.map((detail, index) => (
50
+ <Box key={index} sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 0.5 }}>
51
+ <Typography variant="caption" color="text.secondary">
52
+ {detail.label}
53
+ </Typography>
54
+ <Typography variant="caption">
55
+ {detail.value}
56
+ </Typography>
57
+ </Box>
58
+ ))}
59
+ </Box>
60
+ )}
61
+ </Paper>
62
+ )
63
+ }
64
+
65
+ interface CorrectionMetricsProps {
66
+ // Anchor metrics
67
+ anchorCount?: number
68
+ multiSourceAnchors?: number
69
+ singleSourceMatches?: {
70
+ spotify: number
71
+ genius: number
72
+ }
73
+ // Gap metrics
74
+ correctedGapCount?: number
75
+ uncorrectedGapCount?: number
76
+ uncorrectedGaps?: Array<{
77
+ position: number
78
+ length: number
79
+ }>
80
+ // Correction details
81
+ replacedCount?: number
82
+ addedCount?: number
83
+ deletedCount?: number
84
+ onMetricClick?: {
85
+ anchor?: () => void
86
+ corrected?: () => void
87
+ uncorrected?: () => void
88
+ }
89
+ }
90
+
91
+ export default function CorrectionMetrics({
92
+ anchorCount,
93
+ multiSourceAnchors = 0,
94
+ singleSourceMatches = { spotify: 0, genius: 0 },
95
+ correctedGapCount = 0,
96
+ uncorrectedGapCount = 0,
97
+ uncorrectedGaps = [],
98
+ replacedCount = 0,
99
+ addedCount = 0,
100
+ deletedCount = 0,
101
+ onMetricClick
102
+ }: 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
+ };
109
+
110
+ return (
111
+ <Grid container spacing={2}>
112
+ <Grid item xs={12} sm={6} md={4}>
113
+ <Metric
114
+ color={COLORS.anchor}
115
+ label="Anchor Sequences"
116
+ value={anchorCount ?? '-'}
117
+ description="Matched sections between transcription and reference"
118
+ details={[
119
+ { label: "Multi-source Matches", value: multiSourceAnchors },
120
+ { label: "Spotify Only", value: singleSourceMatches.spotify },
121
+ { label: "Genius Only", value: singleSourceMatches.genius },
122
+ ]}
123
+ onClick={onMetricClick?.anchor}
124
+ />
125
+ </Grid>
126
+ <Grid item xs={12} sm={6} md={4}>
127
+ <Metric
128
+ color={COLORS.corrected}
129
+ label="Corrected Gaps"
130
+ value={correctedGapCount ?? '-'}
131
+ description="Successfully corrected sections"
132
+ details={[
133
+ { label: "Words Replaced", value: replacedCount },
134
+ { label: "Words Added", value: addedCount },
135
+ { label: "Words Deleted", value: deletedCount },
136
+ ]}
137
+ onClick={onMetricClick?.corrected}
138
+ />
139
+ </Grid>
140
+ <Grid item xs={12} sm={6} md={4}>
141
+ <Metric
142
+ color={COLORS.uncorrectedGap}
143
+ label="Uncorrected Gaps"
144
+ value={uncorrectedGapCount}
145
+ description="Sections that may need manual review"
146
+ details={uncorrectedGaps.map(gap => ({
147
+ label: formatPositionLabel(gap.position, gap.length),
148
+ value: gap.length
149
+ }))}
150
+ onClick={onMetricClick?.uncorrected}
151
+ />
152
+ </Grid>
153
+ </Grid>
154
+ )
155
+ }