lyrics-transcriber 0.36.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.
Files changed (35) 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 +9 -0
  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/frontend/dist/assets/index-BNNbsbVN.js +182 -0
  10. lyrics_transcriber/frontend/dist/index.html +1 -1
  11. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +1 -2
  12. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +76 -70
  13. lyrics_transcriber/frontend/src/components/EditModal.tsx +10 -2
  14. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +128 -125
  15. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -3
  16. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +24 -12
  17. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +8 -15
  18. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +3 -3
  19. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +34 -52
  20. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +39 -31
  21. lyrics_transcriber/frontend/src/components/shared/types.ts +3 -3
  22. lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +146 -0
  23. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +23 -24
  24. lyrics_transcriber/frontend/src/types.ts +25 -15
  25. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  26. lyrics_transcriber/output/cdg.py +32 -6
  27. lyrics_transcriber/output/video.py +17 -7
  28. lyrics_transcriber/review/server.py +24 -8
  29. {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/METADATA +1 -1
  30. {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/RECORD +33 -33
  31. {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/entry_points.txt +1 -0
  32. lyrics_transcriber/frontend/dist/assets/index-ztlAYPYT.js +0 -181
  33. lyrics_transcriber/frontend/src/components/shared/utils/newlineCalculator.ts +0 -37
  34. {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/LICENSE +0 -0
  35. {lyrics_transcriber-0.36.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,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,62 @@ 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
94
  // On mount, try to load saved data
99
- const savedData = localStorage.getItem('lyrics_analyzer_data')
100
- if (savedData) {
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]) {
101
100
  try {
102
- const parsed = JSON.parse(savedData)
103
- // Only restore if it's the same song (matching transcribed text)
101
+ const parsed = savedDataObj[storageKey];
102
+ // Verify it's the same song (extra safety check)
104
103
  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')
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
+ }
110
112
  }
111
113
  } catch (error) {
112
- console.error('Failed to parse saved data:', error)
113
- localStorage.removeItem('lyrics_analyzer_data')
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));
114
118
  }
115
119
  }
116
- }, [initialData.transcribed_text])
120
+ }, [initialData.transcribed_text]);
117
121
 
118
122
  // Save to local storage whenever data changes
119
123
  useEffect(() => {
120
124
  if (!isReadOnly) {
121
- localStorage.setItem('lyrics_analyzer_data', JSON.stringify(data))
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));
122
131
  }
123
- }, [data, isReadOnly])
132
+ }, [data, isReadOnly, initialData.transcribed_text]);
124
133
 
125
- // Add keyboard event handlers
134
+ // Update keyboard event handler
126
135
  useEffect(() => {
127
136
  const handleKeyDown = (e: KeyboardEvent) => {
128
- // Ignore if user is typing in an input or textarea
129
137
  if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
130
138
  return
131
139
  }
@@ -136,9 +144,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
136
144
  } else if (e.key === 'Meta') {
137
145
  setIsCtrlPressed(true)
138
146
  } else if (e.key === ' ' || e.code === 'Space') {
139
- e.preventDefault() // Prevent page scroll
140
- if ((window as any).toggleAudioPlayback) {
141
- (window as any).toggleAudioPlayback()
147
+ e.preventDefault()
148
+ if (window.toggleAudioPlayback) {
149
+ window.toggleAudioPlayback()
142
150
  }
143
151
  }
144
152
  }
@@ -186,63 +194,45 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
186
194
 
187
195
  const handleWordClick = useCallback((info: WordClickInfo) => {
188
196
  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
- })
197
+ const segment = data.corrected_segments.find(segment =>
198
+ segment.words.some(word => word.id === info.word_id)
199
+ )
198
200
 
199
- if (segmentIndex !== -1) {
201
+ if (segment) {
202
+ const segmentIndex = data.corrected_segments.indexOf(segment)
200
203
  setEditModalSegment({
201
- segment: data.corrected_segments[segmentIndex],
204
+ segment,
202
205
  index: segmentIndex,
203
206
  originalSegment: originalData.corrected_segments[segmentIndex]
204
207
  })
205
208
  }
206
209
  } else {
207
- // Existing word click handling for other modes...
210
+ // Update flash handling for anchors/gaps
208
211
  if (info.type === 'anchor' && info.anchor) {
209
212
  handleFlash('word', {
210
213
  type: 'anchor',
211
- transcriptionIndex: info.anchor.transcription_position,
212
- transcriptionLength: info.anchor.length,
213
- referenceIndices: info.anchor.reference_positions,
214
- referenceLength: info.anchor.length
214
+ word_ids: info.anchor.word_ids,
215
+ reference_word_ids: info.anchor.reference_word_ids
215
216
  })
216
217
  } else if (info.type === 'gap' && info.gap) {
217
218
  handleFlash('word', {
218
219
  type: 'gap',
219
- transcriptionIndex: info.gap.transcription_position,
220
- transcriptionLength: info.gap.length,
221
- referenceIndices: {},
222
- referenceLength: info.gap.length
220
+ word_ids: info.gap.word_ids
223
221
  })
224
222
  }
225
223
  }
226
224
  }, [effectiveMode, data.corrected_segments, handleFlash, originalData.corrected_segments])
227
225
 
228
226
  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
