lyrics-transcriber 0.41.0__py3-none-any.whl → 0.43.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 (78) hide show
  1. lyrics_transcriber/core/controller.py +30 -52
  2. lyrics_transcriber/correction/anchor_sequence.py +325 -150
  3. lyrics_transcriber/correction/corrector.py +224 -107
  4. lyrics_transcriber/correction/handlers/base.py +28 -10
  5. lyrics_transcriber/correction/handlers/extend_anchor.py +47 -24
  6. lyrics_transcriber/correction/handlers/levenshtein.py +75 -33
  7. lyrics_transcriber/correction/handlers/llm.py +290 -0
  8. lyrics_transcriber/correction/handlers/no_space_punct_match.py +81 -36
  9. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +46 -26
  10. lyrics_transcriber/correction/handlers/repeat.py +28 -11
  11. lyrics_transcriber/correction/handlers/sound_alike.py +68 -32
  12. lyrics_transcriber/correction/handlers/syllables_match.py +80 -30
  13. lyrics_transcriber/correction/handlers/word_count_match.py +36 -19
  14. lyrics_transcriber/correction/handlers/word_operations.py +68 -22
  15. lyrics_transcriber/correction/text_utils.py +3 -7
  16. lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
  17. lyrics_transcriber/frontend/.yarn/releases/yarn-4.6.0.cjs +934 -0
  18. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  19. lyrics_transcriber/frontend/dist/assets/{index-DKnNJHRK.js → index-D0Gr3Ep7.js} +16509 -9038
  20. lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +1 -0
  21. lyrics_transcriber/frontend/dist/index.html +1 -1
  22. lyrics_transcriber/frontend/package.json +6 -2
  23. lyrics_transcriber/frontend/src/App.tsx +18 -2
  24. lyrics_transcriber/frontend/src/api.ts +103 -6
  25. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +14 -6
  26. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +86 -59
  27. lyrics_transcriber/frontend/src/components/EditModal.tsx +281 -63
  28. lyrics_transcriber/frontend/src/components/FileUpload.tsx +2 -2
  29. lyrics_transcriber/frontend/src/components/Header.tsx +249 -0
  30. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +320 -266
  31. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +120 -0
  32. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +174 -52
  33. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +158 -114
  34. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +59 -78
  35. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +39 -16
  36. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +4 -10
  37. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +134 -68
  38. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +1 -1
  39. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +85 -115
  40. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  41. lyrics_transcriber/frontend/src/components/shared/types.ts +15 -7
  42. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +67 -0
  43. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  44. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +7 -7
  45. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +121 -0
  46. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  47. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  48. lyrics_transcriber/frontend/src/types.js +2 -0
  49. lyrics_transcriber/frontend/src/types.ts +70 -49
  50. lyrics_transcriber/frontend/src/validation.ts +132 -0
  51. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  52. lyrics_transcriber/frontend/yarn.lock +3752 -0
  53. lyrics_transcriber/lyrics/base_lyrics_provider.py +75 -12
  54. lyrics_transcriber/lyrics/file_provider.py +6 -5
  55. lyrics_transcriber/lyrics/genius.py +5 -2
  56. lyrics_transcriber/lyrics/spotify.py +58 -21
  57. lyrics_transcriber/output/ass/config.py +16 -5
  58. lyrics_transcriber/output/cdg.py +1 -1
  59. lyrics_transcriber/output/generator.py +22 -8
  60. lyrics_transcriber/output/plain_text.py +15 -10
  61. lyrics_transcriber/output/segment_resizer.py +16 -3
  62. lyrics_transcriber/output/subtitles.py +27 -1
  63. lyrics_transcriber/output/video.py +107 -1
  64. lyrics_transcriber/review/__init__.py +0 -1
  65. lyrics_transcriber/review/server.py +337 -164
  66. lyrics_transcriber/transcribers/audioshake.py +3 -0
  67. lyrics_transcriber/transcribers/base_transcriber.py +11 -3
  68. lyrics_transcriber/transcribers/whisper.py +11 -1
  69. lyrics_transcriber/types.py +151 -105
  70. lyrics_transcriber/utils/word_utils.py +27 -0
  71. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/METADATA +3 -1
  72. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/RECORD +75 -61
  73. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/WHEEL +1 -1
  74. lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js.map +0 -1
  75. lyrics_transcriber/frontend/package-lock.json +0 -4260
  76. lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +0 -202
  77. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/LICENSE +0 -0
  78. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,120 @@
