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.
- lyrics_transcriber/core/controller.py +30 -52
- lyrics_transcriber/correction/anchor_sequence.py +325 -150
- lyrics_transcriber/correction/corrector.py +224 -107
- lyrics_transcriber/correction/handlers/base.py +28 -10
- lyrics_transcriber/correction/handlers/extend_anchor.py +47 -24
- lyrics_transcriber/correction/handlers/levenshtein.py +75 -33
- lyrics_transcriber/correction/handlers/llm.py +290 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +81 -36
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +46 -26
- lyrics_transcriber/correction/handlers/repeat.py +28 -11
- lyrics_transcriber/correction/handlers/sound_alike.py +68 -32
- lyrics_transcriber/correction/handlers/syllables_match.py +80 -30
- lyrics_transcriber/correction/handlers/word_count_match.py +36 -19
- lyrics_transcriber/correction/handlers/word_operations.py +68 -22
- lyrics_transcriber/correction/text_utils.py +3 -7
- lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
- lyrics_transcriber/frontend/.yarn/releases/yarn-4.6.0.cjs +934 -0
- lyrics_transcriber/frontend/.yarnrc.yml +3 -0
- lyrics_transcriber/frontend/dist/assets/{index-DKnNJHRK.js → index-D0Gr3Ep7.js} +16509 -9038
- lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/package.json +6 -2
- lyrics_transcriber/frontend/src/App.tsx +18 -2
- lyrics_transcriber/frontend/src/api.ts +103 -6
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +14 -6
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +86 -59
- lyrics_transcriber/frontend/src/components/EditModal.tsx +281 -63
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +2 -2
- lyrics_transcriber/frontend/src/components/Header.tsx +249 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +320 -266
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +120 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +174 -52
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +158 -114
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +59 -78
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +39 -16
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +4 -10
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +134 -68
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +1 -1
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +85 -115
- lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
- lyrics_transcriber/frontend/src/components/shared/types.ts +15 -7
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +67 -0
- lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +7 -7
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +121 -0
- lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
- lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
- lyrics_transcriber/frontend/src/types.js +2 -0
- lyrics_transcriber/frontend/src/types.ts +70 -49
- lyrics_transcriber/frontend/src/validation.ts +132 -0
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/yarn.lock +3752 -0
- lyrics_transcriber/lyrics/base_lyrics_provider.py +75 -12
- lyrics_transcriber/lyrics/file_provider.py +6 -5
- lyrics_transcriber/lyrics/genius.py +5 -2
- lyrics_transcriber/lyrics/spotify.py +58 -21
- lyrics_transcriber/output/ass/config.py +16 -5
- lyrics_transcriber/output/cdg.py +1 -1
- lyrics_transcriber/output/generator.py +22 -8
- lyrics_transcriber/output/plain_text.py +15 -10
- lyrics_transcriber/output/segment_resizer.py +16 -3
- lyrics_transcriber/output/subtitles.py +27 -1
- lyrics_transcriber/output/video.py +107 -1
- lyrics_transcriber/review/__init__.py +0 -1
- lyrics_transcriber/review/server.py +337 -164
- lyrics_transcriber/transcribers/audioshake.py +3 -0
- lyrics_transcriber/transcribers/base_transcriber.py +11 -3
- lyrics_transcriber/transcribers/whisper.py +11 -1
- lyrics_transcriber/types.py +151 -105
- lyrics_transcriber/utils/word_utils.py +27 -0
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/METADATA +3 -1
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/RECORD +75 -61
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/WHEEL +1 -1
- lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js.map +0 -1
- lyrics_transcriber/frontend/package-lock.json +0 -4260
- lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +0 -202
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.43.0.dist-info}/entry_points.txt +0 -0
@@ -1,18 +1,8 @@
|
|
1
1
|
import { useCallback } from 'react'
|
2
|
-
import { AnchorSequence, GapSequence, InteractionMode } from '../../../types'
|
2
|
+
import { AnchorSequence, GapSequence, InteractionMode, WordCorrection } from '../../../types'
|
3
3
|
import { ModalContent } from '../../LyricsAnalyzer'
|
4
4
|
import { WordClickInfo } from '../types'
|
5
5
|
|
6
|
-
// Define debug info type
|
7
|
-
interface WordDebugInfo {
|
8
|
-
wordSplitInfo?: {
|
9
|
-
text: string
|
10
|
-
startIndex: number
|
11
|
-
endIndex: number
|
12
|
-
}
|
13
|
-
nearbyAnchors?: AnchorSequence[]
|
14
|
-
}
|
15
|
-
|
16
6
|
export interface UseWordClickProps {
|
17
7
|
mode: InteractionMode
|
18
8
|
onElementClick: (content: ModalContent) => void
|
@@ -20,103 +10,93 @@ export interface UseWordClickProps {
|
|
20
10
|
isReference?: boolean
|
21
11
|
currentSource?: string
|
22
12
|
gaps?: GapSequence[]
|
13
|
+
anchors?: AnchorSequence[]
|
14
|
+
corrections?: WordCorrection[]
|
23
15
|
}
|
24
16
|
|
25
17
|
export function useWordClick({
|
26
18
|
mode,
|
27
19
|
onElementClick,
|
28
20
|
onWordClick,
|
29
|
-
isReference,
|
30
|
-
currentSource,
|
31
|
-
gaps = []
|
21
|
+
isReference = false,
|
22
|
+
currentSource = '',
|
23
|
+
gaps = [],
|
24
|
+
anchors = [],
|
25
|
+
corrections = []
|
32
26
|
}: UseWordClickProps) {
|
33
27
|
const handleWordClick = useCallback((
|
34
28
|
word: string,
|
35
29
|
wordId: string,
|
36
30
|
anchor?: AnchorSequence,
|
37
|
-
gap?: GapSequence
|
38
|
-
debugInfo?: WordDebugInfo
|
31
|
+
gap?: GapSequence
|
39
32
|
) => {
|
40
|
-
|
41
|
-
|
42
|
-
|
33
|
+
// Check if word belongs to anchor
|
34
|
+
const belongsToAnchor = anchor && (
|
35
|
+
isReference
|
36
|
+
? anchor.reference_word_ids[currentSource]?.includes(wordId)
|
37
|
+
: anchor.transcribed_word_ids.includes(wordId)
|
38
|
+
)
|
39
|
+
|
40
|
+
// Find matching gap if not provided
|
41
|
+
const matchingGap = gap || gaps.find(g =>
|
42
|
+
g.transcribed_word_ids.includes(wordId) ||
|
43
|
+
Object.values(g.reference_word_ids).some(ids => ids.includes(wordId))
|
44
|
+
)
|
45
|
+
|
46
|
+
// Check if word belongs to gap - include both original and corrected words
|
47
|
+
const belongsToGap = matchingGap && (
|
48
|
+
isReference
|
49
|
+
? matchingGap.reference_word_ids[currentSource]?.includes(wordId)
|
50
|
+
: (matchingGap.transcribed_word_ids.includes(wordId) ||
|
51
|
+
corrections.some(c =>
|
52
|
+
c.corrected_word_id === wordId ||
|
53
|
+
c.word_id === wordId
|
54
|
+
))
|
55
|
+
)
|
56
|
+
|
57
|
+
// Debug info
|
58
|
+
console.log('Word Click Debug:', {
|
59
|
+
clickInfo: {
|
60
|
+
word,
|
43
61
|
wordId,
|
44
62
|
isReference,
|
45
63
|
currentSource,
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
? gap.reference_words[currentSource!]?.includes(wordId)
|
65
|
-
: gap.word_ids.includes(wordId)
|
66
|
-
},
|
67
|
-
belongsToAnchor: anchor && (
|
68
|
-
isReference
|
69
|
-
? anchor.reference_word_ids[currentSource!]?.includes(wordId)
|
70
|
-
: anchor.word_ids.includes(wordId)
|
71
|
-
),
|
72
|
-
belongsToGap: gap && (
|
73
|
-
isReference
|
74
|
-
? gap.corrections.some(c => c.word_id === wordId)
|
75
|
-
: gap.word_ids.includes(wordId)
|
76
|
-
),
|
77
|
-
wordIndexInGap: gap && gap.words.indexOf(word),
|
78
|
-
hasMatchingCorrection: gap && gap.corrections.some(c => c.word_id === wordId)
|
64
|
+
mode
|
65
|
+
},
|
66
|
+
anchorInfo: anchor && {
|
67
|
+
id: anchor.id,
|
68
|
+
transcribedWordIds: anchor.transcribed_word_ids,
|
69
|
+
referenceWordIds: anchor.reference_word_ids,
|
70
|
+
belongsToAnchor
|
71
|
+
},
|
72
|
+
gapInfo: matchingGap && {
|
73
|
+
id: matchingGap.id,
|
74
|
+
transcribedWordIds: matchingGap.transcribed_word_ids,
|
75
|
+
referenceWordIds: matchingGap.reference_word_ids,
|
76
|
+
belongsToGap,
|
77
|
+
relatedCorrections: corrections.filter(c =>
|
78
|
+
matchingGap.transcribed_word_ids.includes(c.word_id) ||
|
79
|
+
c.corrected_word_id === wordId ||
|
80
|
+
c.word_id === wordId
|
81
|
+
)
|
79
82
|
}
|
80
|
-
}
|
83
|
+
})
|
81
84
|
|
82
85
|
// For reference view clicks, find the corresponding gap
|
83
86
|
if (isReference && currentSource) {
|
84
|
-
// Extract position from wordId (e.g., "genius-word-3" -> 3)
|
85
|
-
const position = parseInt(wordId.split('-').pop() || '', 10);
|
86
|
-
|
87
|
-
// Find gap that has a correction matching this reference position
|
88
87
|
const matchingGap = gaps?.find(g =>
|
89
|
-
g.
|
90
|
-
|
91
|
-
return typeof refPosition === 'number' && refPosition === position;
|
92
|
-
})
|
93
|
-
);
|
88
|
+
g.reference_word_ids[currentSource]?.includes(wordId)
|
89
|
+
)
|
94
90
|
|
95
91
|
if (matchingGap) {
|
96
92
|
console.log('Found matching gap for reference click:', {
|
97
|
-
|
93
|
+
wordId,
|
98
94
|
gap: matchingGap
|
99
|
-
})
|
100
|
-
gap = matchingGap
|
95
|
+
})
|
96
|
+
gap = matchingGap
|
101
97
|
}
|
102
98
|
}
|
103
99
|
|
104
|
-
const belongsToAnchor = anchor && (
|
105
|
-
isReference
|
106
|
-
? anchor.reference_word_ids[currentSource!]?.includes(wordId)
|
107
|
-
: anchor.word_ids.includes(wordId)
|
108
|
-
)
|
109
|
-
|
110
|
-
const belongsToGap = gap && (
|
111
|
-
isReference
|
112
|
-
? gap.corrections.some(c => {
|
113
|
-
const refPosition = c.reference_positions?.[currentSource!];
|
114
|
-
const clickedPosition = parseInt(wordId.split('-').pop() || '', 10);
|
115
|
-
return typeof refPosition === 'number' && refPosition === clickedPosition;
|
116
|
-
})
|
117
|
-
: gap.word_ids.includes(wordId)
|
118
|
-
)
|
119
|
-
|
120
100
|
if (mode === 'highlight' || mode === 'edit') {
|
121
101
|
if (belongsToAnchor && anchor) {
|
122
102
|
onWordClick?.({
|
@@ -126,32 +106,22 @@ export function useWordClick({
|
|
126
106
|
gap: undefined
|
127
107
|
})
|
128
108
|
} else if (belongsToGap && gap) {
|
129
|
-
// Create highlight info that includes both transcription and reference IDs
|
130
|
-
const referenceWords: Record<string, string[]> = {};
|
131
|
-
|
132
|
-
// For each correction in the gap, add its reference positions
|
133
|
-
gap.corrections.forEach(correction => {
|
134
|
-
Object.entries(correction.reference_positions || {}).forEach(([source, position]) => {
|
135
|
-
if (typeof position === 'number') {
|
136
|
-
const refId = `${source}-word-${position}`;
|
137
|
-
if (!referenceWords[source]) {
|
138
|
-
referenceWords[source] = [];
|
139
|
-
}
|
140
|
-
if (!referenceWords[source].includes(refId)) {
|
141
|
-
referenceWords[source].push(refId);
|
142
|
-
}
|
143
|
-
}
|
144
|
-
});
|
145
|
-
});
|
146
|
-
|
147
109
|
onWordClick?.({
|
148
110
|
word_id: wordId,
|
149
111
|
type: 'gap',
|
150
112
|
anchor: undefined,
|
151
|
-
gap
|
152
|
-
|
153
|
-
|
154
|
-
|
113
|
+
gap
|
114
|
+
})
|
115
|
+
} else if (corrections.some(c =>
|
116
|
+
(c.corrected_word_id === wordId || c.word_id === wordId) &&
|
117
|
+
gap?.transcribed_word_ids.includes(c.word_id)
|
118
|
+
)) {
|
119
|
+
// If the word is part of a correction, mark it as a gap
|
120
|
+
onWordClick?.({
|
121
|
+
word_id: wordId,
|
122
|
+
type: 'gap',
|
123
|
+
anchor: undefined,
|
124
|
+
gap
|
155
125
|
})
|
156
126
|
} else {
|
157
127
|
onWordClick?.({
|
@@ -168,7 +138,8 @@ export function useWordClick({
|
|
168
138
|
data: {
|
169
139
|
...anchor,
|
170
140
|
wordId,
|
171
|
-
word
|
141
|
+
word,
|
142
|
+
anchor_sequences: anchors
|
172
143
|
}
|
173
144
|
})
|
174
145
|
} else if (belongsToGap && gap) {
|
@@ -177,33 +148,32 @@ export function useWordClick({
|
|
177
148
|
data: {
|
178
149
|
...gap,
|
179
150
|
wordId,
|
180
|
-
word
|
151
|
+
word,
|
152
|
+
anchor_sequences: anchors
|
181
153
|
}
|
182
154
|
})
|
183
155
|
} else if (!isReference) {
|
184
156
|
// Create synthetic gap for non-sequence words (transcription view only)
|
185
157
|
const syntheticGap: GapSequence = {
|
186
158
|
id: `synthetic-${wordId}`,
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
preceding_anchor: null,
|
193
|
-
following_anchor: null,
|
194
|
-
reference_words: {}
|
159
|
+
transcribed_word_ids: [wordId],
|
160
|
+
transcription_position: -1,
|
161
|
+
preceding_anchor_id: null,
|
162
|
+
following_anchor_id: null,
|
163
|
+
reference_word_ids: {}
|
195
164
|
}
|
196
165
|
onElementClick({
|
197
166
|
type: 'gap',
|
198
167
|
data: {
|
199
168
|
...syntheticGap,
|
200
169
|
wordId,
|
201
|
-
word
|
170
|
+
word,
|
171
|
+
anchor_sequences: anchors
|
202
172
|
}
|
203
173
|
})
|
204
174
|
}
|
205
175
|
}
|
206
|
-
}, [mode, onWordClick, onElementClick, isReference, currentSource, gaps])
|
176
|
+
}, [mode, onWordClick, onElementClick, isReference, currentSource, gaps, anchors, corrections])
|
207
177
|
|
208
178
|
return { handleWordClick }
|
209
179
|
}
|
@@ -1,8 +1,8 @@
|
|
1
|
-
import { AnchorSequence, GapSequence, HighlightInfo, InteractionMode,
|
1
|
+
import { AnchorSequence, GapSequence, HighlightInfo, InteractionMode, CorrectionData, LyricsSegment, ReferenceSource, WordCorrection } from '../../types'
|
2
2
|
import { ModalContent } from '../LyricsAnalyzer'
|
3
3
|
|
4
4
|
// Add FlashType definition directly in shared types
|
5
|
-
export type FlashType = 'anchor' | 'corrected' | 'uncorrected' | 'word' | null
|
5
|
+
export type FlashType = 'anchor' | 'corrected' | 'uncorrected' | 'word' | 'handler' | null
|
6
6
|
|
7
7
|
// Common word click handling
|
8
8
|
export interface WordClickInfo {
|
@@ -66,10 +66,17 @@ export interface TextSegmentProps extends BaseViewProps {
|
|
66
66
|
}
|
67
67
|
|
68
68
|
// View-specific props
|
69
|
-
export interface TranscriptionViewProps
|
70
|
-
data:
|
69
|
+
export interface TranscriptionViewProps {
|
70
|
+
data: CorrectionData
|
71
|
+
onElementClick: (content: ModalContent) => void
|
72
|
+
onWordClick?: (info: WordClickInfo) => void
|
73
|
+
flashingType: FlashType
|
74
|
+
highlightInfo: HighlightInfo | null
|
75
|
+
mode: InteractionMode
|
71
76
|
onPlaySegment?: (startTime: number) => void
|
72
77
|
currentTime?: number
|
78
|
+
anchors?: AnchorSequence[]
|
79
|
+
flashingHandler?: string | null
|
73
80
|
}
|
74
81
|
|
75
82
|
// Add LinePosition type here since it's used in multiple places
|
@@ -81,12 +88,13 @@ export interface LinePosition {
|
|
81
88
|
|
82
89
|
// Reference-specific props
|
83
90
|
export interface ReferenceViewProps extends BaseViewProps {
|
84
|
-
|
85
|
-
anchors:
|
86
|
-
gaps:
|
91
|
+
referenceSources: Record<string, ReferenceSource>
|
92
|
+
anchors: CorrectionData['anchor_sequences']
|
93
|
+
gaps: CorrectionData['gap_sequences']
|
87
94
|
currentSource: string
|
88
95
|
onSourceChange: (source: string) => void
|
89
96
|
corrected_segments: LyricsSegment[]
|
97
|
+
corrections: WordCorrection[]
|
90
98
|
}
|
91
99
|
|
92
100
|
// Update HighlightedTextProps to include linePositions
|
@@ -0,0 +1,67 @@
|
|
1
|
+
// Add a global ref for the modal handler
|
2
|
+
let currentModalHandler: ((e: KeyboardEvent) => void) | undefined
|
3
|
+
let isModalOpen = false
|
4
|
+
|
5
|
+
type KeyboardState = {
|
6
|
+
setIsShiftPressed: (value: boolean) => void
|
7
|
+
setIsCtrlPressed: (value: boolean) => void
|
8
|
+
modalHandler?: {
|
9
|
+
isOpen: boolean
|
10
|
+
onSpacebar?: (e: KeyboardEvent) => void
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
// Add functions to update the modal handler state
|
15
|
+
export const setModalHandler = (handler: ((e: KeyboardEvent) => void) | undefined, open: boolean) => {
|
16
|
+
currentModalHandler = handler
|
17
|
+
isModalOpen = open
|
18
|
+
}
|
19
|
+
|
20
|
+
export const setupKeyboardHandlers = (state: KeyboardState) => {
|
21
|
+
const handlerId = Math.random().toString(36).substr(2, 9)
|
22
|
+
console.log(`Setting up keyboard handlers [${handlerId}]`)
|
23
|
+
|
24
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
25
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
26
|
+
console.log(`[${handlerId}] Ignoring keydown in input/textarea`)
|
27
|
+
return
|
28
|
+
}
|
29
|
+
|
30
|
+
if (e.key === 'Shift') {
|
31
|
+
state.setIsShiftPressed(true)
|
32
|
+
document.body.style.userSelect = 'none'
|
33
|
+
} else if (e.key === 'Meta') {
|
34
|
+
state.setIsCtrlPressed(true)
|
35
|
+
} else if (e.key === ' ' || e.code === 'Space') {
|
36
|
+
console.log(`[${handlerId}] Spacebar pressed:`, {
|
37
|
+
modalOpen: isModalOpen,
|
38
|
+
hasModalHandler: !!currentModalHandler,
|
39
|
+
hasGlobalToggle: !!window.toggleAudioPlayback
|
40
|
+
})
|
41
|
+
|
42
|
+
e.preventDefault()
|
43
|
+
|
44
|
+
// If modal is open and has a handler, use that
|
45
|
+
if (isModalOpen && currentModalHandler) {
|
46
|
+
console.log(`[${handlerId}] Using modal spacebar handler`)
|
47
|
+
currentModalHandler(e)
|
48
|
+
}
|
49
|
+
// Otherwise use global audio control
|
50
|
+
else if (window.toggleAudioPlayback && !isModalOpen) {
|
51
|
+
console.log(`[${handlerId}] Using global audio toggle`)
|
52
|
+
window.toggleAudioPlayback()
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
const handleKeyUp = (e: KeyboardEvent) => {
|
58
|
+
if (e.key === 'Shift') {
|
59
|
+
state.setIsShiftPressed(false)
|
60
|
+
document.body.style.userSelect = ''
|
61
|
+
} else if (e.key === 'Meta') {
|
62
|
+
state.setIsCtrlPressed(false)
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
return { handleKeyDown, handleKeyUp }
|
67
|
+
}
|
@@ -0,0 +1,78 @@
|
|
1
|
+
import { CorrectionData, LyricsSegment } from '../../../types'
|
2
|
+
|
3
|
+
// Change the key generation to use a hash of the first segment's text instead
|
4
|
+
export const generateStorageKey = (data: CorrectionData): string => {
|
5
|
+
const text = data.original_segments[0]?.text || ''
|
6
|
+
let hash = 0
|
7
|
+
for (let i = 0; i < text.length; i++) {
|
8
|
+
const char = text.charCodeAt(i)
|
9
|
+
hash = ((hash << 5) - hash) + char
|
10
|
+
hash = hash & hash // Convert to 32-bit integer
|
11
|
+
}
|
12
|
+
return `song_${hash}`
|
13
|
+
}
|
14
|
+
|
15
|
+
const stripIds = (obj: CorrectionData): LyricsSegment[] => {
|
16
|
+
const clone = JSON.parse(JSON.stringify(obj))
|
17
|
+
return clone.corrected_segments.map((segment: LyricsSegment) => {
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
19
|
+
const { id: _id, ...strippedSegment } = segment
|
20
|
+
return {
|
21
|
+
...strippedSegment,
|
22
|
+
words: segment.words.map(word => {
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
24
|
+
const { id: _wordId, ...strippedWord } = word
|
25
|
+
return strippedWord
|
26
|
+
})
|
27
|
+
}
|
28
|
+
})
|
29
|
+
}
|
30
|
+
|
31
|
+
export const loadSavedData = (initialData: CorrectionData): CorrectionData | null => {
|
32
|
+
const storageKey = generateStorageKey(initialData)
|
33
|
+
const savedDataStr = localStorage.getItem('lyrics_analyzer_data')
|
34
|
+
const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {}
|
35
|
+
|
36
|
+
if (savedDataObj[storageKey]) {
|
37
|
+
try {
|
38
|
+
const parsed = savedDataObj[storageKey]
|
39
|
+
// Compare first segment text instead of transcribed_text
|
40
|
+
if (parsed.original_segments[0]?.text === initialData.original_segments[0]?.text) {
|
41
|
+
const strippedSaved = stripIds(parsed)
|
42
|
+
const strippedInitial = stripIds(initialData)
|
43
|
+
const hasChanges = JSON.stringify(strippedSaved) !== JSON.stringify(strippedInitial)
|
44
|
+
|
45
|
+
if (hasChanges) {
|
46
|
+
return parsed
|
47
|
+
} else {
|
48
|
+
// Clean up storage if no changes
|
49
|
+
delete savedDataObj[storageKey]
|
50
|
+
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj))
|
51
|
+
}
|
52
|
+
}
|
53
|
+
} catch (error) {
|
54
|
+
console.error('Failed to parse saved data:', error)
|
55
|
+
delete savedDataObj[storageKey]
|
56
|
+
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj))
|
57
|
+
}
|
58
|
+
}
|
59
|
+
return null
|
60
|
+
}
|
61
|
+
|
62
|
+
export const saveData = (data: CorrectionData, initialData: CorrectionData): void => {
|
63
|
+
const storageKey = generateStorageKey(initialData)
|
64
|
+
const savedDataStr = localStorage.getItem('lyrics_analyzer_data')
|
65
|
+
const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {}
|
66
|
+
|
67
|
+
savedDataObj[storageKey] = data
|
68
|
+
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj))
|
69
|
+
}
|
70
|
+
|
71
|
+
export const clearSavedData = (data: CorrectionData): void => {
|
72
|
+
const storageKey = generateStorageKey(data)
|
73
|
+
const savedDataStr = localStorage.getItem('lyrics_analyzer_data')
|
74
|
+
const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {}
|
75
|
+
|
76
|
+
delete savedDataObj[storageKey]
|
77
|
+
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj))
|
78
|
+
}
|
@@ -1,9 +1,9 @@
|
|
1
|
-
import {
|
1
|
+
import { AnchorSequence, LyricsSegment } from '../../../types'
|
2
2
|
import { LinePosition } from '../types'
|
3
3
|
|
4
4
|
export function calculateReferenceLinePositions(
|
5
5
|
corrected_segments: LyricsSegment[],
|
6
|
-
anchors:
|
6
|
+
anchors: AnchorSequence[],
|
7
7
|
currentSource: string
|
8
8
|
): { linePositions: LinePosition[] } {
|
9
9
|
const linePositions: LinePosition[] = []
|
@@ -11,9 +11,7 @@ export function calculateReferenceLinePositions(
|
|
11
11
|
|
12
12
|
// First, find all anchor sequences that cover entire lines
|
13
13
|
const fullLineAnchors = anchors?.map(anchor => {
|
14
|
-
//
|
15
|
-
if (!anchor?.reference_word_ids?.[currentSource]) return null
|
16
|
-
|
14
|
+
// Check if we have reference word IDs for this source
|
17
15
|
const referenceWordIds = anchor.reference_word_ids[currentSource]
|
18
16
|
if (!referenceWordIds?.length) return null
|
19
17
|
|
@@ -23,8 +21,10 @@ export function calculateReferenceLinePositions(
|
|
23
21
|
const wordIds = segment.words.map(w => w.id)
|
24
22
|
if (!wordIds.length) return false
|
25
23
|
|
26
|
-
// Check if all word IDs in this segment are part of the anchor
|
27
|
-
return wordIds.every(id =>
|
24
|
+
// Check if all word IDs in this segment are part of the anchor's transcribed word IDs
|
25
|
+
return wordIds.every(id =>
|
26
|
+
anchor.transcribed_word_ids.includes(id)
|
27
|
+
)
|
28
28
|
})
|
29
29
|
}
|
30
30
|
})?.filter((a): a is NonNullable<typeof a> => a !== null) ?? []
|
@@ -0,0 +1,121 @@
|
|
1
|
+
import { nanoid } from 'nanoid'
|
2
|
+
import { CorrectionData, LyricsSegment } from '../../../types'
|
3
|
+
|
4
|
+
export const addSegmentBefore = (
|
5
|
+
data: CorrectionData,
|
6
|
+
beforeIndex: number
|
7
|
+
): CorrectionData => {
|
8
|
+
const newData = { ...data }
|
9
|
+
const beforeSegment = newData.corrected_segments[beforeIndex]
|
10
|
+
|
11
|
+
// Create new segment starting 1 second before the target segment
|
12
|
+
// Use 0 as default if start_time is null
|
13
|
+
const newStartTime = Math.max(0, (beforeSegment.start_time ?? 1) - 1)
|
14
|
+
const newEndTime = newStartTime + 1
|
15
|
+
|
16
|
+
const newSegment: LyricsSegment = {
|
17
|
+
id: nanoid(),
|
18
|
+
text: "REPLACE",
|
19
|
+
start_time: newStartTime,
|
20
|
+
end_time: newEndTime,
|
21
|
+
words: [{
|
22
|
+
id: nanoid(),
|
23
|
+
text: "REPLACE",
|
24
|
+
start_time: newStartTime,
|
25
|
+
end_time: newEndTime,
|
26
|
+
confidence: 1.0
|
27
|
+
}]
|
28
|
+
}
|
29
|
+
|
30
|
+
// Insert the new segment before the current one
|
31
|
+
newData.corrected_segments.splice(beforeIndex, 0, newSegment)
|
32
|
+
|
33
|
+
return newData
|
34
|
+
}
|
35
|
+
|
36
|
+
export const splitSegment = (
|
37
|
+
data: CorrectionData,
|
38
|
+
segmentIndex: number,
|
39
|
+
afterWordIndex: number
|
40
|
+
): CorrectionData | null => {
|
41
|
+
const newData = { ...data }
|
42
|
+
const segment = newData.corrected_segments[segmentIndex]
|
43
|
+
|
44
|
+
// Split the words array
|
45
|
+
const firstHalfWords = segment.words.slice(0, afterWordIndex + 1)
|
46
|
+
const secondHalfWords = segment.words.slice(afterWordIndex + 1)
|
47
|
+
|
48
|
+
if (secondHalfWords.length === 0) return null // Nothing to split
|
49
|
+
|
50
|
+
const lastFirstWord = firstHalfWords[firstHalfWords.length - 1]
|
51
|
+
const firstSecondWord = secondHalfWords[0]
|
52
|
+
const lastSecondWord = secondHalfWords[secondHalfWords.length - 1]
|
53
|
+
|
54
|
+
// Create two segments from the split
|
55
|
+
const firstSegment: LyricsSegment = {
|
56
|
+
...segment,
|
57
|
+
words: firstHalfWords,
|
58
|
+
text: firstHalfWords.map(w => w.text).join(' '),
|
59
|
+
end_time: lastFirstWord.end_time ?? null
|
60
|
+
}
|
61
|
+
|
62
|
+
const secondSegment: LyricsSegment = {
|
63
|
+
id: nanoid(),
|
64
|
+
words: secondHalfWords,
|
65
|
+
text: secondHalfWords.map(w => w.text).join(' '),
|
66
|
+
start_time: firstSecondWord.start_time ?? null,
|
67
|
+
end_time: lastSecondWord.end_time ?? null
|
68
|
+
}
|
69
|
+
|
70
|
+
// Replace the original segment with the two new segments
|
71
|
+
newData.corrected_segments.splice(segmentIndex, 1, firstSegment, secondSegment)
|
72
|
+
|
73
|
+
return newData
|
74
|
+
}
|
75
|
+
|
76
|
+
export const deleteSegment = (
|
77
|
+
data: CorrectionData,
|
78
|
+
segmentIndex: number
|
79
|
+
): CorrectionData => {
|
80
|
+
const newData = { ...data }
|
81
|
+
const deletedSegment = newData.corrected_segments[segmentIndex]
|
82
|
+
|
83
|
+
// Remove segment
|
84
|
+
newData.corrected_segments = newData.corrected_segments.filter((_, index) => index !== segmentIndex)
|
85
|
+
|
86
|
+
// Update anchor sequences to remove references to deleted words
|
87
|
+
newData.anchor_sequences = newData.anchor_sequences.map(anchor => ({
|
88
|
+
...anchor,
|
89
|
+
transcribed_word_ids: anchor.transcribed_word_ids.filter(wordId =>
|
90
|
+
!deletedSegment.words.some(deletedWord => deletedWord.id === wordId)
|
91
|
+
)
|
92
|
+
}))
|
93
|
+
|
94
|
+
// Update gap sequences to remove references to deleted words
|
95
|
+
newData.gap_sequences = newData.gap_sequences.map(gap => ({
|
96
|
+
...gap,
|
97
|
+
transcribed_word_ids: gap.transcribed_word_ids.filter(wordId =>
|
98
|
+
!deletedSegment.words.some(deletedWord => deletedWord.id === wordId)
|
99
|
+
)
|
100
|
+
}))
|
101
|
+
|
102
|
+
return newData
|
103
|
+
}
|
104
|
+
|
105
|
+
export const updateSegment = (
|
106
|
+
data: CorrectionData,
|
107
|
+
segmentIndex: number,
|
108
|
+
updatedSegment: LyricsSegment
|
109
|
+
): CorrectionData => {
|
110
|
+
const newData = { ...data }
|
111
|
+
|
112
|
+
// Ensure new words have IDs
|
113
|
+
updatedSegment.words = updatedSegment.words.map(word => ({
|
114
|
+
...word,
|
115
|
+
id: word.id || nanoid()
|
116
|
+
}))
|
117
|
+
|
118
|
+
newData.corrected_segments[segmentIndex] = updatedSegment
|
119
|
+
|
120
|
+
return newData
|
121
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { Word, LyricsSegment } from '../../../types'
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Find a Word object by its ID within an array of segments
|
5
|
+
*/
|
6
|
+
export function findWordById(segments: LyricsSegment[], wordId: string): Word | undefined {
|
7
|
+
for (const segment of segments) {
|
8
|
+
const word = segment.words.find(w => w.id === wordId)
|
9
|
+
if (word) return word
|
10
|
+
}
|
11
|
+
return undefined
|
12
|
+
}
|
13
|
+
|
14
|
+
/**
|
15
|
+
* Convert an array of word IDs to their corresponding Word objects
|
16
|
+
* Filters out any IDs that don't match to valid words
|
17
|
+
*/
|
18
|
+
export function getWordsFromIds(segments: LyricsSegment[], wordIds: string[]): Word[] {
|
19
|
+
return wordIds
|
20
|
+
.map(id => findWordById(segments, id))
|
21
|
+
.filter((word): word is Word => word !== undefined)
|
22
|
+
}
|