lyrics-transcriber 0.37.0__py3-none-any.whl → 0.40.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/correction/handlers/extend_anchor.py +13 -2
- 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/EditModal.tsx +1 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +36 -13
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +41 -1
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +48 -16
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +71 -16
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +11 -7
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +45 -12
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +83 -19
- lyrics_transcriber/frontend/src/components/shared/types.ts +3 -0
- lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +65 -9
- 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 +106 -29
- lyrics_transcriber/output/cdgmaker/composer.py +822 -528
- lyrics_transcriber/output/lrc_to_cdg.py +61 -0
- lyrics_transcriber/review/server.py +10 -12
- {lyrics_transcriber-0.37.0.dist-info → lyrics_transcriber-0.40.0.dist-info}/METADATA +3 -2
- {lyrics_transcriber-0.37.0.dist-info → lyrics_transcriber-0.40.0.dist-info}/RECORD +28 -26
- {lyrics_transcriber-0.37.0.dist-info → lyrics_transcriber-0.40.0.dist-info}/entry_points.txt +1 -0
- lyrics_transcriber/frontend/dist/assets/index-BNNbsbVN.js +0 -182
- {lyrics_transcriber-0.37.0.dist-info → lyrics_transcriber-0.40.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.37.0.dist-info → lyrics_transcriber-0.40.0.dist-info}/WHEEL +0 -0
@@ -5,7 +5,7 @@
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7
7
|
<title>Lyrics Transcriber Analyzer</title>
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
8
|
+
<script type="module" crossorigin src="/assets/index-DKnNJHRK.js"></script>
|
9
9
|
</head>
|
10
10
|
<body>
|
11
11
|
<div id="root"></div>
|
@@ -6,10 +6,11 @@
|
|
6
6
|
"type": "module",
|
7
7
|
"scripts": {
|
8
8
|
"dev": "vite",
|
9
|
-
"build": "tsc -b && vite build",
|
9
|
+
"build": "tsc -b && vite build --mode development",
|
10
|
+
"build-prod": "tsc -b && vite build",
|
10
11
|
"lint": "eslint .",
|
11
12
|
"preview": "vite preview",
|
12
|
-
"predeploy": "npm run build",
|
13
|
+
"predeploy": "npm run build-prod",
|
13
14
|
"deploy": "gh-pages -d dist"
|
14
15
|
},
|
15
16
|
"dependencies": {
|
@@ -91,7 +91,6 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
91
91
|
|
92
92
|
// Add local storage handling
|
93
93
|
useEffect(() => {
|
94
|
-
// On mount, try to load saved data
|
95
94
|
const storageKey = generateStorageKey(initialData.transcribed_text);
|
96
95
|
const savedDataStr = localStorage.getItem('lyrics_analyzer_data');
|
97
96
|
const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {};
|
@@ -99,25 +98,42 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
99
98
|
if (savedDataObj[storageKey]) {
|
100
99
|
try {
|
101
100
|
const parsed = savedDataObj[storageKey];
|
102
|
-
// Verify it's the same song (extra safety check)
|
103
101
|
if (parsed.transcribed_text === initialData.transcribed_text) {
|
104
|
-
|
105
|
-
|
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?')) {
|
106
124
|
setData(parsed);
|
107
|
-
} else {
|
108
|
-
// User declined to restore - remove the saved data
|
125
|
+
} else if (!hasChanges) {
|
109
126
|
delete savedDataObj[storageKey];
|
110
127
|
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj));
|
111
128
|
}
|
112
129
|
}
|
113
130
|
} catch (error) {
|
114
131
|
console.error('Failed to parse saved data:', error);
|
115
|
-
// Remove only this song's data
|
116
132
|
delete savedDataObj[storageKey];
|
117
133
|
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj));
|
118
134
|
}
|
119
135
|
}
|
120
|
-
}, [initialData
|
136
|
+
}, [initialData]);
|
121
137
|
|
122
138
|
// Save to local storage whenever data changes
|
123
139
|
useEffect(() => {
|
@@ -315,8 +331,15 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
315
331
|
delete savedDataObj[storageKey];
|
316
332
|
localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj));
|
317
333
|
|
318
|
-
// Reset data to initial state
|
319
|
-
|
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');
|
320
343
|
}
|
321
344
|
}, [initialData]);
|
322
345
|
|
@@ -370,10 +393,10 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
370
393
|
uncorrectedGapCount={data.gap_sequences?.filter(gap =>
|
371
394
|
!gap.corrections?.length).length ?? 0}
|
372
395
|
uncorrectedGaps={data.gap_sequences
|
373
|
-
?.filter(gap => !gap.corrections?.length)
|
396
|
+
?.filter(gap => !gap.corrections?.length && gap.word_ids)
|
374
397
|
.map(gap => ({
|
375
|
-
position: gap.word_ids[0],
|
376
|
-
length: gap.length
|
398
|
+
position: gap.word_ids?.[0] ?? '',
|
399
|
+
length: gap.length ?? 0
|
377
400
|
})) ?? []}
|
378
401
|
// Correction details
|
379
402
|
replacedCount={data.gap_sequences?.reduce((count, gap) =>
|
@@ -4,6 +4,7 @@ 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,
|
@@ -15,7 +16,8 @@ export default function ReferenceView({
|
|
15
16
|
currentSource,
|
16
17
|
onSourceChange,
|
17
18
|
highlightInfo,
|
18
|
-
mode
|
19
|
+
mode,
|
20
|
+
gaps
|
19
21
|
}: ReferenceViewProps) {
|
20
22
|
// Get available sources from referenceTexts object
|
21
23
|
const availableSources = useMemo(() =>
|
@@ -32,6 +34,42 @@ export default function ReferenceView({
|
|
32
34
|
[corrected_segments, anchors, currentSource]
|
33
35
|
)
|
34
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
|
+
|
35
73
|
return (
|
36
74
|
<Paper sx={{ p: 2 }}>
|
37
75
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
@@ -56,6 +94,8 @@ export default function ReferenceView({
|
|
56
94
|
isReference={true}
|
57
95
|
currentSource={currentSource}
|
58
96
|
linePositions={linePositions}
|
97
|
+
referenceCorrections={referenceCorrections}
|
98
|
+
gaps={gaps}
|
59
99
|
/>
|
60
100
|
</Box>
|
61
101
|
</Paper>
|
@@ -31,6 +31,35 @@ interface DiffResult {
|
|
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,27 +73,29 @@ 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',
|
67
|
-
path: `Word ${
|
98
|
+
path: `Word ${wordIndex}`,
|
68
99
|
oldValue: `"${word.text}" (${word.start_time.toFixed(4)} - ${word.end_time.toFixed(4)})`
|
69
100
|
})
|
70
101
|
return
|
@@ -75,7 +106,7 @@ export default function ReviewChangesModal({
|
|
75
106
|
Math.abs(word.end_time - updatedWord.end_time) > 0.0001) {
|
76
107
|
wordChanges.push({
|
77
108
|
type: 'modified',
|
78
|
-
path: `Word ${
|
109
|
+
path: `Word ${wordIndex}`,
|
79
110
|
oldValue: `"${word.text}" (${word.start_time.toFixed(4)} - ${word.end_time.toFixed(4)})`,
|
80
111
|
newValue: `"${updatedWord.text}" (${updatedWord.start_time.toFixed(4)} - ${updatedWord.end_time.toFixed(4)})`
|
81
112
|
})
|
@@ -83,26 +114,27 @@ export default function ReviewChangesModal({
|
|
83
114
|
})
|
84
115
|
|
85
116
|
// Check for added words
|
86
|
-
|
87
|
-
|
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]
|
88
120
|
wordChanges.push({
|
89
121
|
type: 'added',
|
90
|
-
path: `Word ${
|
122
|
+
path: `Word ${i}`,
|
91
123
|
newValue: `"${word.text}" (${word.start_time.toFixed(4)} - ${word.end_time.toFixed(4)})`
|
92
124
|
})
|
93
125
|
}
|
94
|
-
}
|
126
|
+
}
|
95
127
|
|
96
|
-
if (
|
97
|
-
|
98
|
-
|
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 ||
|
99
131
|
wordChanges.length > 0) {
|
100
132
|
diffs.push({
|
101
133
|
type: 'modified',
|
102
134
|
path: `Segment ${index}`,
|
103
135
|
segmentIndex: index,
|
104
|
-
oldValue: `"${
|
105
|
-
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)})`,
|
106
138
|
wordChanges: wordChanges.length > 0 ? wordChanges : undefined
|
107
139
|
})
|
108
140
|
}
|
@@ -8,11 +8,12 @@ interface TimelineEditorProps {
|
|
8
8
|
endTime: number
|
9
9
|
onWordUpdate: (index: number, updates: Partial<Word>) => void
|
10
10
|
currentTime?: number
|
11
|
+
onPlaySegment?: (time: number) => void
|
11
12
|
}
|
12
13
|
|
13
14
|
const TimelineContainer = styled(Box)(({ theme }) => ({
|
14
15
|
position: 'relative',
|
15
|
-
height: '
|
16
|
+
height: '80px',
|
16
17
|
backgroundColor: theme.palette.grey[200],
|
17
18
|
borderRadius: theme.shape.borderRadius,
|
18
19
|
margin: theme.spacing(2, 0),
|
@@ -24,30 +25,38 @@ const TimelineRuler = styled(Box)(({ theme }) => ({
|
|
24
25
|
top: 0,
|
25
26
|
left: 0,
|
26
27
|
right: 0,
|
27
|
-
height: '
|
28
|
+
height: '40px',
|
28
29
|
borderBottom: `1px solid ${theme.palette.grey[300]}`,
|
30
|
+
cursor: 'pointer',
|
29
31
|
}))
|
30
32
|
|
31
33
|
const TimelineMark = styled(Box)(({ theme }) => ({
|
32
34
|
position: 'absolute',
|
33
|
-
top: '
|
35
|
+
top: '20px',
|
34
36
|
width: '1px',
|
35
|
-
height: '
|
36
|
-
backgroundColor: theme.palette.grey[
|
37
|
+
height: '18px',
|
38
|
+
backgroundColor: theme.palette.grey[700],
|
39
|
+
'&.subsecond': {
|
40
|
+
top: '25px',
|
41
|
+
height: '13px',
|
42
|
+
backgroundColor: theme.palette.grey[500],
|
43
|
+
}
|
37
44
|
}))
|
38
45
|
|
39
46
|
const TimelineLabel = styled(Box)(({ theme }) => ({
|
40
47
|
position: 'absolute',
|
41
|
-
top:
|
48
|
+
top: '5px',
|
42
49
|
transform: 'translateX(-50%)',
|
43
|
-
fontSize: '0.
|
44
|
-
color: theme.palette.
|
50
|
+
fontSize: '0.8rem',
|
51
|
+
color: theme.palette.text.primary,
|
52
|
+
fontWeight: 700,
|
53
|
+
backgroundColor: theme.palette.grey[200],
|
45
54
|
}))
|
46
55
|
|
47
56
|
const TimelineWord = styled(Box)(({ theme }) => ({
|
48
57
|
position: 'absolute',
|
49
58
|
height: '30px',
|
50
|
-
top: '
|
59
|
+
top: '40px',
|
51
60
|
backgroundColor: theme.palette.primary.main,
|
52
61
|
borderRadius: theme.shape.borderRadius,
|
53
62
|
color: theme.palette.primary.contrastText,
|
@@ -57,6 +66,7 @@ const TimelineWord = styled(Box)(({ theme }) => ({
|
|
57
66
|
display: 'flex',
|
58
67
|
alignItems: 'center',
|
59
68
|
fontSize: '0.875rem',
|
69
|
+
fontFamily: 'sans-serif',
|
60
70
|
transition: 'background-color 0.1s ease',
|
61
71
|
'&.highlighted': {
|
62
72
|
backgroundColor: theme.palette.secondary.main,
|
@@ -75,7 +85,19 @@ const ResizeHandle = styled(Box)(({ theme }) => ({
|
|
75
85
|
},
|
76
86
|
}))
|
77
87
|
|
78
|
-
|
88
|
+
// Add new styled component for the cursor
|
89
|
+
const TimelineCursor = styled(Box)(({ theme }) => ({
|
90
|
+
position: 'absolute',
|
91
|
+
top: 0,
|
92
|
+
width: '2px',
|
93
|
+
height: '100%', // Full height of container
|
94
|
+
backgroundColor: theme.palette.error.main, // Red color
|
95
|
+
pointerEvents: 'none', // Ensure it doesn't interfere with clicks
|
96
|
+
transition: 'left 0.1s linear', // Smooth movement
|
97
|
+
zIndex: 1, // Ensure it's above other elements
|
98
|
+
}))
|
99
|
+
|
100
|
+
export default function TimelineEditor({ words, startTime, endTime, onWordUpdate, currentTime = 0, onPlaySegment }: TimelineEditorProps) {
|
79
101
|
const containerRef = useRef<HTMLDivElement>(null)
|
80
102
|
const [dragState, setDragState] = useState<{
|
81
103
|
wordIndex: number
|
@@ -142,15 +164,23 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
|
|
142
164
|
const startSecond = Math.floor(startTime)
|
143
165
|
const endSecond = Math.ceil(endTime)
|
144
166
|
|
145
|
-
|
167
|
+
// Generate marks for each 0.1 second interval
|
168
|
+
for (let time = startSecond; time <= endSecond; time += 0.1) {
|
146
169
|
if (time >= startTime && time <= endTime) {
|
147
170
|
const position = timeToPosition(time)
|
171
|
+
const isFullSecond = Math.abs(time - Math.round(time)) < 0.001
|
172
|
+
|
148
173
|
marks.push(
|
149
174
|
<Box key={time}>
|
150
|
-
<TimelineMark
|
151
|
-
|
152
|
-
{
|
153
|
-
|
175
|
+
<TimelineMark
|
176
|
+
className={isFullSecond ? '' : 'subsecond'}
|
177
|
+
sx={{ left: `${position}%` }}
|
178
|
+
/>
|
179
|
+
{isFullSecond && (
|
180
|
+
<TimelineLabel sx={{ left: `${position}%` }}>
|
181
|
+
{Math.round(time)}s
|
182
|
+
</TimelineLabel>
|
183
|
+
)}
|
154
184
|
</Box>
|
155
185
|
)
|
156
186
|
}
|
@@ -266,6 +296,22 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
|
|
266
296
|
return currentTime >= word.start_time && currentTime <= word.end_time
|
267
297
|
}
|
268
298
|
|
299
|
+
const handleTimelineClick = (e: React.MouseEvent) => {
|
300
|
+
const rect = containerRef.current?.getBoundingClientRect()
|
301
|
+
if (!rect || !onPlaySegment) return
|
302
|
+
|
303
|
+
const x = e.clientX - rect.left
|
304
|
+
const clickedPosition = (x / rect.width) * (endTime - startTime) + startTime
|
305
|
+
|
306
|
+
console.log('Timeline clicked:', {
|
307
|
+
x,
|
308
|
+
width: rect.width,
|
309
|
+
clickedTime: clickedPosition
|
310
|
+
})
|
311
|
+
|
312
|
+
onPlaySegment(clickedPosition)
|
313
|
+
}
|
314
|
+
|
269
315
|
return (
|
270
316
|
<TimelineContainer
|
271
317
|
ref={containerRef}
|
@@ -273,9 +319,18 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
|
|
273
319
|
onMouseUp={handleMouseUp}
|
274
320
|
onMouseLeave={handleMouseUp}
|
275
321
|
>
|
276
|
-
<TimelineRuler>
|
322
|
+
<TimelineRuler onClick={handleTimelineClick}>
|
277
323
|
{generateTimelineMarks()}
|
278
324
|
</TimelineRuler>
|
325
|
+
|
326
|
+
{/* Add cursor line */}
|
327
|
+
<TimelineCursor
|
328
|
+
sx={{
|
329
|
+
left: `${timeToPosition(currentTime)}%`,
|
330
|
+
display: currentTime >= startTime && currentTime <= endTime ? 'block' : 'none'
|
331
|
+
}}
|
332
|
+
/>
|
333
|
+
|
279
334
|
{words.map((word, index) => {
|
280
335
|
const leftPosition = timeToPosition(word.start_time)
|
281
336
|
const rightPosition = timeToPosition(word.end_time)
|
@@ -55,18 +55,21 @@ export default function TranscriptionView({
|
|
55
55
|
</Typography>
|
56
56
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
57
57
|
{data.corrected_segments.map((segment, segmentIndex) => {
|
58
|
-
// Convert segment words to TranscriptionWordPosition format
|
59
58
|
const segmentWords: TranscriptionWordPosition[] = segment.words.map(word => {
|
60
|
-
|
61
|
-
|
62
|
-
a.word_ids.includes(word.id)
|
59
|
+
const anchor = data.anchor_sequences?.find(a =>
|
60
|
+
a?.word_ids?.includes(word.id)
|
63
61
|
)
|
64
62
|
|
65
63
|
// If not in an anchor, check if it belongs to a gap sequence
|
66
|
-
const gap = !anchor ? data.gap_sequences
|
67
|
-
g
|
64
|
+
const gap = !anchor ? data.gap_sequences?.find(g =>
|
65
|
+
g?.word_ids?.includes(word.id)
|
68
66
|
) : undefined
|
69
67
|
|
68
|
+
// Check if this specific word has been corrected
|
69
|
+
const isWordCorrected = gap?.corrections?.some(
|
70
|
+
correction => correction.word_id === word.id
|
71
|
+
)
|
72
|
+
|
70
73
|
return {
|
71
74
|
word: {
|
72
75
|
id: word.id,
|
@@ -76,7 +79,8 @@ export default function TranscriptionView({
|
|
76
79
|
},
|
77
80
|
type: anchor ? 'anchor' : gap ? 'gap' : 'other',
|
78
81
|
sequence: anchor || gap,
|
79
|
-
isInRange: true
|
82
|
+
isInRange: true,
|
83
|
+
isCorrected: isWordCorrected
|
80
84
|
}
|
81
85
|
})
|
82
86
|
|
@@ -25,6 +25,8 @@ export interface HighlightedTextProps {
|
|
25
25
|
preserveSegments?: boolean
|
26
26
|
linePositions?: LinePosition[]
|
27
27
|
currentTime?: number
|
28
|
+
referenceCorrections?: Map<string, string>
|
29
|
+
gaps?: GapSequence[]
|
28
30
|
}
|
29
31
|
|
30
32
|
export function HighlightedText({
|
@@ -40,14 +42,17 @@ export function HighlightedText({
|
|
40
42
|
currentSource,
|
41
43
|
preserveSegments = false,
|
42
44
|
linePositions = [],
|
43
|
-
currentTime = 0
|
45
|
+
currentTime = 0,
|
46
|
+
referenceCorrections = new Map(),
|
47
|
+
gaps = []
|
44
48
|
}: HighlightedTextProps) {
|
45
49
|
const { handleWordClick } = useWordClick({
|
46
50
|
mode,
|
47
51
|
onElementClick,
|
48
52
|
onWordClick,
|
49
53
|
isReference,
|
50
|
-
currentSource
|
54
|
+
currentSource,
|
55
|
+
gaps
|
51
56
|
})
|
52
57
|
|
53
58
|
const shouldWordFlash = (wordPos: TranscriptionWordPosition | { word: string; id: string }): boolean => {
|
@@ -65,9 +70,14 @@ export function HighlightedText({
|
|
65
70
|
(flashingType === 'anchor' && wordPos.type === 'anchor') ||
|
66
71
|
(flashingType === 'corrected' && isCorrected) ||
|
67
72
|
(flashingType === 'uncorrected' && wordPos.type === 'gap' && !isCorrected) ||
|
68
|
-
(flashingType === 'word' &&
|
69
|
-
|
70
|
-
highlightInfo
|
73
|
+
(flashingType === 'word' && (
|
74
|
+
// Handle anchor highlighting
|
75
|
+
(highlightInfo?.type === 'anchor' && wordPos.type === 'anchor' &&
|
76
|
+
wordPos.sequence && highlightInfo.word_ids?.includes(wordPos.word.id)) ||
|
77
|
+
// Handle gap highlighting - only highlight the specific word
|
78
|
+
(highlightInfo?.type === 'gap' && wordPos.type === 'gap' &&
|
79
|
+
highlightInfo.word_ids?.includes(wordPos.word.id))
|
80
|
+
))
|
71
81
|
)
|
72
82
|
} else {
|
73
83
|
// Handle reference word
|
@@ -77,10 +87,32 @@ export function HighlightedText({
|
|
77
87
|
a?.reference_word_ids?.[currentSource]?.includes(wordPos.id)
|
78
88
|
)
|
79
89
|
|
90
|
+
// Check if this word should flash as part of a gap
|
91
|
+
const shouldFlashGap = flashingType === 'word' &&
|
92
|
+
highlightInfo?.type === 'gap' &&
|
93
|
+
// Check if this reference word corresponds to the clicked word
|
94
|
+
gaps?.some(gap =>
|
95
|
+
gap.corrections.some(correction => {
|
96
|
+
// Only flash if this correction corresponds to the clicked word
|
97
|
+
if (!highlightInfo.word_ids?.includes(correction.word_id)) {
|
98
|
+
return false;
|
99
|
+
}
|
100
|
+
|
101
|
+
const refPosition = correction.reference_positions?.[currentSource];
|
102
|
+
const wordPosition = parseInt(wordPos.id.split('-').pop() || '', 10);
|
103
|
+
return typeof refPosition === 'number' && refPosition === wordPosition;
|
104
|
+
})
|
105
|
+
)
|
106
|
+
|
80
107
|
return Boolean(
|
81
108
|
(flashingType === 'anchor' && anchor) ||
|
82
|
-
(flashingType === 'word' &&
|
83
|
-
|
109
|
+
(flashingType === 'word' && (
|
110
|
+
// Handle anchor highlighting
|
111
|
+
(highlightInfo?.type === 'anchor' &&
|
112
|
+
highlightInfo.reference_word_ids?.[currentSource]?.includes(wordPos.id)) ||
|
113
|
+
// Handle gap highlighting
|
114
|
+
shouldFlashGap
|
115
|
+
))
|
84
116
|
)
|
85
117
|
}
|
86
118
|
}
|
@@ -103,8 +135,8 @@ export function HighlightedText({
|
|
103
135
|
shouldFlash={shouldWordFlash(wordPos)}
|
104
136
|
isCurrentlyPlaying={shouldHighlightWord(wordPos)}
|
105
137
|
isAnchor={wordPos.type === 'anchor'}
|
106
|
-
isCorrectedGap={wordPos.type === 'gap' &&
|
107
|
-
isUncorrectedGap={wordPos.type === 'gap' && !
|
138
|
+
isCorrectedGap={wordPos.type === 'gap' && wordPos.isCorrected}
|
139
|
+
isUncorrectedGap={wordPos.type === 'gap' && !wordPos.isCorrected}
|
108
140
|
onClick={() => handleWordClick(
|
109
141
|
wordPos.word.text,
|
110
142
|
wordPos.word.id,
|
@@ -183,22 +215,23 @@ export function HighlightedText({
|
|
183
215
|
return <span key={`space-${lineIndex}-${wordIndex}`}> </span>
|
184
216
|
}
|
185
217
|
|
186
|
-
// Generate word ID based on position in the reference text
|
187
218
|
const wordId = `${currentSource}-word-${wordCount}`
|
188
219
|
wordCount++
|
189
220
|
|
190
|
-
// Find if this word is part of any anchor sequence
|
191
221
|
const anchor = currentSource ? anchors?.find(a =>
|
192
222
|
a?.reference_word_ids?.[currentSource]?.includes(wordId)
|
193
223
|
) : undefined
|
194
224
|
|
225
|
+
// Check if this word has a correction
|
226
|
+
const hasCorrection = referenceCorrections.has(wordId)
|
227
|
+
|
195
228
|
return (
|
196
229
|
<Word
|
197
230
|
key={wordId}
|
198
231
|
word={word}
|
199
232
|
shouldFlash={shouldWordFlash({ word, id: wordId })}
|
200
233
|
isAnchor={Boolean(anchor)}
|
201
|
-
isCorrectedGap={
|
234
|
+
isCorrectedGap={hasCorrection}
|
202
235
|
isUncorrectedGap={false}
|
203
236
|
onClick={() => handleWordClick(word, wordId, anchor, undefined)}
|
204
237
|
/>
|