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.
Files changed (43) hide show
  1. lyrics_transcriber/core/controller.py +22 -2
  2. lyrics_transcriber/correction/corrector.py +8 -8
  3. lyrics_transcriber/correction/handlers/base.py +4 -0
  4. lyrics_transcriber/correction/handlers/extend_anchor.py +22 -2
  5. lyrics_transcriber/correction/handlers/no_space_punct_match.py +21 -10
  6. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +21 -11
  7. lyrics_transcriber/correction/handlers/syllables_match.py +4 -4
  8. lyrics_transcriber/correction/handlers/word_count_match.py +19 -10
  9. lyrics_transcriber/correction/handlers/word_operations.py +8 -2
  10. lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js +26696 -0
  11. lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js.map +1 -0
  12. lyrics_transcriber/frontend/dist/index.html +1 -1
  13. lyrics_transcriber/frontend/package.json +3 -2
  14. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +1 -2
  15. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +76 -70
  16. lyrics_transcriber/frontend/src/components/EditModal.tsx +11 -2
  17. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +154 -128
  18. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +42 -4
  19. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +59 -15
  20. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +71 -16
  21. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +16 -19
  22. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +3 -3
  23. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +72 -57
  24. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +113 -41
  25. lyrics_transcriber/frontend/src/components/shared/types.ts +6 -3
  26. lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +202 -0
  27. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +23 -24
  28. lyrics_transcriber/frontend/src/types.ts +25 -15
  29. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  30. lyrics_transcriber/frontend/vite.config.js +4 -0
  31. lyrics_transcriber/frontend/vite.config.ts +4 -0
  32. lyrics_transcriber/lyrics/genius.py +41 -12
  33. lyrics_transcriber/output/cdg.py +33 -6
  34. lyrics_transcriber/output/cdgmaker/composer.py +839 -534
  35. lyrics_transcriber/output/video.py +17 -7
  36. lyrics_transcriber/review/server.py +22 -8
  37. {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/METADATA +3 -2
  38. {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/RECORD +41 -40
  39. {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/entry_points.txt +1 -0
  40. lyrics_transcriber/frontend/dist/assets/index-ztlAYPYT.js +0 -181
  41. lyrics_transcriber/frontend/src/components/shared/utils/newlineCalculator.ts +0 -37
  42. {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.39.0.dist-info}/LICENSE +0 -0
  43. {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: LyricsData['anchor_sequences'][0] & {
34
- position: number
44
+ data: AnchorSequence & {
45
+ wordId: string
35
46
  word?: string
36
47
  }
37
48
  } | {
38
49
  type: 'gap'
39
- data: LyricsData['gap_sequences'][0] & {
40
- position: number
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
- // On mount, try to load saved data
99
- const savedData = localStorage.getItem('lyrics_analyzer_data')
100
- if (savedData) {
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 = JSON.parse(savedData)
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
- console.log('Restored saved progress from local storage')
106
- setData(parsed)
107
- } else {
108
- // Clear old data if it's a different song
109
- localStorage.removeItem('lyrics_analyzer_data')
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
- localStorage.removeItem('lyrics_analyzer_data')
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.transcribed_text])
136
+ }, [initialData]);
117
137
 
118
138
  // Save to local storage whenever data changes
119
139
  useEffect(() => {
120
140
  if (!isReadOnly) {
121
- localStorage.setItem('lyrics_analyzer_data', JSON.stringify(data))
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
- // Add keyboard event handlers
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() // Prevent page scroll
140
- if ((window as any).toggleAudioPlayback) {
141
- (window as any).toggleAudioPlayback()
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
- let currentPosition = 0
190
- const segmentIndex = data.corrected_segments.findIndex(segment => {
191
- if (info.wordIndex >= currentPosition &&
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 (segmentIndex !== -1) {
217
+ if (segment) {
218
+ const segmentIndex = data.corrected_segments.indexOf(segment)
200
219
  setEditModalSegment({
201
- segment: data.corrected_segments[segmentIndex],
220
+ segment,
202
221
  index: segmentIndex,
203
222
  originalSegment: originalData.corrected_segments[segmentIndex]
204
223
  })
205
224
  }
206
225
  } else {
207
- // Existing word click handling for other modes...
226
+ // Update flash handling for anchors/gaps
208
227
  if (info.type === 'anchor' && info.anchor) {
209
228
  handleFlash('word', {
210
229
  type: 'anchor',
211
- transcriptionIndex: info.anchor.transcription_position,
212
- transcriptionLength: info.anchor.length,
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
- transcriptionIndex: info.gap.transcription_position,
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
- console.log('LyricsAnalyzer - handleUpdateSegment called:', {
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
- console.log('LyricsAnalyzer - Before update:', {
242
- segmentIndex: editModalSegment.index,
243
- oldText: newData.corrected_segments[editModalSegment.index].text,
244
- newText: updatedSegment.text
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) // Close the modal
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
- // Access the globally exposed seekAndPlay method
310
- if ((window as any).seekAndPlayAudio) {
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
- // Clear local storage
318
- localStorage.removeItem('lyrics_analyzer_data')
319
- // Reset data to initial state
320
- setData(JSON.parse(JSON.stringify(initialData)))
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.filter(anchor =>
362
- Object.keys(anchor.reference_positions).length > 1).length}
363
- anchorWordCount={data.anchor_sequences.reduce((sum, anchor) => sum + anchor.length, 0)}
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.filter(gap =>
366
- gap.corrections?.length > 0).length}
367
- uncorrectedGapCount={data.gap_sequences.filter(gap =>
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
- .filter(gap => !gap.corrections?.length)
396
+ ?.filter(gap => !gap.corrections?.length && gap.word_ids)
371
397
  .map(gap => ({
372
- position: gap.transcription_position,
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.reduce((count, gap) =>
377
- count + (gap.corrections?.filter(c => !c.is_deletion && !c.split_total).length ?? 0), 0)}
378
- addedCount={data.gap_sequences.reduce((count, gap) =>
379
- count + (gap.corrections?.filter(c => c.split_total).length ?? 0), 0)}
380
- deletedCount={data.gap_sequences.reduce((count, gap) =>
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?: any
30
- newValue?: any
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((segment, index) => {
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: segment.text
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
- segment.words.forEach((word, wordIndex) => {
63
- const updatedWord = updatedSegment.words[wordIndex]
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 (updatedSegment.words.length > segment.words.length) {
87
- for (let i = segment.words.length; i < updatedSegment.words.length; i++) {
88
- const word = updatedSegment.words[i]
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 (segment.text !== updatedSegment.text ||
98
- segment.start_time !== updatedSegment.start_time ||
99
- segment.end_time !== updatedSegment.end_time ||
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: `"${segment.text}" (${segment.start_time.toFixed(4)} - ${segment.end_time.toFixed(4)})`,
106
- newValue: `"${updatedSegment.text}" (${updatedSegment.start_time.toFixed(4)} - ${updatedSegment.end_time.toFixed(4)})`,
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