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.
- 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 +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/CorrectionMetrics.tsx +1 -2
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +76 -70
- lyrics_transcriber/frontend/src/components/EditModal.tsx +10 -2
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +128 -125
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -3
- 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 +34 -52
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +39 -31
- lyrics_transcriber/frontend/src/components/shared/types.ts +3 -3
- lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +146 -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/output/cdg.py +32 -6
- lyrics_transcriber/output/video.py +17 -7
- lyrics_transcriber/review/server.py +24 -8
- {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/METADATA +1 -1
- {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.37.0.dist-info}/RECORD +33 -33
- {lyrics_transcriber-0.36.1.dist-info → lyrics_transcriber-0.37.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.37.0.dist-info}/LICENSE +0 -0
- {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:
|
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,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
|
100
|
-
|
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 =
|
103
|
-
//
|
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
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
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
|
-
|
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
|
-
//
|
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()
|
140
|
-
if (
|
141
|
-
|
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
|
-
|
190
|
-
|
191
|
-
|
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 (
|
201
|
+
if (segment) {
|
202
|
+
const segmentIndex = data.corrected_segments.indexOf(segment)
|
200
203
|
setEditModalSegment({
|
201
|
-
segment
|
204
|
+
segment,
|
202
205
|
index: segmentIndex,
|
203
206
|
originalSegment: originalData.corrected_segments[segmentIndex]
|
204
207
|
})
|
205
208
|
}
|
206
209
|
} else {
|
207
|
-
//
|
210
|
+
// Update flash handling for anchors/gaps
|
208
211
|
if (info.type === 'anchor' && info.anchor) {
|
209
212
|
handleFlash('word', {
|
210
213
|
type: 'anchor',
|
211
|
-
|
212
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
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)
|
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
|
-
|
310
|
-
|
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
|
-
|
318
|
-
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
|
+
|
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
|
362
|
-
|
363
|
-
|
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
|
366
|
-
gap.corrections?.length > 0).length}
|
367
|
-
uncorrectedGapCount={data.gap_sequences
|
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
|
-
|
373
|
+
?.filter(gap => !gap.corrections?.length)
|
371
374
|
.map(gap => ({
|
372
|
-
position: gap.
|
375
|
+
position: gap.word_ids[0],
|
373
376
|
length: gap.length
|
374
|
-
}))}
|
377
|
+
})) ?? []}
|
375
378
|
// 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)}
|
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?:
|
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
|
}
|