1
+ import { Box, Typography, CircularProgress, Alert, Button } from '@mui/material'
2
+ import { useState, useEffect } from 'react'
3
+ import { ApiClient } from '../api'
4
+ import { CorrectionData } from '../types'
5
+
6
+ interface PreviewVideoSectionProps {
7
+ apiClient: ApiClient | null
8
+ isModalOpen: boolean
9
+ updatedData: CorrectionData
10
+ videoRef?: React.RefObject<HTMLVideoElement>
11
+ }
12
+
13
+ export default function PreviewVideoSection({
14
+ apiClient,
15
+ isModalOpen,
16
+ updatedData,
17
+ videoRef
18
+ }: PreviewVideoSectionProps) {
19
+ const [previewState, setPreviewState] = useState<{
20
+ status: 'loading' | 'ready' | 'error';
21
+ videoUrl?: string;
22
+ error?: string;
23
+ }>({ status: 'loading' });
24
+
25
+ // Generate preview when modal opens
26
+ useEffect(() => {
27
+ if (isModalOpen && apiClient) {
28
+ const generatePreview = async () => {
29
+ setPreviewState({ status: 'loading' });
30
+ try {
31
+ const response = await apiClient.generatePreviewVideo(updatedData);
32
+
33
+ if (response.status === 'error') {
34
+ setPreviewState({
35
+ status: 'error',
36
+ error: response.message || 'Failed to generate preview video'
37
+ });
38
+ return;
39
+ }
40
+
41
+ if (!response.preview_hash) {
42
+ setPreviewState({
43
+ status: 'error',
44
+ error: 'No preview hash received from server'
45
+ });
46
+ return;
47
+ }
48
+
49
+ const videoUrl = apiClient.getPreviewVideoUrl(response.preview_hash);
50
+ setPreviewState({
51
+ status: 'ready',
52
+ videoUrl
53
+ });
54
+ } catch (error) {
55
+ setPreviewState({
56
+ status: 'error',
57
+ error: (error as Error).message || 'Failed to generate preview video'
58
+ });
59
+ }
60
+ };
61
+
62
+ generatePreview();
63
+ }
64
+ }, [isModalOpen, apiClient, updatedData]);
65
+
66
+ if (!apiClient) return null;
67
+
68
+ return (
69
+ <Box sx={{ mb: 2 }}>
70
+ {previewState.status === 'loading' && (
71
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, p: 2 }}>
72
+ <CircularProgress size={24} />
73
+ <Typography>Generating preview video...</Typography>
74
+ </Box>
75
+ )}
76
+
77
+ {previewState.status === 'error' && (
78
+ <Box sx={{ mb: 2 }}>
79
+ <Alert
80
+ severity="error"
81
+ action={
82
+ <Button
83
+ color="inherit"
84
+ size="small"
85
+ onClick={() => {
86
+ // Re-trigger the effect by toggling isModalOpen
87
+ setPreviewState({ status: 'loading' });
88
+ }}
89
+ >
90
+ Retry
91
+ </Button>
92
+ }
93
+ >
94
+ {previewState.error}
95
+ </Alert>
96
+ </Box>
97
+ )}
98
+
99
+ {previewState.status === 'ready' && previewState.videoUrl && (
100
+ <Box sx={{
101
+ width: '100%',
102
+ margin: '0',
103
+ }}>
104
+ <video
105
+ ref={videoRef}
106
+ controls
107
+ src={previewState.videoUrl}
108
+ style={{
109
+ display: 'block',
110
+ width: '100%',
111
+ height: 'auto',
112
+ }}
113
+ >
114
+ Your browser does not support the video tag.
115
+ </video>
116
+ </Box>
117
+ )}
118
+ </Box>
119
+ );
120
+ }
@@ -1,13 +1,29 @@
1
1
  import { useMemo } from 'react'
2
- import { Paper, Typography, Box } from '@mui/material'
2
+ import { Paper, Typography, Box, IconButton } from '@mui/material'
3
3
  import { ReferenceViewProps } from './shared/types'
4
4
  import { calculateReferenceLinePositions } from './shared/utils/referenceLineCalculator'
5
5
  import { SourceSelector } from './shared/components/SourceSelector'
6
6
  import { HighlightedText } from './shared/components/HighlightedText'
7
- import { WordCorrection } from '@/types'
7
+ import { TranscriptionWordPosition } from './shared/types'
8
+ import { getWordsFromIds } from './shared/utils/wordUtils'
9
+ import ContentCopyIcon from '@mui/icons-material/ContentCopy'
10
+ import { styled } from '@mui/material/styles'
11
+
12
+ const SegmentControls = styled(Box)({
13
+ display: 'flex',
14
+ alignItems: 'center',
15
+ gap: '4px',
16
+ paddingTop: '3px',
17
+ paddingRight: '8px'
18
+ })
19
+
20
+ const TextContainer = styled(Box)({
21
+ flex: 1,
22
+ minWidth: 0,
23
+ })
8
24
 
