lyrics-transcriber 0.34.2__py3-none-any.whl → 0.35.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 +10 -1
- lyrics_transcriber/correction/corrector.py +4 -3
- lyrics_transcriber/frontend/dist/assets/index-CQCER5Fo.js +181 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/App.tsx +6 -2
- lyrics_transcriber/frontend/src/api.ts +9 -0
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +155 -0
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +1 -1
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +23 -191
- lyrics_transcriber/frontend/src/components/EditModal.tsx +407 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +255 -221
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +39 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +35 -264
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +232 -0
- lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +315 -0
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +116 -138
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +116 -0
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +243 -0
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +28 -0
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +52 -0
- lyrics_transcriber/frontend/src/components/{constants.ts → shared/constants.ts} +1 -0
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +137 -0
- lyrics_transcriber/frontend/src/components/{styles.ts → shared/styles.ts} +1 -1
- lyrics_transcriber/frontend/src/components/shared/types.ts +99 -0
- lyrics_transcriber/frontend/src/components/shared/utils/newlineCalculator.ts +37 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +76 -0
- lyrics_transcriber/frontend/src/types.ts +2 -43
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/lyrics/spotify.py +11 -0
- lyrics_transcriber/output/generator.py +28 -11
- lyrics_transcriber/review/server.py +38 -12
- {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/METADATA +1 -1
- {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/RECORD +37 -24
- lyrics_transcriber/frontend/dist/assets/index-DqFgiUni.js +0 -245
- lyrics_transcriber/frontend/src/components/DebugPanel.tsx +0 -311
- {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/WHEEL +0 -0
- {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/entry_points.txt +0 -0
@@ -1,21 +1,24 @@
|
|
1
|
+
import {
|
2
|
+
CorrectionData,
|
3
|
+
HighlightInfo,
|
4
|
+
InteractionMode,
|
5
|
+
LyricsData,
|
6
|
+
LyricsSegment
|
7
|
+
} from '../types'
|
1
8
|
import LockIcon from '@mui/icons-material/Lock'
|
2
9
|
import UploadFileIcon from '@mui/icons-material/UploadFile'
|
3
10
|
import { Box, Button, Grid, Typography, useMediaQuery, useTheme } from '@mui/material'
|
4
|
-
import { useCallback, useState } from 'react'
|
11
|
+
import { useCallback, useState, useEffect } from 'react'
|
5
12
|
import { ApiClient } from '../api'
|
6
|
-
import { CorrectionData, LyricsData, HighlightInfo, AnchorMatchInfo, GapSequence, AnchorSequence, LyricsSegment, WordCorrection } from '../types'
|
7
13
|
import CorrectionMetrics from './CorrectionMetrics'
|
8
14
|
import DetailsModal from './DetailsModal'
|
15
|
+
import ModeSelector from './ModeSelector'
|
9
16
|
import ReferenceView from './ReferenceView'
|
10
17
|
import TranscriptionView from './TranscriptionView'
|
11
|
-
import
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
type: 'anchor' | 'gap' | 'other'
|
16
|
-
anchor?: AnchorSequence
|
17
|
-
gap?: GapSequence
|
18
|
-
}
|
18
|
+
import { WordClickInfo, FlashType } from './shared/types'
|
19
|
+
import EditModal from './EditModal'
|
20
|
+
import ReviewChangesModal from './ReviewChangesModal'
|
21
|
+
import AudioPlayer from './AudioPlayer'
|
19
22
|
|
20
23
|
interface LyricsAnalyzerProps {
|
21
24
|
data: CorrectionData
|
@@ -29,6 +32,7 @@ export type ModalContent = {
|
|
29
32
|
type: 'anchor'
|
30
33
|
data: LyricsData['anchor_sequences'][0] & {
|
31
34
|
position: number
|
35
|
+
word?: string
|
32
36
|
}
|
33
37
|
} | {
|
34
38
|
type: 'gap'
|
@@ -38,8 +42,6 @@ export type ModalContent = {
|
|
38
42
|
}
|
39
43
|
}
|
40
44
|
|
41
|
-
export type FlashType = 'anchor' | 'corrected' | 'uncorrected' | 'word' | null
|
42
|
-
|
43
45
|
function normalizeDataForSubmission(data: CorrectionData): CorrectionData {
|
44
46
|
// Create a deep clone to avoid modifying the original
|
45
47
|
const normalized = JSON.parse(JSON.stringify(data))
|
@@ -71,13 +73,96 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
71
73
|
const [flashingType, setFlashingType] = useState<FlashType>(null)
|
72
74
|
const [highlightInfo, setHighlightInfo] = useState<HighlightInfo | null>(null)
|
73
75
|
const [currentSource, setCurrentSource] = useState<'genius' | 'spotify'>('genius')
|
74
|
-
const [anchorMatchInfo, setAnchorMatchInfo] = useState<AnchorMatchInfo[]>([])
|
75
|
-
const [manualCorrections, setManualCorrections] = useState<Map<number, string[]>>(new Map())
|
76
76
|
const [isReviewComplete, setIsReviewComplete] = useState(false)
|
77
77
|
const [data, setData] = useState(initialData)
|
78
|
+
// Create deep copy of initial data for comparison later
|
79
|
+
const [originalData] = useState(() => JSON.parse(JSON.stringify(initialData)))
|
80
|
+
const [interactionMode, setInteractionMode] = useState<InteractionMode>('details')
|
81
|
+
const [isShiftPressed, setIsShiftPressed] = useState(false)
|
82
|
+
const [isCtrlPressed, setIsCtrlPressed] = useState(false)
|
83
|
+
const [editModalSegment, setEditModalSegment] = useState<{
|
84
|
+
segment: LyricsSegment
|
85
|
+
index: number
|
86
|
+
originalSegment: LyricsSegment
|
87
|
+
} | null>(null)
|
88
|
+
const [isReviewModalOpen, setIsReviewModalOpen] = useState(false)
|
89
|
+
const [currentAudioTime, setCurrentAudioTime] = useState(0)
|
78
90
|
const theme = useTheme()
|
79
91
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
80
92
|
|
93
|
+
// Add local storage handling
|
94
|
+
useEffect(() => {
|
95
|
+
// On mount, try to load saved data
|
96
|
+
const savedData = localStorage.getItem('lyrics_analyzer_data')
|
97
|
+
if (savedData) {
|
98
|
+
try {
|
99
|
+
const parsed = JSON.parse(savedData)
|
100
|
+
// Only restore if it's the same song (matching transcribed text)
|
101
|
+
if (parsed.transcribed_text === initialData.transcribed_text) {
|
102
|
+
console.log('Restored saved progress from local storage')
|
103
|
+
setData(parsed)
|
104
|
+
} else {
|
105
|
+
// Clear old data if it's a different song
|
106
|
+
localStorage.removeItem('lyrics_analyzer_data')
|
107
|
+
}
|
108
|
+
} catch (error) {
|
109
|
+
console.error('Failed to parse saved data:', error)
|
110
|
+
localStorage.removeItem('lyrics_analyzer_data')
|
111
|
+
}
|
112
|
+
}
|
113
|
+
}, [initialData.transcribed_text])
|
114
|
+
|
115
|
+
// Save to local storage whenever data changes
|
116
|
+
useEffect(() => {
|
117
|
+
if (!isReadOnly) {
|
118
|
+
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(data))
|
119
|
+
}
|
120
|
+
}, [data, isReadOnly])
|
121
|
+
|
122
|
+
// Add keyboard event handlers
|
123
|
+
useEffect(() => {
|
124
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
125
|
+
// Ignore if user is typing in an input or textarea
|
126
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
127
|
+
return
|
128
|
+
}
|
129
|
+
|
130
|
+
if (e.key === 'Shift') {
|
131
|
+
setIsShiftPressed(true)
|
132
|
+
document.body.style.userSelect = 'none'
|
133
|
+
} else if (e.key === 'Meta') {
|
134
|
+
setIsCtrlPressed(true)
|
135
|
+
} else if (e.key === ' ' || e.code === 'Space') {
|
136
|
+
e.preventDefault() // Prevent page scroll
|
137
|
+
if ((window as any).toggleAudioPlayback) {
|
138
|
+
(window as any).toggleAudioPlayback()
|
139
|
+
}
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
const handleKeyUp = (e: KeyboardEvent) => {
|
144
|
+
if (e.key === 'Shift') {
|
145
|
+
setIsShiftPressed(false)
|
146
|
+
document.body.style.userSelect = ''
|
147
|
+
} else if (e.key === 'Meta') {
|
148
|
+
setIsCtrlPressed(false)
|
149
|
+
}
|
150
|
+
}
|
151
|
+
|
152
|
+
window.addEventListener('keydown', handleKeyDown)
|
153
|
+
window.addEventListener('keyup', handleKeyUp)
|
154
|
+
return () => {
|
155
|
+
window.removeEventListener('keydown', handleKeyDown)
|
156
|
+
window.removeEventListener('keyup', handleKeyUp)
|
157
|
+
document.body.style.userSelect = ''
|
158
|
+
}
|
159
|
+
}, [])
|
160
|
+
|
161
|
+
// Calculate effective mode based on modifier key states
|
162
|
+
const effectiveMode = isShiftPressed ? 'highlight' :
|
163
|
+
isCtrlPressed ? 'edit' :
|
164
|
+
interactionMode
|
165
|
+
|
81
166
|
const handleFlash = useCallback((type: FlashType, info?: HighlightInfo) => {
|
82
167
|
setFlashingType(null)
|
83
168
|
setHighlightInfo(null)
|
@@ -97,229 +182,141 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
97
182
|
}, [])
|
98
183
|
|
99
184
|
const handleWordClick = useCallback((info: WordClickInfo) => {
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
type: 'gap' as const,
|
107
|
-
data: {
|
108
|
-
...info.gap,
|
109
|
-
position: info.gap.transcription_position + (info.wordIndex - info.gap.transcription_position),
|
110
|
-
word: info.gap.words[info.wordIndex - info.gap.transcription_position]
|
185
|
+
if (effectiveMode === 'edit') {
|
186
|
+
let currentPosition = 0
|
187
|
+
const segmentIndex = data.corrected_segments.findIndex(segment => {
|
188
|
+
if (info.wordIndex >= currentPosition &&
|
189
|
+
info.wordIndex < currentPosition + segment.words.length) {
|
190
|
+
return true
|
111
191
|
}
|
112
|
-
|
113
|
-
|
114
|
-
console.log('Set modal content:', JSON.stringify(modalData, null, 2))
|
115
|
-
}
|
116
|
-
|
117
|
-
console.groupEnd()
|
118
|
-
}, [])
|
119
|
-
|
120
|
-
const handleUpdateCorrection = useCallback((position: number, updatedWords: string[]) => {
|
121
|
-
console.group('handleUpdateCorrection Debug')
|
122
|
-
console.log('Position:', position)
|
123
|
-
console.log('Updated words:', updatedWords)
|
124
|
-
|
125
|
-
// Create a deep clone of the data
|
126
|
-
const newData = JSON.parse(JSON.stringify(data))
|
127
|
-
|
128
|
-
// Find the gap that contains this position
|
129
|
-
const gapIndex = newData.gap_sequences.findIndex(
|
130
|
-
(gap: GapSequence) =>
|
131
|
-
position >= gap.transcription_position &&
|
132
|
-
position < gap.transcription_position + gap.words.length
|
133
|
-
)
|
134
|
-
|
135
|
-
if (gapIndex !== -1) {
|
136
|
-
const originalGap = newData.gap_sequences[gapIndex]
|
137
|
-
const wordIndexInGap = position - originalGap.transcription_position
|
138
|
-
console.log('Found gap at index:', gapIndex, 'word index in gap:', wordIndexInGap)
|
139
|
-
|
140
|
-
// Update manual corrections
|
141
|
-
setManualCorrections(prev => {
|
142
|
-
const newCorrections = new Map(prev)
|
143
|
-
newCorrections.set(position, updatedWords)
|
144
|
-
return newCorrections
|
192
|
+
currentPosition += segment.words.length
|
193
|
+
return false
|
145
194
|
})
|
146
195
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
source: 'manual',
|
154
|
-
confidence: 1.0,
|
155
|
-
reason: 'Manual correction during review',
|
156
|
-
alternatives: {},
|
157
|
-
is_deletion: false,
|
158
|
-
length: updatedWords.length,
|
159
|
-
reference_positions: {}
|
196
|
+
if (segmentIndex !== -1) {
|
197
|
+
setEditModalSegment({
|
198
|
+
segment: data.corrected_segments[segmentIndex],
|
199
|
+
index: segmentIndex,
|
200
|
+
originalSegment: originalData.corrected_segments[segmentIndex]
|
201
|
+
})
|
160
202
|
}
|
161
|
-
|
162
|
-
//
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
203
|
+
} else {
|
204
|
+
// Existing word click handling for other modes...
|
205
|
+
if (info.type === 'anchor' && info.anchor) {
|
206
|
+
handleFlash('word', {
|
207
|
+
type: 'anchor',
|
208
|
+
transcriptionIndex: info.anchor.transcription_position,
|
209
|
+
transcriptionLength: info.anchor.length,
|
210
|
+
referenceIndices: info.anchor.reference_positions,
|
211
|
+
referenceLength: info.anchor.length
|
212
|
+
})
|
213
|
+
} else if (info.type === 'gap' && info.gap) {
|
214
|
+
handleFlash('word', {
|
215
|
+
type: 'gap',
|
216
|
+
transcriptionIndex: info.gap.transcription_position,
|
217
|
+
transcriptionLength: info.gap.length,
|
218
|
+
referenceIndices: {},
|
219
|
+
referenceLength: info.gap.length
|
220
|
+
})
|
175
221
|
}
|
222
|
+
}
|
223
|
+
}, [effectiveMode, data.corrected_segments, handleFlash, originalData.corrected_segments])
|
176
224
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
225
|
+
const handleUpdateSegment = useCallback((updatedSegment: LyricsSegment) => {
|
226
|
+
console.log('LyricsAnalyzer - handleUpdateSegment called:', {
|
227
|
+
editModalSegment,
|
228
|
+
updatedSegment,
|
229
|
+
currentSegmentsCount: data.corrected_segments.length
|
230
|
+
})
|
183
231
|
|
184
|
-
|
185
|
-
|
186
|
-
|
232
|
+
if (!editModalSegment) {
|
233
|
+
console.warn('LyricsAnalyzer - No editModalSegment found')
|
234
|
+
return
|
235
|
+
}
|
187
236
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
237
|
+
const newData = { ...data }
|
238
|
+
console.log('LyricsAnalyzer - Before update:', {
|
239
|
+
segmentIndex: editModalSegment.index,
|
240
|
+
oldText: newData.corrected_segments[editModalSegment.index].text,
|
241
|
+
newText: updatedSegment.text
|
242
|
+
})
|
194
243
|
|
195
|
-
|
196
|
-
console.error('Could not find timing word in segment')
|
197
|
-
console.groupEnd()
|
198
|
-
return
|
199
|
-
}
|
244
|
+
newData.corrected_segments[editModalSegment.index] = updatedSegment
|
200
245
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
...originalGap,
|
206
|
-
words: newWords,
|
207
|
-
text: newWords.join(' '),
|
208
|
-
corrections: originalGap.corrections
|
209
|
-
.filter((c: WordCorrection) => c.source !== 'manual')
|
210
|
-
.concat([newCorrection])
|
211
|
-
}
|
246
|
+
// Update corrected_text
|
247
|
+
newData.corrected_text = newData.corrected_segments
|
248
|
+
.map(segment => segment.text)
|
249
|
+
.join('\n')
|
212
250
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
text: updatedWords[0],
|
218
|
-
confidence: 1.0
|
219
|
-
}
|
251
|
+
console.log('LyricsAnalyzer - After update:', {
|
252
|
+
segmentsCount: newData.corrected_segments.length,
|
253
|
+
updatedText: newData.corrected_text
|
254
|
+
})
|
220
255
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
text: newSegmentWords.map(word => word.text).join(' ')
|
225
|
-
}
|
256
|
+
setData(newData)
|
257
|
+
setEditModalSegment(null) // Close the modal
|
258
|
+
}, [data, editModalSegment])
|
226
259
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
}
|
260
|
+
const handleDeleteSegment = useCallback((segmentIndex: number) => {
|
261
|
+
console.log('LyricsAnalyzer - handleDeleteSegment called:', {
|
262
|
+
segmentIndex,
|
263
|
+
currentSegmentsCount: data.corrected_segments.length
|
264
|
+
})
|
265
|
+
|
266
|
+
const newData = { ...data }
|
267
|
+
newData.corrected_segments = newData.corrected_segments.filter((_, index) => index !== segmentIndex)
|
232
268
|
|
233
|
-
// Update
|
269
|
+
// Update corrected_text
|
234
270
|
newData.corrected_text = newData.corrected_segments
|
235
|
-
.map(
|
271
|
+
.map(segment => segment.text)
|
236
272
|
.join('\n')
|
237
273
|
|
274
|
+
console.log('LyricsAnalyzer - After delete:', {
|
275
|
+
segmentsCount: newData.corrected_segments.length,
|
276
|
+
updatedText: newData.corrected_text
|
277
|
+
})
|
278
|
+
|
238
279
|
setData(newData)
|
239
|
-
console.groupEnd()
|
240
280
|
}, [data])
|
241
281
|
|
242
|
-
const handleFinishReview = useCallback(
|
282
|
+
const handleFinishReview = useCallback(() => {
|
283
|
+
setIsReviewModalOpen(true)
|
284
|
+
}, [])
|
285
|
+
|
286
|
+
const handleSubmitToServer = useCallback(async () => {
|
243
287
|
if (!apiClient) return
|
244
288
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
// Only proceed with data modifications if there were manual corrections
|
250
|
-
const updatedData = JSON.parse(JSON.stringify(data))
|
251
|
-
console.log('Deep cloned data:', JSON.stringify(updatedData, null, 2))
|
252
|
-
|
253
|
-
// Only update the specific gaps that were manually corrected
|
254
|
-
updatedData.gap_sequences = updatedData.gap_sequences.map((gap: GapSequence) => {
|
255
|
-
const manualUpdate = manualCorrections.get(gap.transcription_position)
|
256
|
-
if (manualUpdate) {
|
257
|
-
return {
|
258
|
-
...gap,
|
259
|
-
words: manualUpdate,
|
260
|
-
text: manualUpdate.join(' '),
|
261
|
-
corrections: [
|
262
|
-
...gap.corrections,
|
263
|
-
{
|
264
|
-
original_word: gap.text,
|
265
|
-
corrected_word: manualUpdate.join(' '),
|
266
|
-
segment_index: 0,
|
267
|
-
original_position: gap.transcription_position,
|
268
|
-
source: 'manual',
|
269
|
-
confidence: 1.0,
|
270
|
-
reason: 'Manual correction during review',
|
271
|
-
alternatives: {},
|
272
|
-
is_deletion: false,
|
273
|
-
length: manualUpdate.length,
|
274
|
-
reference_positions: {}
|
275
|
-
}
|
276
|
-
]
|
277
|
-
}
|
278
|
-
}
|
279
|
-
return gap
|
280
|
-
})
|
289
|
+
try {
|
290
|
+
console.log('Submitting changes to server')
|
291
|
+
const dataToSubmit = normalizeDataForSubmission(data)
|
292
|
+
await apiClient.submitCorrections(dataToSubmit)
|
281
293
|
|
282
|
-
|
283
|
-
|
284
|
-
const lines: string[] = updatedData.corrected_text.split('\n')
|
285
|
-
let currentPosition = 0
|
286
|
-
const updatedLines = lines.map((line: string) => {
|
287
|
-
const words = line.trim().split(/\s+/)
|
288
|
-
const lineLength = words.length
|
289
|
-
|
290
|
-
// Check if this line contains any corrections
|
291
|
-
let lineUpdated = false
|
292
|
-
for (const [position, updatedWords] of manualCorrections.entries()) {
|
293
|
-
if (position >= currentPosition && position < currentPosition + lineLength) {
|
294
|
-
const gapPosition = position - currentPosition
|
295
|
-
const gap = updatedData.gap_sequences.find((g: GapSequence) =>
|
296
|
-
g.transcription_position === position
|
297
|
-
)
|
298
|
-
if (gap) {
|
299
|
-
words.splice(gapPosition, gap.length, ...updatedWords)
|
300
|
-
lineUpdated = true
|
301
|
-
}
|
302
|
-
}
|
303
|
-
}
|
304
|
-
currentPosition += lineLength
|
305
|
-
return lineUpdated ? words.join(' ') : line
|
306
|
-
})
|
307
|
-
updatedData.corrected_text = updatedLines.join('\n')
|
308
|
-
}
|
294
|
+
setIsReviewComplete(true)
|
295
|
+
setIsReviewModalOpen(false)
|
309
296
|
|
310
|
-
|
311
|
-
|
312
|
-
}
|
313
|
-
console.
|
314
|
-
|
315
|
-
dataToSubmit = normalizeDataForSubmission(initialData)
|
297
|
+
// Close the browser tab
|
298
|
+
window.close()
|
299
|
+
} catch (error) {
|
300
|
+
console.error('Failed to submit corrections:', error)
|
301
|
+
alert('Failed to submit corrections. Please try again.')
|
316
302
|
}
|
303
|
+
}, [apiClient, data])
|
317
304
|
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
305
|
+
const handlePlaySegment = useCallback((startTime: number) => {
|
306
|
+
// Access the globally exposed seekAndPlay method
|
307
|
+
if ((window as any).seekAndPlayAudio) {
|
308
|
+
(window as any).seekAndPlayAudio(startTime)
|
309
|
+
}
|
310
|
+
}, [])
|
311
|
+
|
312
|
+
const handleResetCorrections = useCallback(() => {
|
313
|
+
if (window.confirm('Are you sure you want to reset all corrections? This cannot be undone.')) {
|
314
|
+
// Clear local storage
|
315
|
+
localStorage.removeItem('lyrics_analyzer_data')
|
316
|
+
// Reset data to initial state
|
317
|
+
setData(JSON.parse(JSON.stringify(initialData)))
|
318
|
+
}
|
319
|
+
}, [initialData])
|
323
320
|
|
324
321
|
return (
|
325
322
|
<Box>
|
@@ -354,6 +351,13 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
354
351
|
)}
|
355
352
|
</Box>
|
356
353
|
|
354
|
+
<Box sx={{ mb: 3 }}>
|
355
|
+
<AudioPlayer
|
356
|
+
apiClient={apiClient}
|
357
|
+
onTimeUpdate={setCurrentAudioTime}
|
358
|
+
/>
|
359
|
+
</Box>
|
360
|
+
|
357
361
|
<Box sx={{ mb: 3 }}>
|
358
362
|
<CorrectionMetrics
|
359
363
|
// Anchor metrics
|
@@ -394,20 +398,24 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
394
398
|
/>
|
395
399
|
</Box>
|
396
400
|
|
397
|
-
<
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
401
|
+
<Box sx={{ mb: 3 }}>
|
402
|
+
<ModeSelector
|
403
|
+
effectiveMode={effectiveMode}
|
404
|
+
onChange={setInteractionMode}
|
405
|
+
/>
|
406
|
+
</Box>
|
402
407
|
|
403
408
|
<Grid container spacing={2} direction={isMobile ? 'column' : 'row'}>
|
404
409
|
<Grid item xs={12} md={6}>
|
405
410
|
<TranscriptionView
|
406
411
|
data={data}
|
412
|
+
mode={effectiveMode}
|
407
413
|
onElementClick={setModalContent}
|
408
414
|
onWordClick={handleWordClick}
|
409
415
|
flashingType={flashingType}
|
410
416
|
highlightInfo={highlightInfo}
|
417
|
+
onPlaySegment={handlePlaySegment}
|
418
|
+
currentTime={currentAudioTime}
|
411
419
|
/>
|
412
420
|
</Grid>
|
413
421
|
<Grid item xs={12} md={6}>
|
@@ -415,13 +423,14 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
415
423
|
referenceTexts={data.reference_texts}
|
416
424
|
anchors={data.anchor_sequences}
|
417
425
|
gaps={data.gap_sequences}
|
426
|
+
mode={effectiveMode}
|
418
427
|
onElementClick={setModalContent}
|
419
428
|
onWordClick={handleWordClick}
|
420
429
|
flashingType={flashingType}
|
421
|
-
|
430
|
+
highlightInfo={highlightInfo}
|
422
431
|
currentSource={currentSource}
|
423
432
|
onSourceChange={setCurrentSource}
|
424
|
-
|
433
|
+
corrected_segments={data.corrected_segments}
|
425
434
|
/>
|
426
435
|
</Grid>
|
427
436
|
</Grid>
|
@@ -430,12 +439,30 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
430
439
|
open={modalContent !== null}
|
431
440
|
content={modalContent}
|
432
441
|
onClose={() => setModalContent(null)}
|
433
|
-
|
434
|
-
|
442
|
+
/>
|
443
|
+
|
444
|
+
<EditModal
|
445
|
+
open={Boolean(editModalSegment)}
|
446
|
+
onClose={() => setEditModalSegment(null)}
|
447
|
+
segment={editModalSegment?.segment ?? null}
|
448
|
+
segmentIndex={editModalSegment?.index ?? null}
|
449
|
+
originalSegment={editModalSegment?.originalSegment ?? null}
|
450
|
+
onSave={handleUpdateSegment}
|
451
|
+
onDelete={handleDeleteSegment}
|
452
|
+
onPlaySegment={handlePlaySegment}
|
453
|
+
currentTime={currentAudioTime}
|
454
|
+
/>
|
455
|
+
|
456
|
+
<ReviewChangesModal
|
457
|
+
open={isReviewModalOpen}
|
458
|
+
onClose={() => setIsReviewModalOpen(false)}
|
459
|
+
originalData={originalData}
|
460
|
+
updatedData={data}
|
461
|
+
onSubmit={handleSubmitToServer}
|
435
462
|
/>
|
436
463
|
|
437
464
|
{!isReadOnly && apiClient && (
|
438
|
-
<Box sx={{ mt: 2 }}>
|
465
|
+
<Box sx={{ mt: 2, mb: 3, display: 'flex', gap: 2 }}>
|
439
466
|
<Button
|
440
467
|
variant="contained"
|
441
468
|
onClick={handleFinishReview}
|
@@ -443,6 +470,13 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
443
470
|
>
|
444
471
|
{isReviewComplete ? 'Review Complete' : 'Finish Review'}
|
445
472
|
</Button>
|
473
|
+
<Button
|
474
|
+
variant="outlined"
|
475
|
+
color="warning"
|
476
|
+
onClick={handleResetCorrections}
|
477
|
+
>
|
478
|
+
Reset Corrections
|
479
|
+
</Button>
|
446
480
|
</Box>
|
447
481
|
)}
|
448
482
|
</Box>
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import { ToggleButton, ToggleButtonGroup, Box, Typography } from '@mui/material';
|
2
|
+
import HighlightIcon from '@mui/icons-material/Highlight';
|
3
|
+
import InfoIcon from '@mui/icons-material/Info';
|
4
|
+
import EditIcon from '@mui/icons-material/Edit';
|
5
|
+
import { InteractionMode } from '../types';
|
6
|
+
|
7
|
+
interface ModeSelectorProps {
|
8
|
+
effectiveMode: InteractionMode;
|
9
|
+
onChange: (mode: InteractionMode) => void;
|
10
|
+
}
|
11
|
+
|
12
|
+
export default function ModeSelector({ effectiveMode, onChange }: ModeSelectorProps) {
|
13
|
+
return (
|
14
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
15
|
+
<Typography variant="body2" color="text.secondary">
|
16
|
+
Click Mode:
|
17
|
+
</Typography>
|
18
|
+
<ToggleButtonGroup
|
19
|
+
value={effectiveMode}
|
20
|
+
exclusive
|
21
|
+
onChange={(_, newMode) => newMode && onChange(newMode)}
|
22
|
+
size="small"
|
23
|
+
>
|
24
|
+
<ToggleButton value="details">
|
25
|
+
<InfoIcon sx={{ mr: 1 }} />
|
26
|
+
Details
|
27
|
+
</ToggleButton>
|
28
|
+
<ToggleButton value="highlight">
|
29
|
+
<HighlightIcon sx={{ mr: 1 }} />
|
30
|
+
Highlight
|
31
|
+
</ToggleButton>
|
32
|
+
<ToggleButton value="edit">
|
33
|
+
<EditIcon sx={{ mr: 1 }} />
|
34
|
+
Edit
|
35
|
+
</ToggleButton>
|
36
|
+
</ToggleButtonGroup>
|
37
|
+
</Box>
|
38
|
+
);
|
39
|
+
}
|