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.
Files changed (39) hide show
  1. lyrics_transcriber/core/controller.py +10 -1
  2. lyrics_transcriber/correction/corrector.py +4 -3
  3. lyrics_transcriber/frontend/dist/assets/index-CQCER5Fo.js +181 -0
  4. lyrics_transcriber/frontend/dist/index.html +1 -1
  5. lyrics_transcriber/frontend/src/App.tsx +6 -2
  6. lyrics_transcriber/frontend/src/api.ts +9 -0
  7. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +155 -0
  8. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +1 -1
  9. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +23 -191
  10. lyrics_transcriber/frontend/src/components/EditModal.tsx +407 -0
  11. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +255 -221
  12. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +39 -0
  13. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +35 -264
  14. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +232 -0
  15. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
  16. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +315 -0
  17. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +116 -138
  18. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +116 -0
  19. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +243 -0
  20. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +28 -0
  21. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +52 -0
  22. lyrics_transcriber/frontend/src/components/{constants.ts → shared/constants.ts} +1 -0
  23. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +137 -0
  24. lyrics_transcriber/frontend/src/components/{styles.ts → shared/styles.ts} +1 -1
  25. lyrics_transcriber/frontend/src/components/shared/types.ts +99 -0
  26. lyrics_transcriber/frontend/src/components/shared/utils/newlineCalculator.ts +37 -0
  27. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +76 -0
  28. lyrics_transcriber/frontend/src/types.ts +2 -43
  29. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  30. lyrics_transcriber/lyrics/spotify.py +11 -0
  31. lyrics_transcriber/output/generator.py +28 -11
  32. lyrics_transcriber/review/server.py +38 -12
  33. {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/METADATA +1 -1
  34. {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/RECORD +37 -24
  35. lyrics_transcriber/frontend/dist/assets/index-DqFgiUni.js +0 -245
  36. lyrics_transcriber/frontend/src/components/DebugPanel.tsx +0 -311
  37. {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/LICENSE +0 -0
  38. {lyrics_transcriber-0.34.2.dist-info → lyrics_transcriber-0.35.0.dist-info}/WHEEL +0 -0
  39. {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 DebugPanel from './DebugPanel'
12
-
13
- interface WordClickInfo {
14
- wordIndex: number
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
- console.group('Word Click Debug Info')
101
- console.log('Clicked word info:', JSON.stringify(info, null, 2))
102
-
103
- if (info.type === 'gap' && info.gap) {
104
- console.log('Gap sequence:', JSON.stringify(info.gap, null, 2))
105
- const modalData = {
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
- setModalContent(modalData)
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
- // Create a new correction
148
- const newCorrection: WordCorrection = {
149
- original_word: originalGap.words[wordIndexInGap],
150
- corrected_word: updatedWords.join(' '),
151
- segment_index: 0,
152
- original_position: position,
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
- // Find the corresponding segment by counting words
163
- let currentPosition = 0
164
- let segmentIndex = -1
165
- let wordIndex = -1
166
-
167
- for (let i = 0; i < newData.corrected_segments.length; i++) {
168
- const segment = newData.corrected_segments[i]
169
- if (position >= currentPosition && position < currentPosition + segment.words.length) {
170
- segmentIndex = i
171
- wordIndex = position - currentPosition
172
- break
173
- }
174
- currentPosition += segment.words.length
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
- console.log('Segment search:', {
178
- position,
179
- segmentIndex,
180
- wordIndex,
181
- totalSegments: newData.corrected_segments.length
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
- if (segmentIndex !== -1 && wordIndex !== -1) {
185
- const segment = newData.corrected_segments[segmentIndex]
186
- const timingWord = segment.words[wordIndex]
232
+ if (!editModalSegment) {
233
+ console.warn('LyricsAnalyzer - No editModalSegment found')
234
+ return
235
+ }
187
236
 
188
- console.log('Found matching segment:', {
189
- text: segment.text,
190
- wordCount: segment.words.length,
191
- wordIndex,
192
- word: timingWord?.text
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
- if (!timingWord) {
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
- // Update gap sequence
202
- const newWords = [...originalGap.words]
203
- newWords[wordIndexInGap] = updatedWords[0]
204
- newData.gap_sequences[gapIndex] = {
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
- // Update segment
214
- const newSegmentWords = [...segment.words]
215
- newSegmentWords[wordIndex] = {
216
- ...timingWord,
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
- newData.corrected_segments[segmentIndex] = {
222
- ...segment,
223
- words: newSegmentWords,
224
- text: newSegmentWords.map(word => word.text).join(' ')
225
- }
256
+ setData(newData)
257
+ setEditModalSegment(null) // Close the modal
258
+ }, [data, editModalSegment])
226
259
 
227
- console.log('Updated both gap and segment')
228
- } else {
229
- console.error('Could not find matching segment for position:', position)
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 the corrected_text field
269
+ // Update corrected_text
234
270
  newData.corrected_text = newData.corrected_segments
235
- .map((segment: LyricsSegment) => segment.text)
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(async () => {
282
+ const handleFinishReview = useCallback(() => {
283
+ setIsReviewModalOpen(true)
284
+ }, [])
285
+
286
+ const handleSubmitToServer = useCallback(async () => {
243
287
  if (!apiClient) return
244
288
 
245
- let dataToSubmit: CorrectionData
246
- if (manualCorrections.size > 0) {
247
- console.log('Manual corrections found:', Array.from(manualCorrections.entries()))
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
- // Preserve original newline formatting in corrected_text
283
- if (manualCorrections.size > 0) {
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
- dataToSubmit = normalizeDataForSubmission(updatedData)
311
- console.log('Submitting data with manual corrections:', dataToSubmit)
312
- } else {
313
- console.log('Original data:', initialData)
314
- console.log('No manual corrections, submitting original data')
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
- console.log('Data being sent to API:', dataToSubmit)
319
- await apiClient.submitCorrections(dataToSubmit)
320
- setIsReviewComplete(true)
321
- // eslint-disable-next-line react-hooks/exhaustive-deps
322
- }, [apiClient, initialData, manualCorrections])
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
- <DebugPanel
398
- data={data}
399
- currentSource={currentSource}
400
- anchorMatchInfo={anchorMatchInfo}
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
- corrected_segments={data.corrected_segments}
430
+ highlightInfo={highlightInfo}
422
431
  currentSource={currentSource}
423
432
  onSourceChange={setCurrentSource}
424
- onDebugInfoUpdate={setAnchorMatchInfo}
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
- onUpdateCorrection={handleUpdateCorrection}
434
- isReadOnly={isReadOnly}
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
+ }