lyrics-transcriber 0.36.1__py3-none-any.whl → 0.39.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 +22 -2
- lyrics_transcriber/correction/corrector.py +8 -8
- lyrics_transcriber/correction/handlers/base.py +4 -0
- lyrics_transcriber/correction/handlers/extend_anchor.py +22 -2
- 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/correction/handlers/word_operations.py +8 -2
- lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js +26696 -0
- lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/package.json +3 -2
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +1 -2
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +76 -70
- lyrics_transcriber/frontend/src/components/EditModal.tsx +11 -2
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +154 -128
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +42 -4
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +59 -15
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +71 -16
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +16 -19
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +3 -3
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +72 -57
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +113 -41
- lyrics_transcriber/frontend/src/components/shared/types.ts +6 -3
- lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +202 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +23 -24
- lyrics_transcriber/frontend/src/types.ts +25 -15
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/vite.config.js +4 -0
- lyrics_transcriber/frontend/vite.config.ts +4 -0
- lyrics_transcriber/lyrics/genius.py +41 -12
- lyrics_transcriber/output/cdg.py +33 -6
- lyrics_transcriber/output/cdgmaker/composer.py +839 -534
- lyrics_transcriber/output/video.py +17 -7
- lyrics_transcriber/review/server.py +22 -8
- {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/METADATA +3 -2
- {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/RECORD +41 -40
- {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/entry_points.txt +1 -0
- lyrics_transcriber/frontend/dist/assets/index-ztlAYPYT.js +0 -181
- lyrics_transcriber/frontend/src/components/shared/utils/newlineCalculator.ts +0 -37
- {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.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,44 +41,18 @@ 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)
|
@@ -77,7 +62,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
77
62
|
return availableSources.length > 0 ? availableSources[0] : ''
|
78
63
|
})
|
79
64
|
const [isReviewComplete, setIsReviewComplete] = useState(false)
|
80
|
-
const [data, setData] = useState(initialData)
|
65
|
+
const [data, setData] = useState(() => initializeDataWithIds(initialData))
|
81
66
|
// Create deep copy of initial data for comparison later
|
82
67
|
const [originalData] = useState(() => JSON.parse(JSON.stringify(initialData)))
|
83
68
|
const [interactionMode, setInteractionMode] = useState<InteractionMode>('details')
|
@@ -93,39 +78,78 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
93
78
|
const theme = useTheme()
|
94
79
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
95
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
|
+
|
96
92
|
// Add local storage handling
|
97
93
|
useEffect(() => {
|
98
|
-
|
99
|
-
const
|
100
|
-
|
94
|
+
const storageKey = generateStorageKey(initialData.transcribed_text);
|
95
|
+
const savedDataStr = localStorage.getItem('lyrics_analyzer_data');
|
96
|
+
const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {};
|
97
|
+
|
98
|
+
if (savedDataObj[storageKey]) {
|
101
99
|
try {
|
102
|
-
const parsed =
|
103
|
-
// Only restore if it's the same song (matching transcribed text)
|
100
|
+
const parsed = savedDataObj[storageKey];
|
104
101
|
if (parsed.transcribed_text === initialData.transcribed_text) {
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
102
|
+
const stripIds = (obj: CorrectionData): LyricsSegment[] => {
|
103
|
+
const clone = JSON.parse(JSON.stringify(obj));
|
104
|
+
return clone.corrected_segments.map((segment: LyricsSegment) => {
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
106
|
+
const { id: _id, ...strippedSegment } = segment;
|
107
|
+
return {
|
108
|
+
...strippedSegment,
|
109
|
+
words: segment.words.map(word => {
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
111
|
+
const { id: _wordId, ...strippedWord } = word;
|
112
|
+
return strippedWord;
|
113
|
+
})
|
114
|
+
};
|
115
|
+
});
|
116
|
+
};
|
117
|
+
|
118
|
+
const strippedSaved = stripIds(parsed);
|
119
|
+
const strippedInitial = stripIds(initialData);
|
120
|
+
|
121
|
+
const hasChanges = JSON.stringify(strippedSaved) !== JSON.stringify(strippedInitial);
|
122
|
+
|
123
|
+
if (hasChanges && window.confirm('Found saved progress for this song. Would you like to restore it?')) {
|
124
|
+
setData(parsed);
|
125
|
+
} else if (!hasChanges) {
|
126
|
+
delete savedDataObj[storageKey];
|
127
|
+
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj));
|
128
|
+
}
|
110
129
|
}
|
111
130
|
} catch (error) {
|
112
|
-
console.error('Failed to parse saved data:', error)
|
113
|
-
|
131
|
+
console.error('Failed to parse saved data:', error);
|
132
|
+
delete savedDataObj[storageKey];
|
133
|
+
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj));
|
114
134
|
}
|
115
135
|
}
|
116
|
-
}, [initialData
|
136
|
+
}, [initialData]);
|
117
137
|
|
118
138
|
// Save to local storage whenever data changes
|
119
139
|
useEffect(() => {
|
120
140
|
if (!isReadOnly) {
|
121
|
-
|
141
|
+
const storageKey = generateStorageKey(initialData.transcribed_text);
|
142
|
+
const savedDataStr = localStorage.getItem('lyrics_analyzer_data');
|
143
|
+
const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {};
|
144
|
+
|
145
|
+
savedDataObj[storageKey] = data;
|
146
|
+
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj));
|
122
147
|
}
|
123
|
-
}, [data, isReadOnly])
|
148
|
+
}, [data, isReadOnly, initialData.transcribed_text]);
|
124
149
|
|
125
|
-
//
|
150
|
+
// Update keyboard event handler
|
126
151
|
useEffect(() => {
|
127
152
|
const handleKeyDown = (e: KeyboardEvent) => {
|
128
|
-
// Ignore if user is typing in an input or textarea
|
129
153
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
130
154
|
return
|
131
155
|
}
|
@@ -136,9 +160,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
136
160
|
} else if (e.key === 'Meta') {
|
137
161
|
setIsCtrlPressed(true)
|
138
162
|
} else if (e.key === ' ' || e.code === 'Space') {
|
139
|
-
e.preventDefault()
|
140
|
-
if (
|
141
|
-
|
163
|
+
e.preventDefault()
|
164
|
+
if (window.toggleAudioPlayback) {
|
165
|
+
window.toggleAudioPlayback()
|
142
166
|
}
|
143
167
|
}
|
144
168
|
}
|
@@ -186,63 +210,45 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
186
210
|
|
187
211
|
const handleWordClick = useCallback((info: WordClickInfo) => {
|
188
212
|
if (effectiveMode === 'edit') {
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
info.wordIndex < currentPosition + segment.words.length) {
|
193
|
-
return true
|
194
|
-
}
|
195
|
-
currentPosition += segment.words.length
|
196
|
-
return false
|
197
|
-
})
|
213
|
+
const segment = data.corrected_segments.find(segment =>
|
214
|
+
segment.words.some(word => word.id === info.word_id)
|
215
|
+
)
|
198
216
|
|
199
|
-
if (
|
217
|
+
if (segment) {
|
218
|
+
const segmentIndex = data.corrected_segments.indexOf(segment)
|
200
219
|
setEditModalSegment({
|
201
|
-
segment
|
220
|
+
segment,
|
202
221
|
index: segmentIndex,
|
203
222
|
originalSegment: originalData.corrected_segments[segmentIndex]
|
204
223
|
})
|
205
224
|
}
|
206
225
|
} else {
|
207
|
-
//
|
226
|
+
// Update flash handling for anchors/gaps
|
208
227
|
if (info.type === 'anchor' && info.anchor) {
|
209
228
|
handleFlash('word', {
|
210
229
|
type: 'anchor',
|
211
|
-
|
212
|
-
|
213
|
-
referenceIndices: info.anchor.reference_positions,
|
214
|
-
referenceLength: info.anchor.length
|
230
|
+
word_ids: info.anchor.word_ids,
|
231
|
+
reference_word_ids: info.anchor.reference_word_ids
|
215
232
|
})
|
216
233
|
} else if (info.type === 'gap' && info.gap) {
|
217
234
|
handleFlash('word', {
|
218
235
|
type: 'gap',
|
219
|
-
|
220
|
-
transcriptionLength: info.gap.length,
|
221
|
-
referenceIndices: {},
|
222
|
-
referenceLength: info.gap.length
|
236
|
+
word_ids: info.gap.word_ids
|
223
237
|
})
|
224
238
|
}
|
225
239
|
}
|
226
240
|
}, [effectiveMode, data.corrected_segments, handleFlash, originalData.corrected_segments])
|
227
241
|
|
228
242
|
const handleUpdateSegment = useCallback((updatedSegment: LyricsSegment) => {
|
229
|
-
|
230
|
-
editModalSegment,
|
231
|
-
updatedSegment,
|
232
|
-
currentSegmentsCount: data.corrected_segments.length
|
233
|
-
})
|
234
|
-
|
235
|
-
if (!editModalSegment) {
|
236
|
-
console.warn('LyricsAnalyzer - No editModalSegment found')
|
237
|
-
return
|
238
|
-
}
|
243
|
+
if (!editModalSegment) return
|
239
244
|
|
240
245
|
const newData = { ...data }
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
+
|
247
|
+
// Ensure new words have IDs
|
248
|
+
updatedSegment.words = updatedSegment.words.map(word => ({
|
249
|
+
...word,
|
250
|
+
id: word.id || nanoid()
|
251
|
+
}))
|
246
252
|
|
247
253
|
newData.corrected_segments[editModalSegment.index] = updatedSegment
|
248
254
|
|
@@ -251,34 +257,37 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
251
257
|
.map(segment => segment.text)
|
252
258
|
.join('\n')
|
253
259
|
|
254
|
-
console.log('LyricsAnalyzer - After update:', {
|
255
|
-
segmentsCount: newData.corrected_segments.length,
|
256
|
-
updatedText: newData.corrected_text
|
257
|
-
})
|
258
|
-
|
259
260
|
setData(newData)
|
260
|
-
setEditModalSegment(null)
|
261
|
+
setEditModalSegment(null)
|
261
262
|
}, [data, editModalSegment])
|
262
263
|
|
263
264
|
const handleDeleteSegment = useCallback((segmentIndex: number) => {
|
264
|
-
console.log('LyricsAnalyzer - handleDeleteSegment called:', {
|
265
|
-
segmentIndex,
|
266
|
-
currentSegmentsCount: data.corrected_segments.length
|
267
|
-
})
|
268
|
-
|
269
265
|
const newData = { ...data }
|
266
|
+
const deletedSegment = newData.corrected_segments[segmentIndex]
|
267
|
+
|
268
|
+
// Remove segment
|
270
269
|
newData.corrected_segments = newData.corrected_segments.filter((_, index) => index !== segmentIndex)
|
271
270
|
|
271
|
+
// Update anchor and gap sequences to remove references to deleted words
|
272
|
+
newData.anchor_sequences = newData.anchor_sequences.map(anchor => ({
|
273
|
+
...anchor,
|
274
|
+
word_ids: anchor.word_ids.filter(id =>
|
275
|
+
!deletedSegment.words.some(word => word.id === id)
|
276
|
+
)
|
277
|
+
}))
|
278
|
+
|
279
|
+
newData.gap_sequences = newData.gap_sequences.map(gap => ({
|
280
|
+
...gap,
|
281
|
+
word_ids: gap.word_ids.filter(id =>
|
282
|
+
!deletedSegment.words.some(word => word.id === id)
|
283
|
+
)
|
284
|
+
}))
|
285
|
+
|
272
286
|
// Update corrected_text
|
273
287
|
newData.corrected_text = newData.corrected_segments
|
274
288
|
.map(segment => segment.text)
|
275
289
|
.join('\n')
|
276
290
|
|
277
|
-
console.log('LyricsAnalyzer - After delete:', {
|
278
|
-
segmentsCount: newData.corrected_segments.length,
|
279
|
-
updatedText: newData.corrected_text
|
280
|
-
})
|
281
|
-
|
282
291
|
setData(newData)
|
283
292
|
}, [data])
|
284
293
|
|
@@ -305,21 +314,34 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
305
314
|
}
|
306
315
|
}, [apiClient, data])
|
307
316
|
|
317
|
+
// Update play segment handler
|
308
318
|
const handlePlaySegment = useCallback((startTime: number) => {
|
309
|
-
|
310
|
-
|
311
|
-
(window as any).seekAndPlayAudio(startTime)
|
319
|
+
if (window.seekAndPlayAudio) {
|
320
|
+
window.seekAndPlayAudio(startTime)
|
312
321
|
}
|
313
322
|
}, [])
|
314
323
|
|
315
324
|
const handleResetCorrections = useCallback(() => {
|
316
325
|
if (window.confirm('Are you sure you want to reset all corrections? This cannot be undone.')) {
|
317
|
-
|
318
|
-
localStorage.
|
319
|
-
|
320
|
-
|
326
|
+
const storageKey = generateStorageKey(initialData.transcribed_text);
|
327
|
+
const savedDataStr = localStorage.getItem('lyrics_analyzer_data');
|
328
|
+
const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {};
|
329
|
+
|
330
|
+
// Remove only this song's data
|
331
|
+
delete savedDataObj[storageKey];
|
332
|
+
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj));
|
333
|
+
|
334
|
+
// Reset data to initial state with proper initialization
|
335
|
+
const freshData = initializeDataWithIds(JSON.parse(JSON.stringify(initialData)));
|
336
|
+
setData(freshData);
|
337
|
+
|
338
|
+
// Reset any UI state that might affect highlights
|
339
|
+
setModalContent(null);
|
340
|
+
setFlashingType(null);
|
341
|
+
setHighlightInfo(null);
|
342
|
+
setInteractionMode('details');
|
321
343
|
}
|
322
|
-
}, [initialData])
|
344
|
+
}, [initialData]);
|
323
345
|
|
324
346
|
return (
|
325
347
|
<Box>
|
@@ -358,27 +380,31 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
358
380
|
<CorrectionMetrics
|
359
381
|
// Anchor metrics
|
360
382
|
anchorCount={data.metadata.anchor_sequences_count}
|
361
|
-
multiSourceAnchors={data.anchor_sequences
|
362
|
-
|
363
|
-
|
383
|
+
multiSourceAnchors={data.anchor_sequences?.filter(anchor =>
|
384
|
+
// Add null checks
|
385
|
+
anchor?.reference_word_ids &&
|
386
|
+
Object.keys(anchor.reference_word_ids || {}).length > 1
|
387
|
+
).length ?? 0}
|
388
|
+
anchorWordCount={data.anchor_sequences?.reduce((sum, anchor) =>
|
389
|
+
sum + (anchor.length || 0), 0) ?? 0}
|
364
390
|
// Gap metrics
|
365
|
-
correctedGapCount={data.gap_sequences
|
366
|
-
gap.corrections?.length > 0).length}
|
367
|
-
uncorrectedGapCount={data.gap_sequences
|
368
|
-
!gap.corrections?.length).length}
|
391
|
+
correctedGapCount={data.gap_sequences?.filter(gap =>
|
392
|
+
gap.corrections?.length > 0).length ?? 0}
|
393
|
+
uncorrectedGapCount={data.gap_sequences?.filter(gap =>
|
394
|
+
!gap.corrections?.length).length ?? 0}
|
369
395
|
uncorrectedGaps={data.gap_sequences
|
370
|
-
|
396
|
+
?.filter(gap => !gap.corrections?.length && gap.word_ids)
|
371
397
|
.map(gap => ({
|
372
|
-
position: gap.
|
373
|
-
length: gap.length
|
374
|
-
}))}
|
398
|
+
position: gap.word_ids?.[0] ?? '',
|
399
|
+
length: gap.length ?? 0
|
400
|
+
})) ?? []}
|
375
401
|
// Correction details
|
376
|
-
replacedCount={data.gap_sequences
|
377
|
-
count + (gap.corrections?.filter(c => !c.is_deletion && !c.split_total).length ?? 0), 0)}
|
378
|
-
addedCount={data.gap_sequences
|
379
|
-
count + (gap.corrections?.filter(c => c.split_total).length ?? 0), 0)}
|
380
|
-
deletedCount={data.gap_sequences
|
381
|
-
count + (gap.corrections?.filter(c => c.is_deletion).length ?? 0), 0)}
|
402
|
+
replacedCount={data.gap_sequences?.reduce((count, gap) =>
|
403
|
+
count + (gap.corrections?.filter(c => !c.is_deletion && !c.split_total).length ?? 0), 0) ?? 0}
|
404
|
+
addedCount={data.gap_sequences?.reduce((count, gap) =>
|
405
|
+
count + (gap.corrections?.filter(c => c.split_total).length ?? 0), 0) ?? 0}
|
406
|
+
deletedCount={data.gap_sequences?.reduce((count, gap) =>
|
407
|
+
count + (gap.corrections?.filter(c => c.is_deletion).length ?? 0), 0) ?? 0}
|
382
408
|
onMetricClick={{
|
383
409
|
anchor: () => handleFlash('anchor'),
|
384
410
|
corrected: () => handleFlash('corrected'),
|
@@ -4,11 +4,11 @@ import { ReferenceViewProps } from './shared/types'
|
|
4
4
|
import { calculateReferenceLinePositions } from './shared/utils/referenceLineCalculator'
|
5
5
|
import { SourceSelector } from './shared/components/SourceSelector'
|
6
6
|
import { HighlightedText } from './shared/components/HighlightedText'
|
7
|
+
import { WordCorrection } from '@/types'
|
7
8
|
|
8
9
|
export default function ReferenceView({
|
9
10
|
referenceTexts,
|
10
11
|
anchors,
|
11
|
-
gaps,
|
12
12
|
onElementClick,
|
13
13
|
onWordClick,
|
14
14
|
flashingType,
|
@@ -16,10 +16,11 @@ export default function ReferenceView({
|
|
16
16
|
currentSource,
|
17
17
|
onSourceChange,
|
18
18
|
highlightInfo,
|
19
|
-
mode
|
19
|
+
mode,
|
20
|
+
gaps
|
20
21
|
}: ReferenceViewProps) {
|
21
22
|
// Get available sources from referenceTexts object
|
22
|
-
const availableSources = useMemo(() =>
|
23
|
+
const availableSources = useMemo(() =>
|
23
24
|
Object.keys(referenceTexts) as Array<string>,
|
24
25
|
[referenceTexts]
|
25
26
|
)
|
@@ -33,6 +34,42 @@ export default function ReferenceView({
|
|
33
34
|
[corrected_segments, anchors, currentSource]
|
34
35
|
)
|
35
36
|
|
37
|
+
// Create a mapping of reference words to their corrections
|
38
|
+
const referenceCorrections = useMemo(() => {
|
39
|
+
const corrections = new Map<string, string>();
|
40
|
+
|
41
|
+
console.log('Building referenceCorrections map:', {
|
42
|
+
gapsCount: gaps.length,
|
43
|
+
currentSource,
|
44
|
+
});
|
45
|
+
|
46
|
+
gaps.forEach(gap => {
|
47
|
+
gap.corrections.forEach((correction: WordCorrection) => {
|
48
|
+
// Get the reference position for this correction
|
49
|
+
const referencePosition = correction.reference_positions?.[currentSource];
|
50
|
+
|
51
|
+
if (typeof referencePosition === 'number') {
|
52
|
+
const wordId = `${currentSource}-word-${referencePosition}`;
|
53
|
+
corrections.set(wordId, correction.corrected_word);
|
54
|
+
|
55
|
+
console.log('Adding correction mapping:', {
|
56
|
+
wordId,
|
57
|
+
correctedWord: correction.corrected_word,
|
58
|
+
referencePosition,
|
59
|
+
correction
|
60
|
+
});
|
61
|
+
}
|
62
|
+
});
|
63
|
+
});
|
64
|
+
|
65
|
+
console.log('Final referenceCorrections map:', {
|
66
|
+
size: corrections.size,
|
67
|
+
entries: Array.from(corrections.entries())
|
68
|
+
});
|
69
|
+
|
70
|
+
return corrections;
|
71
|
+
}, [gaps, currentSource]);
|
72
|
+
|
36
73
|
return (
|
37
74
|
<Paper sx={{ p: 2 }}>
|
38
75
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
@@ -49,7 +86,6 @@ export default function ReferenceView({
|
|
49
86
|
<HighlightedText
|
50
87
|
text={referenceTexts[currentSource]}
|
51
88
|
anchors={anchors}
|
52
|
-
gaps={gaps}
|
53
89
|
onElementClick={onElementClick}
|
54
90
|
onWordClick={onWordClick}
|
55
91
|
flashingType={flashingType}
|
@@ -58,6 +94,8 @@ export default function ReferenceView({
|
|
58
94
|
isReference={true}
|
59
95
|
currentSource={currentSource}
|
60
96
|
linePositions={linePositions}
|
97
|
+
referenceCorrections={referenceCorrections}
|
98
|
+
gaps={gaps}
|
61
99
|
/>
|
62
100
|
</Box>
|
63
101
|
</Paper>
|
@@ -26,11 +26,40 @@ 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
|
|
34
|
+
// Add interfaces for the word and segment structures
|
35
|
+
interface Word {
|
36
|
+
text: string
|
37
|
+
start_time: number
|
38
|
+
end_time: number
|
39
|
+
id?: string
|
40
|
+
}
|
41
|
+
|
42
|
+
interface Segment {
|
43
|
+
text: string
|
44
|
+
start_time: number
|
45
|
+
end_time: number
|
46
|
+
words: Word[]
|
47
|
+
id?: string
|
48
|
+
}
|
49
|
+
|
50
|
+
const normalizeWordForComparison = (word: Word): Omit<Word, 'id'> => ({
|
51
|
+
text: word.text,
|
52
|
+
start_time: word.start_time,
|
53
|
+
end_time: word.end_time
|
54
|
+
})
|
55
|
+
|
56
|
+
const normalizeSegmentForComparison = (segment: Segment): Omit<Segment, 'id'> => ({
|
57
|
+
text: segment.text,
|
58
|
+
start_time: segment.start_time,
|
59
|
+
end_time: segment.end_time,
|
60
|
+
words: segment.words.map(normalizeWordForComparison)
|
61
|
+
})
|
62
|
+
|
34
63
|
export default function ReviewChangesModal({
|
35
64
|
open,
|
36
65
|
onClose,
|
@@ -44,23 +73,25 @@ export default function ReviewChangesModal({
|
|
44
73
|
const diffs: DiffResult[] = []
|
45
74
|
|
46
75
|
// Compare corrected segments
|
47
|
-
originalData.corrected_segments.forEach((
|
76
|
+
originalData.corrected_segments.forEach((originalSegment, index) => {
|
48
77
|
const updatedSegment = updatedData.corrected_segments[index]
|
49
78
|
if (!updatedSegment) {
|
50
79
|
diffs.push({
|
51
80
|
type: 'removed',
|
52
81
|
path: `Segment ${index}`,
|
53
82
|
segmentIndex: index,
|
54
|
-
oldValue:
|
83
|
+
oldValue: originalSegment.text
|
55
84
|
})
|
56
85
|
return
|
57
86
|
}
|
58
87
|
|
88
|
+
const normalizedOriginal = normalizeSegmentForComparison(originalSegment)
|
89
|
+
const normalizedUpdated = normalizeSegmentForComparison(updatedSegment)
|
59
90
|
const wordChanges: DiffResult[] = []
|
60
91
|
|
61
|
-
// Compare word-level changes
|
62
|
-
|
63
|
-
const updatedWord =
|
92
|
+
// Compare word-level changes based on position rather than IDs
|
93
|
+
normalizedOriginal.words.forEach((word: Omit<Word, 'id'>, wordIndex: number) => {
|
94
|
+
const updatedWord = normalizedUpdated.words[wordIndex]
|
64
95
|
if (!updatedWord) {
|
65
96
|
wordChanges.push({
|
66
97
|
type: 'removed',
|
@@ -83,9 +114,9 @@ export default function ReviewChangesModal({
|
|
83
114
|
})
|
84
115
|
|
85
116
|
// Check for added words
|
86
|
-
if (
|
87
|
-
for (let i =
|
88
|
-
const word =
|
117
|
+
if (normalizedUpdated.words.length > normalizedOriginal.words.length) {
|
118
|
+
for (let i = normalizedOriginal.words.length; i < normalizedUpdated.words.length; i++) {
|
119
|
+
const word = normalizedUpdated.words[i]
|
89
120
|
wordChanges.push({
|
90
121
|
type: 'added',
|
91
122
|
path: `Word ${i}`,
|
@@ -94,21 +125,34 @@ export default function ReviewChangesModal({
|
|
94
125
|
}
|
95
126
|
}
|
96
127
|
|
97
|
-
if (
|
98
|
-
|
99
|
-
|
128
|
+
if (normalizedOriginal.text !== normalizedUpdated.text ||
|
129
|
+
Math.abs(normalizedOriginal.start_time - normalizedUpdated.start_time) > 0.0001 ||
|
130
|
+
Math.abs(normalizedOriginal.end_time - normalizedUpdated.end_time) > 0.0001 ||
|
100
131
|
wordChanges.length > 0) {
|
101
132
|
diffs.push({
|
102
133
|
type: 'modified',
|
103
134
|
path: `Segment ${index}`,
|
104
135
|
segmentIndex: index,
|
105
|
-
oldValue: `"${
|
106
|
-
newValue: `"${
|
136
|
+
oldValue: `"${normalizedOriginal.text}" (${normalizedOriginal.start_time.toFixed(4)} - ${normalizedOriginal.end_time.toFixed(4)})`,
|
137
|
+
newValue: `"${normalizedUpdated.text}" (${normalizedUpdated.start_time.toFixed(4)} - ${normalizedUpdated.end_time.toFixed(4)})`,
|
107
138
|
wordChanges: wordChanges.length > 0 ? wordChanges : undefined
|
108
139
|
})
|
109
140
|
}
|
110
141
|
})
|
111
142
|
|
143
|
+
// Check for added segments
|
144
|
+
if (updatedData.corrected_segments.length > originalData.corrected_segments.length) {
|
145
|
+
for (let i = originalData.corrected_segments.length; i < updatedData.corrected_segments.length; i++) {
|
146
|
+
const segment = updatedData.corrected_segments[i]
|
147
|
+
diffs.push({
|
148
|
+
type: 'added',
|
149
|
+
path: `Segment ${i}`,
|
150
|
+
segmentIndex: i,
|
151
|
+
newValue: `"${segment.text}" (${segment.start_time.toFixed(4)} - ${segment.end_time.toFixed(4)})`
|
152
|
+
})
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
112
156
|
return diffs
|
113
157
|
}, [originalData, updatedData])
|
114
158
|
|