lyrics-transcriber 0.43.0__py3-none-any.whl → 0.44.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 +58 -24
- lyrics_transcriber/correction/anchor_sequence.py +22 -8
- lyrics_transcriber/correction/corrector.py +47 -3
- lyrics_transcriber/correction/handlers/llm.py +15 -12
- lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
- lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
- lyrics_transcriber/frontend/dist/assets/{index-D0Gr3Ep7.js → index-DVoI6Z16.js} +10799 -7490
- lyrics_transcriber/frontend/dist/assets/index-DVoI6Z16.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/App.tsx +4 -4
- lyrics_transcriber/frontend/src/api.ts +37 -0
- lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +14 -10
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +62 -56
- lyrics_transcriber/frontend/src/components/EditModal.tsx +232 -237
- lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
- lyrics_transcriber/frontend/src/components/GlobalSyncEditor.tsx +675 -0
- lyrics_transcriber/frontend/src/components/Header.tsx +141 -101
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +146 -80
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +22 -13
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +1 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +29 -12
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +21 -4
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +29 -15
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +34 -16
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +186 -0
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +89 -41
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +9 -2
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +28 -3
- lyrics_transcriber/frontend/src/components/shared/types.ts +17 -2
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +63 -14
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +192 -0
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +267 -0
- lyrics_transcriber/frontend/src/main.tsx +7 -1
- lyrics_transcriber/frontend/src/theme.ts +177 -0
- lyrics_transcriber/frontend/src/types.ts +1 -1
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/lyrics/base_lyrics_provider.py +2 -2
- lyrics_transcriber/lyrics/user_input_provider.py +44 -0
- lyrics_transcriber/output/generator.py +40 -12
- lyrics_transcriber/output/video.py +18 -8
- lyrics_transcriber/review/server.py +238 -8
- {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/METADATA +3 -2
- {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/RECORD +47 -41
- lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +0 -1
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +0 -252
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +0 -110
- {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.0.dist-info}/WHEEL +0 -0
- {lyrics_transcriber-0.43.0.dist-info → lyrics_transcriber-0.44.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-
|
8
|
+
<script type="module" crossorigin src="/assets/index-DVoI6Z16.js"></script>
|
9
9
|
</head>
|
10
10
|
<body>
|
11
11
|
<div id="root"></div>
|
@@ -183,18 +183,18 @@ export default function App() {
|
|
183
183
|
|
184
184
|
return (
|
185
185
|
<Box sx={{
|
186
|
-
p:
|
187
|
-
pb:
|
186
|
+
p: 1.5,
|
187
|
+
pb: 3,
|
188
188
|
maxWidth: '100%',
|
189
189
|
overflowX: 'hidden'
|
190
190
|
}}>
|
191
191
|
{error && (
|
192
|
-
<Alert severity="error" sx={{ mb:
|
192
|
+
<Alert severity="error" sx={{ mb: 1 }} onClose={() => setError(null)}>
|
193
193
|
{error}
|
194
194
|
</Alert>
|
195
195
|
)}
|
196
196
|
{isReadOnly && (
|
197
|
-
<Alert severity="info" sx={{ mb:
|
197
|
+
<Alert severity="info" sx={{ mb: 1 }}>
|
198
198
|
Running in read-only mode. Connect to an API to enable editing.
|
199
199
|
</Alert>
|
200
200
|
)}
|
@@ -10,6 +10,7 @@ export interface ApiClient {
|
|
10
10
|
getPreviewVideoUrl: (previewHash: string) => string;
|
11
11
|
updateHandlers: (enabledHandlers: string[]) => Promise<CorrectionData>;
|
12
12
|
isUpdatingHandlers?: boolean;
|
13
|
+
addLyrics: (source: string, lyrics: string) => Promise<CorrectionData>;
|
13
14
|
}
|
14
15
|
|
15
16
|
// Add new interface for the minimal update payload
|
@@ -25,6 +26,12 @@ interface PreviewVideoResponse {
|
|
25
26
|
message?: string;
|
26
27
|
}
|
27
28
|
|
29
|
+
// Add new interface for adding lyrics
|
30
|
+
interface AddLyricsRequest {
|
31
|
+
source: string;
|
32
|
+
lyrics: string;
|
33
|
+
}
|
34
|
+
|
28
35
|
export class LiveApiClient implements ApiClient {
|
29
36
|
constructor(private baseUrl: string) {
|
30
37
|
this.baseUrl = baseUrl.replace(/\/$/, '')
|
@@ -131,6 +138,32 @@ export class LiveApiClient implements ApiClient {
|
|
131
138
|
console.log('API: Set isUpdatingHandlers to', this.isUpdatingHandlers);
|
132
139
|
}
|
133
140
|
}
|
141
|
+
|
142
|
+
async addLyrics(source: string, lyrics: string): Promise<CorrectionData> {
|
143
|
+
const payload: AddLyricsRequest = {
|
144
|
+
source,
|
145
|
+
lyrics
|
146
|
+
};
|
147
|
+
|
148
|
+
const response = await fetch(`${this.baseUrl}/add-lyrics`, {
|
149
|
+
method: 'POST',
|
150
|
+
headers: {
|
151
|
+
'Content-Type': 'application/json',
|
152
|
+
},
|
153
|
+
body: JSON.stringify(payload)
|
154
|
+
});
|
155
|
+
|
156
|
+
if (!response.ok) {
|
157
|
+
throw new Error(`API error: ${response.statusText}`);
|
158
|
+
}
|
159
|
+
|
160
|
+
const data = await response.json();
|
161
|
+
if (data.status === 'error') {
|
162
|
+
throw new Error(data.message || 'Failed to add lyrics');
|
163
|
+
}
|
164
|
+
|
165
|
+
return validateCorrectionData(data.data);
|
166
|
+
}
|
134
167
|
}
|
135
168
|
|
136
169
|
export class FileOnlyClient implements ApiClient {
|
@@ -161,5 +194,9 @@ export class FileOnlyClient implements ApiClient {
|
|
161
194
|
async updateHandlers(_enabledHandlers: string[]): Promise<CorrectionData> {
|
162
195
|
throw new Error('Not supported in file-only mode');
|
163
196
|
}
|
197
|
+
|
198
|
+
async addLyrics(): Promise<CorrectionData> {
|
199
|
+
throw new Error('Not supported in file-only mode');
|
200
|
+
}
|
164
201
|
}
|
165
202
|
|
@@ -0,0 +1,114 @@
|
|
1
|
+
import { useState } from 'react'
|
2
|
+
import {
|
3
|
+
Dialog,
|
4
|
+
DialogTitle,
|
5
|
+
DialogContent,
|
6
|
+
DialogActions,
|
7
|
+
Button,
|
8
|
+
TextField,
|
9
|
+
Box,
|
10
|
+
CircularProgress,
|
11
|
+
Typography
|
12
|
+
} from '@mui/material'
|
13
|
+
|
14
|
+
interface AddLyricsModalProps {
|
15
|
+
open: boolean
|
16
|
+
onClose: () => void
|
17
|
+
onSubmit: (source: string, lyrics: string) => Promise<void>
|
18
|
+
isSubmitting: boolean
|
19
|
+
}
|
20
|
+
|
21
|
+
export default function AddLyricsModal({
|
22
|
+
open,
|
23
|
+
onClose,
|
24
|
+
onSubmit,
|
25
|
+
isSubmitting
|
26
|
+
}: AddLyricsModalProps) {
|
27
|
+
const [source, setSource] = useState('')
|
28
|
+
const [lyrics, setLyrics] = useState('')
|
29
|
+
const [error, setError] = useState<string | null>(null)
|
30
|
+
|
31
|
+
const handleSubmit = async () => {
|
32
|
+
if (!source.trim()) {
|
33
|
+
setError('Please enter a source name')
|
34
|
+
return
|
35
|
+
}
|
36
|
+
if (!lyrics.trim()) {
|
37
|
+
setError('Please enter lyrics text')
|
38
|
+
return
|
39
|
+
}
|
40
|
+
|
41
|
+
try {
|
42
|
+
await onSubmit(source.trim(), lyrics.trim())
|
43
|
+
// Reset form on success
|
44
|
+
setSource('')
|
45
|
+
setLyrics('')
|
46
|
+
setError(null)
|
47
|
+
onClose()
|
48
|
+
} catch (err) {
|
49
|
+
setError(err instanceof Error ? err.message : 'Failed to add lyrics')
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
const handleClose = () => {
|
54
|
+
// Don't allow closing if currently submitting
|
55
|
+
if (isSubmitting) return
|
56
|
+
|
57
|
+
setSource('')
|
58
|
+
setLyrics('')
|
59
|
+
setError(null)
|
60
|
+
onClose()
|
61
|
+
}
|
62
|
+
|
63
|
+
return (
|
64
|
+
<Dialog
|
65
|
+
open={open}
|
66
|
+
onClose={handleClose}
|
67
|
+
maxWidth="md"
|
68
|
+
fullWidth
|
69
|
+
disableEscapeKeyDown={isSubmitting}
|
70
|
+
>
|
71
|
+
<DialogTitle>Add Reference Lyrics</DialogTitle>
|
72
|
+
<DialogContent>
|
73
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
|
74
|
+
{error && (
|
75
|
+
<Typography color="error" variant="body2">
|
76
|
+
{error}
|
77
|
+
</Typography>
|
78
|
+
)}
|
79
|
+
<TextField
|
80
|
+
label="Source Name"
|
81
|
+
value={source}
|
82
|
+
onChange={(e) => setSource(e.target.value)}
|
83
|
+
disabled={isSubmitting}
|
84
|
+
fullWidth
|
85
|
+
placeholder="e.g., Official Lyrics, Album Booklet"
|
86
|
+
/>
|
87
|
+
<TextField
|
88
|
+
label="Lyrics"
|
89
|
+
value={lyrics}
|
90
|
+
onChange={(e) => setLyrics(e.target.value)}
|
91
|
+
disabled={isSubmitting}
|
92
|
+
fullWidth
|
93
|
+
multiline
|
94
|
+
rows={10}
|
95
|
+
placeholder="Paste lyrics text here (one line per segment)"
|
96
|
+
/>
|
97
|
+
</Box>
|
98
|
+
</DialogContent>
|
99
|
+
<DialogActions>
|
100
|
+
<Button onClick={handleClose} disabled={isSubmitting}>
|
101
|
+
Cancel
|
102
|
+
</Button>
|
103
|
+
<Button
|
104
|
+
onClick={handleSubmit}
|
105
|
+
variant="contained"
|
106
|
+
disabled={isSubmitting}
|
107
|
+
startIcon={isSubmitting ? <CircularProgress size={20} /> : undefined}
|
108
|
+
>
|
109
|
+
{isSubmitting ? 'Adding...' : 'Add Lyrics'}
|
110
|
+
</Button>
|
111
|
+
</DialogActions>
|
112
|
+
</Dialog>
|
113
|
+
)
|
114
|
+
}
|
@@ -130,23 +130,24 @@ export default function AudioPlayer({ apiClient, onTimeUpdate, audioHash }: Audi
|
|
130
130
|
<Box sx={{
|
131
131
|
display: 'flex',
|
132
132
|
alignItems: 'center',
|
133
|
-
gap:
|
133
|
+
gap: 0.5,
|
134
134
|
backgroundColor: 'background.paper',
|
135
135
|
borderRadius: 1,
|
136
|
-
height:
|
136
|
+
height: '32px',
|
137
137
|
}}>
|
138
|
-
<Typography variant="body2" color="text.secondary" sx={{ mr:
|
138
|
+
<Typography variant="body2" color="text.secondary" sx={{ mr: 0.5, fontSize: '0.75rem' }}>
|
139
139
|
Playback:
|
140
140
|
</Typography>
|
141
141
|
|
142
142
|
<IconButton
|
143
143
|
onClick={handlePlayPause}
|
144
144
|
size="small"
|
145
|
+
sx={{ p: 0.5 }}
|
145
146
|
>
|
146
|
-
{isPlaying ? <PauseIcon /> : <PlayArrowIcon />}
|
147
|
+
{isPlaying ? <PauseIcon fontSize="small" /> : <PlayArrowIcon fontSize="small" />}
|
147
148
|
</IconButton>
|
148
149
|
|
149
|
-
<Typography variant="body2" sx={{ minWidth:
|
150
|
+
<Typography variant="body2" sx={{ minWidth: 32, fontSize: '0.75rem' }}>
|
150
151
|
{formatTime(currentTime)}
|
151
152
|
</Typography>
|
152
153
|
|
@@ -157,16 +158,19 @@ export default function AudioPlayer({ apiClient, onTimeUpdate, audioHash }: Audi
|
|
157
158
|
onChange={handleSeek}
|
158
159
|
size="small"
|
159
160
|
sx={{
|
160
|
-
width:
|
161
|
-
mx:
|
161
|
+
width: 150,
|
162
|
+
mx: 0.5,
|
162
163
|
'& .MuiSlider-thumb': {
|
163
|
-
width:
|
164
|
-
height:
|
164
|
+
width: 10,
|
165
|
+
height: 10,
|
166
|
+
},
|
167
|
+
'& .MuiSlider-rail, & .MuiSlider-track': {
|
168
|
+
height: 3
|
165
169
|
}
|
166
170
|
}}
|
167
171
|
/>
|
168
172
|
|
169
|
-
<Typography variant="body2" sx={{ minWidth:
|
173
|
+
<Typography variant="body2" sx={{ minWidth: 32, fontSize: '0.75rem' }}>
|
170
174
|
{formatTime(duration)}
|
171
175
|
</Typography>
|
172
176
|
</Box>
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import { Paper, Box, Typography } from '@mui/material'
|
2
2
|
import { COLORS } from './shared/constants'
|
3
3
|
|
4
4
|
interface MetricProps {
|
@@ -14,46 +14,50 @@ function Metric({ color, label, value, description, details, onClick }: MetricPr
|
|
14
14
|
return (
|
15
15
|
<Paper
|
16
16
|
sx={{
|
17
|
-
p:
|
17
|
+
p: 0.8,
|
18
|
+
pt: 0,
|
19
|
+
height: '100%',
|
18
20
|
cursor: onClick ? 'pointer' : 'default',
|
19
21
|
'&:hover': onClick ? {
|
20
22
|
bgcolor: 'action.hover'
|
21
|
-
} : undefined
|
23
|
+
} : undefined,
|
24
|
+
display: 'flex',
|
25
|
+
flexDirection: 'column'
|
22
26
|
}}
|
23
27
|
onClick={onClick}
|
24
28
|
>
|
25
|
-
<Box sx={{ display: 'flex', alignItems: 'center', mb:
|
29
|
+
<Box sx={{ display: 'flex', alignItems: 'center', mb: 0.5, mt: 0.8 }}>
|
26
30
|
{color && (
|
27
31
|
<Box
|
28
32
|
sx={{
|
29
|
-
width:
|
30
|
-
height:
|
33
|
+
width: 12,
|
34
|
+
height: 12,
|
31
35
|
borderRadius: 1,
|
32
36
|
bgcolor: color,
|
33
|
-
mr:
|
37
|
+
mr: 0.5,
|
34
38
|
}}
|
35
39
|
/>
|
36
40
|
)}
|
37
|
-
<Typography variant="subtitle2" color="text.secondary">
|
41
|
+
<Typography variant="subtitle2" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
38
42
|
{label}
|
39
43
|
</Typography>
|
40
44
|
</Box>
|
41
|
-
<Box sx={{ display: 'flex', alignItems: 'baseline', gap:
|
42
|
-
<Typography variant="h6">
|
45
|
+
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.5, mb: 0.3 }}>
|
46
|
+
<Typography variant="h6" sx={{ fontSize: '1.1rem' }}>
|
43
47
|
{value}
|
44
48
|
</Typography>
|
45
49
|
</Box>
|
46
|
-
<Typography variant="caption" color="text.secondary">
|
50
|
+
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
47
51
|
{description}
|
48
52
|
</Typography>
|
49
53
|
{details && (
|
50
|
-
<Box sx={{ mt:
|
54
|
+
<Box sx={{ mt: 0.5, flex: 1, overflow: 'auto' }}>
|
51
55
|
{details.map((detail, index) => (
|
52
|
-
<Box key={index} sx={{ display: 'flex', justifyContent: 'space-between',
|
53
|
-
<Typography variant="caption" color="text.secondary">
|
56
|
+
<Box key={index} sx={{ display: 'flex', justifyContent: 'space-between', mt: 0.3 }}>
|
57
|
+
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
54
58
|
{detail.label}
|
55
59
|
</Typography>
|
56
|
-
<Typography variant="caption">
|
60
|
+
<Typography variant="caption" sx={{ fontSize: '0.7rem' }}>
|
57
61
|
{detail.value}
|
58
62
|
</Typography>
|
59
63
|
</Box>
|
@@ -111,46 +115,48 @@ export default function CorrectionMetrics({
|
|
111
115
|
Math.round((correctedWordCount / totalWords) * 100) : 0
|
112
116
|
|
113
117
|
return (
|
114
|
-
<
|
115
|
-
<
|
116
|
-
<
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
{
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
<
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
{
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
<
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
{
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
118
|
+
<Box sx={{ height: '100%', display: 'flex' }}>
|
119
|
+
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'row', gap: 1, height: '100%' }}>
|
120
|
+
<Box sx={{ flex: 1, height: '100%' }}>
|
121
|
+
<Metric
|
122
|
+
color={COLORS.anchor}
|
123
|
+
label="Anchor Sequences"
|
124
|
+
value={`${anchorCount ?? '-'} (${anchorPercentage}%)`}
|
125
|
+
description="Matched sections between transcription and reference"
|
126
|
+
details={[
|
127
|
+
{ label: 'Words in Anchors', value: anchorWordCount },
|
128
|
+
{ label: 'Multi-source Matches', value: multiSourceAnchors }
|
129
|
+
]}
|
130
|
+
onClick={onMetricClick?.anchor}
|
131
|
+
/>
|
132
|
+
</Box>
|
133
|
+
<Box sx={{ flex: 1, height: '100%' }}>
|
134
|
+
<Metric
|
135
|
+
color={COLORS.corrected}
|
136
|
+
label="Corrected Gaps"
|
137
|
+
value={`${correctedGapCount} (${correctedPercentage}%)`}
|
138
|
+
description="Successfully corrected sections"
|
139
|
+
details={[
|
140
|
+
{ label: 'Words Replaced', value: replacedCount },
|
141
|
+
{ label: 'Words Added / Deleted', value: `+${addedCount} / -${deletedCount}` }
|
142
|
+
]}
|
143
|
+
onClick={onMetricClick?.corrected}
|
144
|
+
/>
|
145
|
+
</Box>
|
146
|
+
<Box sx={{ flex: 1, height: '100%' }}>
|
147
|
+
<Metric
|
148
|
+
color={COLORS.uncorrectedGap}
|
149
|
+
label="Uncorrected Gaps"
|
150
|
+
value={`${uncorrectedGapCount} (${uncorrectedPercentage}%)`}
|
151
|
+
description="Sections that may need manual review"
|
152
|
+
details={[
|
153
|
+
{ label: 'Words Uncorrected', value: uncorrectedWordCount },
|
154
|
+
{ label: 'Number of Gaps', value: uncorrectedGaps.length }
|
155
|
+
]}
|
156
|
+
onClick={onMetricClick?.uncorrected}
|
157
|
+
/>
|
158
|
+
</Box>
|
159
|
+
</Box>
|
160
|
+
</Box>
|
155
161
|
)
|
156
162
|
}
|