9
25
  export default function ReferenceView({
10
- referenceTexts,
26
+ referenceSources,
11
27
  anchors,
12
28
  onElementClick,
13
29
  onWordClick,
@@ -17,58 +33,145 @@ export default function ReferenceView({
17
33
  onSourceChange,
18
34
  highlightInfo,
19
35
  mode,
20
- gaps
36
+ gaps,
37
+ corrections
21
38
  }: ReferenceViewProps) {
22
- // Get available sources from referenceTexts object
39
+ // Get available sources from referenceSources object
23
40
  const availableSources = useMemo(() =>
24
- Object.keys(referenceTexts) as Array<string>,
25
- [referenceTexts]
41
+ Object.keys(referenceSources),
42
+ [referenceSources]
43
+ )
44
+
45
+ // Ensure we always have a valid currentSource
46
+ const effectiveCurrentSource = useMemo(() =>
47
+ currentSource || (availableSources.length > 0 ? availableSources[0] : ''),
48
+ [currentSource, availableSources]
26
49
  )
27
50
 
51
+ // Create word positions for the reference view
52
+ const referenceWordPositions = useMemo(() => {
53
+ const positions: TranscriptionWordPosition[] = [];
54
+ const allPositions = new Map<number, TranscriptionWordPosition[]>();
55
+
56
+ // Map anchor words
57
+ anchors?.forEach(anchor => {
58
+ const position = anchor.reference_positions[effectiveCurrentSource];
59
+ if (position === undefined) return;
60
+
61
+ if (!allPositions.has(position)) {
62
+ allPositions.set(position, []);
63
+ }
64
+
65
+ const referenceWords = getWordsFromIds(
66
+ referenceSources[effectiveCurrentSource].segments,
67
+ anchor.reference_word_ids[effectiveCurrentSource] || []
68
+ );
69
+
70
+ referenceWords.forEach(word => {
71
+ const wordPosition: TranscriptionWordPosition = {
72
+ word: {
73
+ id: word.id,
74
+ text: word.text,
75
+ start_time: word.start_time ?? undefined,
76
+ end_time: word.end_time ?? undefined
77
+ },
78
+ type: 'anchor',
79
+ sequence: anchor,
80
+ isInRange: true
81
+ };
82
+ allPositions.get(position)!.push(wordPosition);
83
+ });
84
+ });
85
+
86
+ // Map gap words
87
+ gaps?.forEach(gap => {
88
+ const precedingAnchor = gap.preceding_anchor_id ?
89
+ anchors?.find(a => a.id === gap.preceding_anchor_id) :
90
+ undefined;
91
+ const followingAnchor = gap.following_anchor_id ?
92
+ anchors?.find(a => a.id === gap.following_anchor_id) :
93
+ undefined;
94
+
95
+ const position = precedingAnchor?.reference_positions[effectiveCurrentSource] ??
96
+ followingAnchor?.reference_positions[effectiveCurrentSource];
97
+
98
+ if (position === undefined) return;
99
+
100
+ const gapPosition = precedingAnchor ? position + 1 : position - 1;
101
+
102
+ if (!allPositions.has(gapPosition)) {
103
+ allPositions.set(gapPosition, []);
104
+ }
105
+
106
+ const referenceWords = getWordsFromIds(
107
+ referenceSources[effectiveCurrentSource].segments,
108
+ gap.reference_word_ids[effectiveCurrentSource] || []
109
+ );
110
+
111
+ referenceWords.forEach(word => {
112
+ // Find if this word has a correction
113
+ const isWordCorrected = corrections?.some(correction =>
114
+ correction.reference_positions?.[effectiveCurrentSource]?.toString() === word.id &&
115
+ gap.transcribed_word_ids.includes(correction.word_id)
116
+ );
117
+
118
+ const wordPosition: TranscriptionWordPosition = {
119
+ word: {
120
+ id: word.id,
121
+ text: word.text,
122
+ start_time: word.start_time ?? undefined,
123
+ end_time: word.end_time ?? undefined
124
+ },
125
+ type: 'gap',
126
+ sequence: gap,
127
+ isInRange: true,
128
+ isCorrected: isWordCorrected
129
+ };
130
+ allPositions.get(gapPosition)!.push(wordPosition);
131
+ });
132
+ });
133
+
134
+ // Sort by position and flatten
135
+ Array.from(allPositions.entries())
136
+ .sort(([a], [b]) => a - b)
137
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
138
+ .forEach(([_, words]) => {
139
+ positions.push(...words);
140
+ });
141
+
142
+ return positions;
143
+ }, [anchors, gaps, effectiveCurrentSource, referenceSources, corrections]);
144
+
28
145
  const { linePositions } = useMemo(() =>
29
146
  calculateReferenceLinePositions(
30
147
  corrected_segments,
31
148
  anchors,
32
- currentSource
149
+ effectiveCurrentSource
33
150
  ),
34
- [corrected_segments, anchors, currentSource]
151
+ [corrected_segments, anchors, effectiveCurrentSource]
35
152
  )
36
153
 
37
154
  // Create a mapping of reference words to their corrections
38
155
  const referenceCorrections = useMemo(() => {
39
- const corrections = new Map<string, string>();
156
+ const correctionMap = new Map<string, string>();
40
157
 
41
- console.log('Building referenceCorrections map:', {
42
- gapsCount: gaps.length,
43
- currentSource,
158
+ corrections?.forEach(correction => {
159
+ const referencePosition = correction.reference_positions?.[effectiveCurrentSource];
160
+ if (referencePosition !== undefined) {
161
+ correctionMap.set(referencePosition.toString(), correction.corrected_word);
162
+ }
44
163
  });
45
164
 
46
- gaps.forEach(gap => {
47
- gap.corrections.forEach((correction: WordCorrection) => {
48
- // Get the reference position for this correction
49
- const referencePosition = correction.reference_positions?.[currentSource];
50
-
51
- if (typeof referencePosition === 'number') {
52
- const wordId = `${currentSource}-word-${referencePosition}`;
53
- corrections.set(wordId, correction.corrected_word);
54
-
55
- console.log('Adding correction mapping:', {
56
- wordId,
57
- correctedWord: correction.corrected_word,
58
- referencePosition,
59
- correction
60
- });
61
- }
62
- });
63
- });
165
+ return correctionMap;
166
+ }, [corrections, effectiveCurrentSource]);
64
167
 
65
- console.log('Final referenceCorrections map:', {
66
- size: corrections.size,
67
- entries: Array.from(corrections.entries())
68
- });
168
+ // Get the segments for the current source
169
+ const currentSourceSegments = referenceSources[effectiveCurrentSource]?.segments || [];
69
170
 
70
- return corrections;
71
- }, [gaps, currentSource]);
171
+ // Helper function to copy text to clipboard
172
+ const copyToClipboard = (text: string) => {
173
+ navigator.clipboard.writeText(text);
174
+ };
72
175
 
73
176
  return (
74
177
  <Paper sx={{ p: 2 }}>
@@ -77,26 +180,45 @@ export default function ReferenceView({
77
180
  Reference Text
78
181
  </Typography>
79
182
  <SourceSelector
80
- currentSource={currentSource}
183
+ currentSource={effectiveCurrentSource}
81
184
  onSourceChange={onSourceChange}
82
185
  availableSources={availableSources}
83
186
  />
84
187
  </Box>
85
188
  <Box sx={{ display: 'flex', flexDirection: 'column' }}>
86
- <HighlightedText
87
- text={referenceTexts[currentSource]}
88
- anchors={anchors}
89
- onElementClick={onElementClick}
90
- onWordClick={onWordClick}
91
- flashingType={flashingType}
92
- highlightInfo={highlightInfo}
93
- mode={mode}
94
- isReference={true}
95
- currentSource={currentSource}
96
- linePositions={linePositions}
97
- referenceCorrections={referenceCorrections}
98
- gaps={gaps}
99
- />
189
+ {currentSourceSegments.map((segment, index) => (
190
+ <Box key={index} sx={{ display: 'flex', alignItems: 'flex-start', width: '100%' }}>
191
+ <SegmentControls>
192
+ <IconButton
193
+ size="small"
194
+ onClick={() => copyToClipboard(segment.words.map(w => w.text).join(' '))}
195
+ sx={{ padding: '2px' }}
196
+ >
197
+ <ContentCopyIcon fontSize="small" />
198
+ </IconButton>
199
+ </SegmentControls>
200
+ <TextContainer>
201
+ <HighlightedText
202
+ wordPositions={referenceWordPositions.filter(wp =>
203
+ segment.words.some(w => w.id === wp.word.id)
204
+ )}
205
+ segments={[segment]}
206
+ anchors={anchors}
207
+ onElementClick={onElementClick}
208
+ onWordClick={onWordClick}
209
+ flashingType={flashingType}
210
+ highlightInfo={highlightInfo}
211
+ mode={mode}
212
+ isReference={true}
213
+ currentSource={effectiveCurrentSource}
214
+ linePositions={linePositions}
215
+ referenceCorrections={referenceCorrections}
216
+ gaps={gaps}
217
+ preserveSegments={true}
218
+ />
219
+ </TextContainer>
220
+ </Box>
221
+ ))}
100
222
  </Box>
101
223
  </Paper>
102
224
  )