- }
227
+ if (!editModalSegment) return
239
228
 
240
229
  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
- })
230
+
231
+ // Ensure new words have IDs
232
+ updatedSegment.words = updatedSegment.words.map(word => ({
233
+ ...word,
234
+ id: word.id || nanoid()
235
+ }))
246
236
 
247
237
  newData.corrected_segments[editModalSegment.index] = updatedSegment
248
238
 
@@ -251,34 +241,37 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
251
241
  .map(segment => segment.text)
252
242
  .join('\n')
253
243
 
254
- console.log('LyricsAnalyzer - After update:', {
255
- segmentsCount: newData.corrected_segments.length,
256
- updatedText: newData.corrected_text
257
- })
258
-
259
244
  setData(newData)
260
- setEditModalSegment(null) // Close the modal
245
+ setEditModalSegment(null)
261
246
  }, [data, editModalSegment])
262
247
 
263
248
  const handleDeleteSegment = useCallback((segmentIndex: number) => {
264
- console.log('LyricsAnalyzer - handleDeleteSegment called:', {
265
- segmentIndex,
266
- currentSegmentsCount: data.corrected_segments.length
267
- })
268
-
269
249
  const newData = { ...data }
250
+ const deletedSegment = newData.corrected_segments[segmentIndex]
251
+
252
+ // Remove segment
270
253
  newData.corrected_segments = newData.corrected_segments.filter((_, index) => index !== segmentIndex)
271
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
+
272
270
  // Update corrected_text
273
271
  newData.corrected_text = newData.corrected_segments
274
272
  .map(segment => segment.text)
275
273
  .join('\n')
276
274
 
277
- console.log('LyricsAnalyzer - After delete:', {
278
- segmentsCount: newData.corrected_segments.length,
279
- updatedText: newData.corrected_text
280
- })
281
-
282
275
  setData(newData)
283
276
  }, [data])
284
277
 
@@ -305,21 +298,27 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
305
298
  }
306
299
  }, [apiClient, data])
307
300
 
301
+ // Update play segment handler
308
302
  const handlePlaySegment = useCallback((startTime: number) => {
309
- // Access the globally exposed seekAndPlay method
310
- if ((window as any).seekAndPlayAudio) {
311
- (window as any).seekAndPlayAudio(startTime)
303
+ if (window.seekAndPlayAudio) {
304
+ window.seekAndPlayAudio(startTime)
312
305
  }
313
306
  }, [])
314
307
 
315
308
  const handleResetCorrections = useCallback(() => {
316
309
  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')
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
+
319
318
  // Reset data to initial state
320
- setData(JSON.parse(JSON.stringify(initialData)))
319
+ setData(JSON.parse(JSON.stringify(initialData)));
321
320
  }
322
- }, [initialData])
321
+ }, [initialData]);
323
322
 
324
323
  return (
325
324
  <Box>
@@ -358,27 +357,31 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
358
357
  <CorrectionMetrics
359
358
  // Anchor metrics
360
359
  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)}
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}
364
367
  // 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}
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}
369
372
  uncorrectedGaps={data.gap_sequences
370
- .filter(gap => !gap.corrections?.length)
373
+ ?.filter(gap => !gap.corrections?.length)
371
374
  .map(gap => ({
372
- position: gap.transcription_position,
375
+ position: gap.word_ids[0],
373
376
  length: gap.length
374
- }))}
377
+ })) ?? []}
375
378
  // 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)}
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}
382
385
  onMetricClick={{
383
386
  anchor: () => handleFlash('anchor'),
384
387
  corrected: () => handleFlash('corrected'),
@@ -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,
@@ -19,7 +18,7 @@ export default function ReferenceView({
19
18
  mode
20
19
  }: ReferenceViewProps) {
21
20
  // Get available sources from referenceTexts object
22
- const availableSources = useMemo(() =>
21
+ const availableSources = useMemo(() =>
23
22
  Object.keys(referenceTexts) as Array<string>,
24
23
  [referenceTexts]
25
24
  )
@@ -49,7 +48,6 @@ export default function ReferenceView({
49
48
  <HighlightedText
50
49
  text={referenceTexts[currentSource]}
51
50
  anchors={anchors}
52
- gaps={gaps}
53
51
  onElementClick={onElementClick}
54
52
  onWordClick={onWordClick}
55
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?: any
30
- newValue?: any
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, wordIndex) => {
63
- const updatedWord = updatedSegment.words[wordIndex]
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 ${wordIndex}`,
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 ${wordIndex}`,
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
- 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]
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 ${i}`,
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((word, idx) => {
63
- const position = globalWordPosition + idx
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
- position >= a.transcription_position &&
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
- position >= g.transcription_position &&
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={segmentIndex} sx={{ display: 'flex', alignItems: 'flex-start', width: '100%' }}>
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?: (position: number, updatedWords: string[]) => void
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.position, [])
50
+ onUpdateCorrection(content.data.wordId, [])
51
51
  onClose()
52
52
  }
53
53
 
54
54
  const handleSaveEdit = () => {
55
55
  if (onUpdateCorrection) {
56
- onUpdateCorrection(content.data.position, [editedWord])
56
+ onUpdateCorrection(content.data.wordId, [editedWord])
57
57
  }
58
58
  onClose()
59
59
  }