lyrics-transcriber 0.35.1__py3-none-any.whl → 0.37.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/cli/cli_main.py +2 -0
- lyrics_transcriber/core/config.py +1 -1
- lyrics_transcriber/core/controller.py +35 -2
- lyrics_transcriber/correction/corrector.py +8 -8
- lyrics_transcriber/correction/handlers/base.py +4 -0
- lyrics_transcriber/correction/handlers/extend_anchor.py +9 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +21 -10
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +21 -11
- lyrics_transcriber/correction/handlers/syllables_match.py +4 -4
- lyrics_transcriber/correction/handlers/word_count_match.py +19 -10
- lyrics_transcriber/frontend/dist/assets/index-BNNbsbVN.js +182 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +18 -7
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +28 -27
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +108 -12
- lyrics_transcriber/frontend/src/components/EditModal.tsx +10 -2
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +145 -141
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +7 -2
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +24 -12
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +8 -15
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +3 -3
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +36 -51
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +17 -19
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +41 -33
- lyrics_transcriber/frontend/src/components/shared/types.ts +6 -6
- lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +146 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +24 -25
- lyrics_transcriber/frontend/src/types.ts +24 -23
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/lyrics/base_lyrics_provider.py +1 -0
- lyrics_transcriber/lyrics/file_provider.py +89 -0
- lyrics_transcriber/output/cdg.py +32 -6
- lyrics_transcriber/output/video.py +17 -7
- lyrics_transcriber/review/server.py +24 -8
- {lyrics_transcriber-0.35.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/METADATA +1 -1
- {lyrics_transcriber-0.35.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/RECORD +39 -38
- {lyrics_transcriber-0.35.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/entry_points.txt +1 -0
- lyrics_transcriber/frontend/dist/assets/index-CQCER5Fo.js +0 -181
- lyrics_transcriber/frontend/src/components/shared/utils/newlineCalculator.ts +0 -37
- {lyrics_transcriber-0.35.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.35.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/WHEEL +0 -0
@@ -1,8 +1,9 @@
|
|
1
1
|
import {
|
2
|
+
AnchorSequence,
|
2
3
|
CorrectionData,
|
4
|
+
GapSequence,
|
3
5
|
HighlightInfo,
|
4
6
|
InteractionMode,
|
5
|
-
LyricsData,
|
6
7
|
LyricsSegment
|
7
8
|
} from '../types'
|
8
9
|
import LockIcon from '@mui/icons-material/Lock'
|
@@ -19,6 +20,16 @@ import { WordClickInfo, FlashType } from './shared/types'
|
|
19
20
|
import EditModal from './EditModal'
|
20
21
|
import ReviewChangesModal from './ReviewChangesModal'
|
21
22
|
import AudioPlayer from './AudioPlayer'
|
23
|
+
import { nanoid } from 'nanoid'
|
24
|
+
import { initializeDataWithIds, normalizeDataForSubmission } from './shared/utils/initializeDataWithIds'
|
25
|
+
|
26
|
+
// Add type for window augmentation at the top of the file
|
27
|
+
declare global {
|
28
|
+
interface Window {
|
29
|
+
toggleAudioPlayback?: () => void;
|
30
|
+
seekAndPlayAudio?: (startTime: number) => void;
|
31
|
+
}
|
32
|
+
}
|
22
33
|
|
23
34
|
interface LyricsAnalyzerProps {
|
24
35
|
data: CorrectionData
|
@@ -30,51 +41,28 @@ interface LyricsAnalyzerProps {
|
|
30
41
|
|
31
42
|
export type ModalContent = {
|
32
43
|
type: 'anchor'
|
33
|
-
data:
|
34
|
-
|
44
|
+
data: AnchorSequence & {
|
45
|
+
wordId: string
|
35
46
|
word?: string
|
36
47
|
}
|
37
48
|
} | {
|
38
49
|
type: 'gap'
|
39
|
-
data:
|
40
|
-
|
50
|
+
data: GapSequence & {
|
51
|
+
wordId: string
|
41
52
|
word: string
|
42
53
|
}
|
43
54
|
}
|
44
55
|
|
45
|
-
function normalizeDataForSubmission(data: CorrectionData): CorrectionData {
|
46
|
-
// Create a deep clone to avoid modifying the original
|
47
|
-
const normalized = JSON.parse(JSON.stringify(data))
|
48
|
-
|
49
|
-
// Preserve floating point numbers with original precision
|
50
|
-
const preserveFloats = (obj: Record<string, unknown>): void => {
|
51
|
-
for (const key in obj) {
|
52
|
-
const value = obj[key]
|
53
|
-
if (typeof value === 'number') {
|
54
|
-
// Handle integers and floats differently
|
55
|
-
let formatted: string
|
56
|
-
if (Number.isInteger(value)) {
|
57
|
-
formatted = value.toFixed(1) // Force decimal point for integers
|
58
|
-
} else {
|
59
|
-
formatted = value.toString() // Keep original precision for floats
|
60
|
-
}
|
61
|
-
obj[key] = parseFloat(formatted)
|
62
|
-
} else if (typeof value === 'object' && value !== null) {
|
63
|
-
preserveFloats(value as Record<string, unknown>)
|
64
|
-
}
|
65
|
-
}
|
66
|
-
}
|
67
|
-
preserveFloats(normalized)
|
68
|
-
return normalized
|
69
|
-
}
|
70
|
-
|
71
56
|
export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly }: LyricsAnalyzerProps) {
|
72
57
|
const [modalContent, setModalContent] = useState<ModalContent | null>(null)
|
73
58
|
const [flashingType, setFlashingType] = useState<FlashType>(null)
|
74
59
|
const [highlightInfo, setHighlightInfo] = useState<HighlightInfo | null>(null)
|
75
|
-
const [currentSource, setCurrentSource] = useState<
|
60
|
+
const [currentSource, setCurrentSource] = useState<string>(() => {
|
61
|
+
const availableSources = Object.keys(initialData.reference_texts)
|
62
|
+
return availableSources.length > 0 ? availableSources[0] : ''
|
63
|
+
})
|
76
64
|
const [isReviewComplete, setIsReviewComplete] = useState(false)
|
77
|
-
const [data, setData] = useState(initialData)
|
65
|
+
const [data, setData] = useState(() => initializeDataWithIds(initialData))
|
78
66
|
// Create deep copy of initial data for comparison later
|
79
67
|
const [originalData] = useState(() => JSON.parse(JSON.stringify(initialData)))
|
80
68
|
const [interactionMode, setInteractionMode] = useState<InteractionMode>('details')
|
@@ -90,39 +78,62 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
90
78
|
const theme = useTheme()
|
91
79
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
92
80
|
|
81
|
+
// Simple hash function for generating storage keys
|
82
|
+
const generateStorageKey = (text: string): string => {
|
83
|
+
let hash = 0;
|
84
|
+
for (let i = 0; i < text.length; i++) {
|
85
|
+
const char = text.charCodeAt(i);
|
86
|
+
hash = ((hash << 5) - hash) + char;
|
87
|
+
hash = hash & hash; // Convert to 32-bit integer
|
88
|
+
}
|
89
|
+
return `song_${hash}`;
|
90
|
+
}
|
91
|
+
|
93
92
|
// Add local storage handling
|
94
93
|
useEffect(() => {
|
95
94
|
// On mount, try to load saved data
|
96
|
-
const
|
97
|
-
|
95
|
+
const storageKey = generateStorageKey(initialData.transcribed_text);
|
96
|
+
const savedDataStr = localStorage.getItem('lyrics_analyzer_data');
|
97
|
+
const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {};
|
98
|
+
|
99
|
+
if (savedDataObj[storageKey]) {
|
98
100
|
try {
|
99
|
-
const parsed =
|
100
|
-
//
|
101
|
+
const parsed = savedDataObj[storageKey];
|
102
|
+
// Verify it's the same song (extra safety check)
|
101
103
|
if (parsed.transcribed_text === initialData.transcribed_text) {
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
104
|
+
if (window.confirm('Found saved progress for this song. Would you like to restore it?')) {
|
105
|
+
console.log('Restored saved progress from local storage');
|
106
|
+
setData(parsed);
|
107
|
+
} else {
|
108
|
+
// User declined to restore - remove the saved data
|
109
|
+
delete savedDataObj[storageKey];
|
110
|
+
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj));
|
111
|
+
}
|
107
112
|
}
|
108
113
|
} catch (error) {
|
109
|
-
console.error('Failed to parse saved data:', error)
|
110
|
-
|
114
|
+
console.error('Failed to parse saved data:', error);
|
115
|
+
// Remove only this song's data
|
116
|
+
delete savedDataObj[storageKey];
|
117
|
+
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj));
|
111
118
|
}
|
112
119
|
}
|
113
|
-
}, [initialData.transcribed_text])
|
120
|
+
}, [initialData.transcribed_text]);
|
114
121
|
|
115
122
|
// Save to local storage whenever data changes
|
116
123
|
useEffect(() => {
|
117
124
|
if (!isReadOnly) {
|
118
|
-
|
125
|
+
const storageKey = generateStorageKey(initialData.transcribed_text);
|
126
|
+
const savedDataStr = localStorage.getItem('lyrics_analyzer_data');
|
127
|
+
const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {};
|
128
|
+
|
129
|
+
savedDataObj[storageKey] = data;
|
130
|
+
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj));
|
119
131
|
}
|
120
|
-
}, [data, isReadOnly])
|
132
|
+
}, [data, isReadOnly, initialData.transcribed_text]);
|
121
133
|
|
122
|
-
//
|
134
|
+
// Update keyboard event handler
|
123
135
|
useEffect(() => {
|
124
136
|
const handleKeyDown = (e: KeyboardEvent) => {
|
125
|
-
// Ignore if user is typing in an input or textarea
|
126
137
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
127
138
|
return
|
128
139
|
}
|
@@ -133,9 +144,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
133
144
|
} else if (e.key === 'Meta') {
|
134
145
|
setIsCtrlPressed(true)
|
135
146
|
} else if (e.key === ' ' || e.code === 'Space') {
|
136
|
-
e.preventDefault()
|
137
|
-
if (
|
138
|
-
|
147
|
+
e.preventDefault()
|
148
|
+
if (window.toggleAudioPlayback) {
|
149
|
+
window.toggleAudioPlayback()
|
139
150
|
}
|
140
151
|
}
|
141
152
|
}
|
@@ -183,63 +194,45 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
183
194
|
|
184
195
|
const handleWordClick = useCallback((info: WordClickInfo) => {
|
185
196
|
if (effectiveMode === 'edit') {
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
info.wordIndex < currentPosition + segment.words.length) {
|
190
|
-
return true
|
191
|
-
}
|
192
|
-
currentPosition += segment.words.length
|
193
|
-
return false
|
194
|
-
})
|
197
|
+
const segment = data.corrected_segments.find(segment =>
|
198
|
+
segment.words.some(word => word.id === info.word_id)
|
199
|
+
)
|
195
200
|
|
196
|
-
if (
|
201
|
+
if (segment) {
|
202
|
+
const segmentIndex = data.corrected_segments.indexOf(segment)
|
197
203
|
setEditModalSegment({
|
198
|
-
segment
|
204
|
+
segment,
|
199
205
|
index: segmentIndex,
|
200
206
|
originalSegment: originalData.corrected_segments[segmentIndex]
|
201
207
|
})
|
202
208
|
}
|
203
209
|
} else {
|
204
|
-
//
|
210
|
+
// Update flash handling for anchors/gaps
|
205
211
|
if (info.type === 'anchor' && info.anchor) {
|
206
212
|
handleFlash('word', {
|
207
213
|
type: 'anchor',
|
208
|
-
|
209
|
-
|
210
|
-
referenceIndices: info.anchor.reference_positions,
|
211
|
-
referenceLength: info.anchor.length
|
214
|
+
word_ids: info.anchor.word_ids,
|
215
|
+
reference_word_ids: info.anchor.reference_word_ids
|
212
216
|
})
|
213
217
|
} else if (info.type === 'gap' && info.gap) {
|
214
218
|
handleFlash('word', {
|
215
219
|
type: 'gap',
|
216
|
-
|
217
|
-
transcriptionLength: info.gap.length,
|
218
|
-
referenceIndices: {},
|
219
|
-
referenceLength: info.gap.length
|
220
|
+
word_ids: info.gap.word_ids
|
220
221
|
})
|
221
222
|
}
|
222
223
|
}
|
223
224
|
}, [effectiveMode, data.corrected_segments, handleFlash, originalData.corrected_segments])
|
224
225
|
|
225
226
|
const handleUpdateSegment = useCallback((updatedSegment: LyricsSegment) => {
|
226
|
-
|
227
|
-
editModalSegment,
|
228
|
-
updatedSegment,
|
229
|
-
currentSegmentsCount: data.corrected_segments.length
|
230
|
-
})
|
231
|
-
|
232
|
-
if (!editModalSegment) {
|
233
|
-
console.warn('LyricsAnalyzer - No editModalSegment found')
|
234
|
-
return
|
235
|
-
}
|
227
|
+
if (!editModalSegment) return
|
236
228
|
|
237
229
|
const newData = { ...data }
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
230
|
+
|
231
|
+
// Ensure new words have IDs
|
232
|
+
updatedSegment.words = updatedSegment.words.map(word => ({
|
233
|
+
...word,
|
234
|
+
id: word.id || nanoid()
|
235
|
+
}))
|
243
236
|
|
244
237
|
newData.corrected_segments[editModalSegment.index] = updatedSegment
|
245
238
|
|
@@ -248,34 +241,37 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
248
241
|
.map(segment => segment.text)
|
249
242
|
.join('\n')
|
250
243
|
|
251
|
-
console.log('LyricsAnalyzer - After update:', {
|
252
|
-
segmentsCount: newData.corrected_segments.length,
|
253
|
-
updatedText: newData.corrected_text
|
254
|
-
})
|
255
|
-
|
256
244
|
setData(newData)
|
257
|
-
setEditModalSegment(null)
|
245
|
+
setEditModalSegment(null)
|
258
246
|
}, [data, editModalSegment])
|
259
247
|
|
260
248
|
const handleDeleteSegment = useCallback((segmentIndex: number) => {
|
261
|
-
console.log('LyricsAnalyzer - handleDeleteSegment called:', {
|
262
|
-
segmentIndex,
|
263
|
-
currentSegmentsCount: data.corrected_segments.length
|
264
|
-
})
|
265
|
-
|
266
249
|
const newData = { ...data }
|
250
|
+
const deletedSegment = newData.corrected_segments[segmentIndex]
|
251
|
+
|
252
|
+
// Remove segment
|
267
253
|
newData.corrected_segments = newData.corrected_segments.filter((_, index) => index !== segmentIndex)
|
268
254
|
|
255
|
+
// Update anchor and gap sequences to remove references to deleted words
|
256
|
+
newData.anchor_sequences = newData.anchor_sequences.map(anchor => ({
|
257
|
+
...anchor,
|
258
|
+
word_ids: anchor.word_ids.filter(id =>
|
259
|
+
!deletedSegment.words.some(word => word.id === id)
|
260
|
+
)
|
261
|
+
}))
|
262
|
+
|
263
|
+
newData.gap_sequences = newData.gap_sequences.map(gap => ({
|
264
|
+
...gap,
|
265
|
+
word_ids: gap.word_ids.filter(id =>
|
266
|
+
!deletedSegment.words.some(word => word.id === id)
|
267
|
+
)
|
268
|
+
}))
|
269
|
+
|
269
270
|
// Update corrected_text
|
270
271
|
newData.corrected_text = newData.corrected_segments
|
271
272
|
.map(segment => segment.text)
|
272
273
|
.join('\n')
|
273
274
|
|
274
|
-
console.log('LyricsAnalyzer - After delete:', {
|
275
|
-
segmentsCount: newData.corrected_segments.length,
|
276
|
-
updatedText: newData.corrected_text
|
277
|
-
})
|
278
|
-
|
279
275
|
setData(newData)
|
280
276
|
}, [data])
|
281
277
|
|
@@ -302,21 +298,27 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
302
298
|
}
|
303
299
|
}, [apiClient, data])
|
304
300
|
|
301
|
+
// Update play segment handler
|
305
302
|
const handlePlaySegment = useCallback((startTime: number) => {
|
306
|
-
|
307
|
-
|
308
|
-
(window as any).seekAndPlayAudio(startTime)
|
303
|
+
if (window.seekAndPlayAudio) {
|
304
|
+
window.seekAndPlayAudio(startTime)
|
309
305
|
}
|
310
306
|
}, [])
|
311
307
|
|
312
308
|
const handleResetCorrections = useCallback(() => {
|
313
309
|
if (window.confirm('Are you sure you want to reset all corrections? This cannot be undone.')) {
|
314
|
-
|
315
|
-
localStorage.
|
310
|
+
const storageKey = generateStorageKey(initialData.transcribed_text);
|
311
|
+
const savedDataStr = localStorage.getItem('lyrics_analyzer_data');
|
312
|
+
const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {};
|
313
|
+
|
314
|
+
// Remove only this song's data
|
315
|
+
delete savedDataObj[storageKey];
|
316
|
+
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj));
|
317
|
+
|
316
318
|
// Reset data to initial state
|
317
|
-
setData(JSON.parse(JSON.stringify(initialData)))
|
319
|
+
setData(JSON.parse(JSON.stringify(initialData)));
|
318
320
|
}
|
319
|
-
}, [initialData])
|
321
|
+
}, [initialData]);
|
320
322
|
|
321
323
|
return (
|
322
324
|
<Box>
|
@@ -351,58 +353,60 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
351
353
|
)}
|
352
354
|
</Box>
|
353
355
|
|
354
|
-
<Box sx={{ mb: 3 }}>
|
355
|
-
<AudioPlayer
|
356
|
-
apiClient={apiClient}
|
357
|
-
onTimeUpdate={setCurrentAudioTime}
|
358
|
-
/>
|
359
|
-
</Box>
|
360
|
-
|
361
356
|
<Box sx={{ mb: 3 }}>
|
362
357
|
<CorrectionMetrics
|
363
358
|
// Anchor metrics
|
364
359
|
anchorCount={data.metadata.anchor_sequences_count}
|
365
|
-
multiSourceAnchors={data.anchor_sequences
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
Object.keys(anchor.reference_positions).length === 1 &&
|
373
|
-
'genius' in anchor.reference_positions).length
|
374
|
-
}}
|
360
|
+
multiSourceAnchors={data.anchor_sequences?.filter(anchor =>
|
361
|
+
// Add null checks
|
362
|
+
anchor?.reference_word_ids &&
|
363
|
+
Object.keys(anchor.reference_word_ids || {}).length > 1
|
364
|
+
).length ?? 0}
|
365
|
+
anchorWordCount={data.anchor_sequences?.reduce((sum, anchor) =>
|
366
|
+
sum + (anchor.length || 0), 0) ?? 0}
|
375
367
|
// Gap metrics
|
376
|
-
correctedGapCount={data.gap_sequences
|
377
|
-
gap.corrections?.length > 0).length}
|
378
|
-
uncorrectedGapCount={data.gap_sequences
|
379
|
-
!gap.corrections?.length).length}
|
368
|
+
correctedGapCount={data.gap_sequences?.filter(gap =>
|
369
|
+
gap.corrections?.length > 0).length ?? 0}
|
370
|
+
uncorrectedGapCount={data.gap_sequences?.filter(gap =>
|
371
|
+
!gap.corrections?.length).length ?? 0}
|
380
372
|
uncorrectedGaps={data.gap_sequences
|
381
|
-
|
373
|
+
?.filter(gap => !gap.corrections?.length)
|
382
374
|
.map(gap => ({
|
383
|
-
position: gap.
|
375
|
+
position: gap.word_ids[0],
|
384
376
|
length: gap.length
|
385
|
-
}))}
|
377
|
+
})) ?? []}
|
386
378
|
// Correction details
|
387
|
-
replacedCount={data.gap_sequences
|
388
|
-
count + (gap.corrections?.filter(c => !c.is_deletion && !c.split_total).length ?? 0), 0)}
|
389
|
-
addedCount={data.gap_sequences
|
390
|
-
count + (gap.corrections?.filter(c => c.split_total).length ?? 0), 0)}
|
391
|
-
deletedCount={data.gap_sequences
|
392
|
-
count + (gap.corrections?.filter(c => c.is_deletion).length ?? 0), 0)}
|
379
|
+
replacedCount={data.gap_sequences?.reduce((count, gap) =>
|
380
|
+
count + (gap.corrections?.filter(c => !c.is_deletion && !c.split_total).length ?? 0), 0) ?? 0}
|
381
|
+
addedCount={data.gap_sequences?.reduce((count, gap) =>
|
382
|
+
count + (gap.corrections?.filter(c => c.split_total).length ?? 0), 0) ?? 0}
|
383
|
+
deletedCount={data.gap_sequences?.reduce((count, gap) =>
|
384
|
+
count + (gap.corrections?.filter(c => c.is_deletion).length ?? 0), 0) ?? 0}
|
393
385
|
onMetricClick={{
|
394
386
|
anchor: () => handleFlash('anchor'),
|
395
387
|
corrected: () => handleFlash('corrected'),
|
396
388
|
uncorrected: () => handleFlash('uncorrected')
|
397
389
|
}}
|
390
|
+
totalWords={data.metadata.total_words}
|
398
391
|
/>
|
399
392
|
</Box>
|
400
393
|
|
401
|
-
<Box sx={{
|
394
|
+
<Box sx={{
|
395
|
+
display: 'flex',
|
396
|
+
flexDirection: isMobile ? 'column' : 'row',
|
397
|
+
gap: 5,
|
398
|
+
alignItems: 'flex-start',
|
399
|
+
justifyContent: 'flex-start',
|
400
|
+
mb: 3
|
401
|
+
}}>
|
402
402
|
<ModeSelector
|
403
403
|
effectiveMode={effectiveMode}
|
404
404
|
onChange={setInteractionMode}
|
405
405
|
/>
|
406
|
+
<AudioPlayer
|
407
|
+
apiClient={apiClient}
|
408
|
+
onTimeUpdate={setCurrentAudioTime}
|
409
|
+
/>
|
406
410
|
</Box>
|
407
411
|
|
408
412
|
<Grid container spacing={2} direction={isMobile ? 'column' : 'row'}>
|
@@ -8,7 +8,6 @@ import { HighlightedText } from './shared/components/HighlightedText'
|
|
8
8
|
export default function ReferenceView({
|
9
9
|
referenceTexts,
|
10
10
|
anchors,
|
11
|
-
gaps,
|
12
11
|
onElementClick,
|
13
12
|
onWordClick,
|
14
13
|
flashingType,
|
@@ -18,6 +17,12 @@ export default function ReferenceView({
|
|
18
17
|
highlightInfo,
|
19
18
|
mode
|
20
19
|
}: ReferenceViewProps) {
|
20
|
+
// Get available sources from referenceTexts object
|
21
|
+
const availableSources = useMemo(() =>
|
22
|
+
Object.keys(referenceTexts) as Array<string>,
|
23
|
+
[referenceTexts]
|
24
|
+
)
|
25
|
+
|
21
26
|
const { linePositions } = useMemo(() =>
|
22
27
|
calculateReferenceLinePositions(
|
23
28
|
corrected_segments,
|
@@ -36,13 +41,13 @@ export default function ReferenceView({
|
|
36
41
|
<SourceSelector
|
37
42
|
currentSource={currentSource}
|
38
43
|
onSourceChange={onSourceChange}
|
44
|
+
availableSources={availableSources}
|
39
45
|
/>
|
40
46
|
</Box>
|
41
47
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
42
48
|
<HighlightedText
|
43
49
|
text={referenceTexts[currentSource]}
|
44
50
|
anchors={anchors}
|
45
|
-
gaps={gaps}
|
46
51
|
onElementClick={onElementClick}
|
47
52
|
onWordClick={onWordClick}
|
48
53
|
flashingType={flashingType}
|
@@ -26,8 +26,8 @@ interface DiffResult {
|
|
26
26
|
type: 'added' | 'removed' | 'modified'
|
27
27
|
path: string
|
28
28
|
segmentIndex?: number
|
29
|
-
oldValue?:
|
30
|
-
newValue?:
|
29
|
+
oldValue?: string
|
30
|
+
newValue?: string
|
31
31
|
wordChanges?: DiffResult[]
|
32
32
|
}
|
33
33
|
|
@@ -58,13 +58,13 @@ export default function ReviewChangesModal({
|
|
58
58
|
|
59
59
|
const wordChanges: DiffResult[] = []
|
60
60
|
|
61
|
-
// Compare word-level changes
|
62
|
-
segment.words.forEach((word
|
63
|
-
const updatedWord = updatedSegment.words
|
61
|
+
// Compare word-level changes using word IDs
|
62
|
+
segment.words.forEach((word) => {
|
63
|
+
const updatedWord = updatedSegment.words.find(w => w.id === word.id)
|
64
64
|
if (!updatedWord) {
|
65
65
|
wordChanges.push({
|
66
66
|
type: 'removed',
|
67
|
-
path: `Word ${
|
67
|
+
path: `Word ${word.id}`,
|
68
68
|
oldValue: `"${word.text}" (${word.start_time.toFixed(4)} - ${word.end_time.toFixed(4)})`
|
69
69
|
})
|
70
70
|
return
|
@@ -75,7 +75,7 @@ export default function ReviewChangesModal({
|
|
75
75
|
Math.abs(word.end_time - updatedWord.end_time) > 0.0001) {
|
76
76
|
wordChanges.push({
|
77
77
|
type: 'modified',
|
78
|
-
path: `Word ${
|
78
|
+
path: `Word ${word.id}`,
|
79
79
|
oldValue: `"${word.text}" (${word.start_time.toFixed(4)} - ${word.end_time.toFixed(4)})`,
|
80
80
|
newValue: `"${updatedWord.text}" (${updatedWord.start_time.toFixed(4)} - ${updatedWord.end_time.toFixed(4)})`
|
81
81
|
})
|
@@ -83,16 +83,15 @@ export default function ReviewChangesModal({
|
|
83
83
|
})
|
84
84
|
|
85
85
|
// Check for added words
|
86
|
-
|
87
|
-
|
88
|
-
const word = updatedSegment.words[i]
|
86
|
+
updatedSegment.words.forEach((word) => {
|
87
|
+
if (!segment.words.find(w => w.id === word.id)) {
|
89
88
|
wordChanges.push({
|
90
89
|
type: 'added',
|
91
|
-
path: `Word ${
|
90
|
+
path: `Word ${word.id}`,
|
92
91
|
newValue: `"${word.text}" (${word.start_time.toFixed(4)} - ${word.end_time.toFixed(4)})`
|
93
92
|
})
|
94
93
|
}
|
95
|
-
}
|
94
|
+
})
|
96
95
|
|
97
96
|
if (segment.text !== updatedSegment.text ||
|
98
97
|
segment.start_time !== updatedSegment.start_time ||
|
@@ -109,6 +108,19 @@ export default function ReviewChangesModal({
|
|
109
108
|
}
|
110
109
|
})
|
111
110
|
|
111
|
+
// Check for added segments
|
112
|
+
if (updatedData.corrected_segments.length > originalData.corrected_segments.length) {
|
113
|
+
for (let i = originalData.corrected_segments.length; i < updatedData.corrected_segments.length; i++) {
|
114
|
+
const segment = updatedData.corrected_segments[i]
|
115
|
+
diffs.push({
|
116
|
+
type: 'added',
|
117
|
+
path: `Segment ${i}`,
|
118
|
+
segmentIndex: i,
|
119
|
+
newValue: `"${segment.text}" (${segment.start_time.toFixed(4)} - ${segment.end_time.toFixed(4)})`
|
120
|
+
})
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
112
124
|
return diffs
|
113
125
|
}, [originalData, updatedData])
|
114
126
|
|
@@ -48,9 +48,6 @@ export default function TranscriptionView({
|
|
48
48
|
}: TranscriptionViewProps) {
|
49
49
|
const [selectedSegmentIndex, setSelectedSegmentIndex] = useState<number | null>(null)
|
50
50
|
|
51
|
-
// Keep track of global word position
|
52
|
-
let globalWordPosition = 0
|
53
|
-
|
54
51
|
return (
|
55
52
|
<Paper sx={{ p: 2 }}>
|
56
53
|
<Typography variant="h6" gutterBottom>
|
@@ -59,35 +56,32 @@ export default function TranscriptionView({
|
|
59
56
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
60
57
|
{data.corrected_segments.map((segment, segmentIndex) => {
|
61
58
|
// Convert segment words to TranscriptionWordPosition format
|
62
|
-
const segmentWords: TranscriptionWordPosition[] = segment.words.map(
|
63
|
-
|
59
|
+
const segmentWords: TranscriptionWordPosition[] = segment.words.map(word => {
|
60
|
+
// Find if this word belongs to an anchor sequence
|
64
61
|
const anchor = data.anchor_sequences.find(a =>
|
65
|
-
|
66
|
-
position < a.transcription_position + a.length
|
62
|
+
a.word_ids.includes(word.id)
|
67
63
|
)
|
64
|
+
|
65
|
+
// If not in an anchor, check if it belongs to a gap sequence
|
68
66
|
const gap = !anchor ? data.gap_sequences.find(g =>
|
69
|
-
|
70
|
-
position < g.transcription_position + g.length
|
67
|
+
g.word_ids.includes(word.id)
|
71
68
|
) : undefined
|
72
69
|
|
73
70
|
return {
|
74
71
|
word: {
|
72
|
+
id: word.id,
|
75
73
|
text: word.text,
|
76
74
|
start_time: word.start_time,
|
77
75
|
end_time: word.end_time
|
78
76
|
},
|
79
|
-
position,
|
80
77
|
type: anchor ? 'anchor' : gap ? 'gap' : 'other',
|
81
78
|
sequence: anchor || gap,
|
82
79
|
isInRange: true
|
83
80
|
}
|
84
81
|
})
|
85
82
|
|
86
|
-
// Update global position counter for next segment
|
87
|
-
globalWordPosition += segment.words.length
|
88
|
-
|
89
83
|
return (
|
90
|
-
<Box key={
|
84
|
+
<Box key={segment.id} sx={{ display: 'flex', alignItems: 'flex-start', width: '100%' }}>
|
91
85
|
<SegmentControls>
|
92
86
|
<SegmentIndex
|
93
87
|
variant="body2"
|
@@ -109,7 +103,6 @@ export default function TranscriptionView({
|
|
109
103
|
<HighlightedText
|
110
104
|
wordPositions={segmentWords}
|
111
105
|
anchors={data.anchor_sequences}
|
112
|
-
gaps={data.gap_sequences}
|
113
106
|
onElementClick={onElementClick}
|
114
107
|
onWordClick={onWordClick}
|
115
108
|
flashingType={flashingType}
|
@@ -5,7 +5,7 @@ import { ModalContent } from './LyricsAnalyzer'
|
|
5
5
|
|
6
6
|
interface WordEditControlsProps {
|
7
7
|
content: ModalContent
|
8
|
-
onUpdateCorrection?: (
|
8
|
+
onUpdateCorrection?: (wordId: string, updatedWords: string[]) => void
|
9
9
|
onClose: () => void
|
10
10
|
}
|
11
11
|
|
@@ -47,13 +47,13 @@ export default function WordEditControls({ content, onUpdateCorrection, onClose
|
|
47
47
|
|
48
48
|
const handleDelete = () => {
|
49
49
|
if (!onUpdateCorrection) return
|
50
|
-
onUpdateCorrection(content.data.
|
50
|
+
onUpdateCorrection(content.data.wordId, [])
|
51
51
|
onClose()
|
52
52
|
}
|
53
53
|
|
54
54
|
const handleSaveEdit = () => {
|
55
55
|
if (onUpdateCorrection) {
|
56
|
-
onUpdateCorrection(content.data.
|
56
|
+
onUpdateCorrection(content.data.wordId, [editedWord])
|
57
57
|
}
|
58
58
|
onClose()
|
59
59
|
}
|