karaoke-gen 0.76.20__py3-none-any.whl → 0.82.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.
- karaoke_gen/instrumental_review/static/index.html +179 -16
- karaoke_gen/karaoke_gen.py +5 -4
- karaoke_gen/lyrics_processor.py +25 -6
- {karaoke_gen-0.76.20.dist-info → karaoke_gen-0.82.0.dist-info}/METADATA +79 -3
- {karaoke_gen-0.76.20.dist-info → karaoke_gen-0.82.0.dist-info}/RECORD +33 -31
- lyrics_transcriber/core/config.py +8 -0
- lyrics_transcriber/core/controller.py +43 -1
- lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +178 -5
- lyrics_transcriber/correction/agentic/prompts/__init__.py +23 -0
- lyrics_transcriber/correction/agentic/prompts/classifier.py +66 -6
- lyrics_transcriber/correction/agentic/prompts/langfuse_prompts.py +298 -0
- lyrics_transcriber/correction/agentic/providers/config.py +7 -0
- lyrics_transcriber/correction/agentic/providers/constants.py +1 -1
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +22 -7
- lyrics_transcriber/correction/agentic/providers/model_factory.py +28 -13
- lyrics_transcriber/correction/agentic/router.py +18 -13
- lyrics_transcriber/correction/corrector.py +1 -45
- lyrics_transcriber/frontend/.gitignore +1 -0
- lyrics_transcriber/frontend/e2e/agentic-corrections.spec.ts +207 -0
- lyrics_transcriber/frontend/e2e/fixtures/agentic-correction-data.json +226 -0
- lyrics_transcriber/frontend/package.json +4 -1
- lyrics_transcriber/frontend/playwright.config.ts +1 -1
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +34 -30
- lyrics_transcriber/frontend/src/components/Header.tsx +141 -34
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +120 -3
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +11 -1
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +122 -35
- lyrics_transcriber/frontend/src/components/shared/types.ts +6 -0
- lyrics_transcriber/output/generator.py +50 -3
- lyrics_transcriber/transcribers/local_whisper.py +260 -0
- lyrics_transcriber/correction/handlers/llm.py +0 -293
- lyrics_transcriber/correction/handlers/llm_providers.py +0 -60
- {karaoke_gen-0.76.20.dist-info → karaoke_gen-0.82.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.76.20.dist-info → karaoke_gen-0.82.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.76.20.dist-info → karaoke_gen-0.82.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, Button, Typography, useMediaQuery, useTheme, Switch, FormControlLabel, Tooltip, Paper, IconButton } from '@mui/material'
|
|
1
|
+
import { Box, Button, Typography, useMediaQuery, useTheme, Switch, FormControlLabel, Tooltip, Paper, IconButton, Chip } from '@mui/material'
|
|
2
2
|
import LockIcon from '@mui/icons-material/Lock'
|
|
3
3
|
import UploadFileIcon from '@mui/icons-material/UploadFile'
|
|
4
4
|
import FindReplaceIcon from '@mui/icons-material/FindReplace'
|
|
@@ -8,6 +8,9 @@ import RedoIcon from '@mui/icons-material/Redo'
|
|
|
8
8
|
import TimerIcon from '@mui/icons-material/Timer'
|
|
9
9
|
import RestoreIcon from '@mui/icons-material/Restore'
|
|
10
10
|
import RateReviewIcon from '@mui/icons-material/RateReview'
|
|
11
|
+
import VisibilityIcon from '@mui/icons-material/Visibility'
|
|
12
|
+
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
|
13
|
+
import HighlightOffIcon from '@mui/icons-material/HighlightOff'
|
|
11
14
|
import { CorrectionData, InteractionMode } from '../types'
|
|
12
15
|
import CorrectionMetrics from './CorrectionMetrics'
|
|
13
16
|
import AgenticCorrectionMetrics from './AgenticCorrectionMetrics'
|
|
@@ -44,6 +47,13 @@ interface HeaderProps {
|
|
|
44
47
|
canRedo: boolean
|
|
45
48
|
annotationsEnabled?: boolean
|
|
46
49
|
onAnnotationsToggle?: (enabled: boolean) => void
|
|
50
|
+
// Review mode props
|
|
51
|
+
reviewMode?: boolean
|
|
52
|
+
onReviewModeToggle?: (enabled: boolean) => void
|
|
53
|
+
// Batch action props
|
|
54
|
+
onAcceptAllCorrections?: () => void
|
|
55
|
+
onAcceptHighConfidenceCorrections?: () => void
|
|
56
|
+
onRevertAllCorrections?: () => void
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
export default function Header({
|
|
@@ -70,6 +80,11 @@ export default function Header({
|
|
|
70
80
|
canRedo,
|
|
71
81
|
annotationsEnabled = true,
|
|
72
82
|
onAnnotationsToggle,
|
|
83
|
+
reviewMode = false,
|
|
84
|
+
onReviewModeToggle,
|
|
85
|
+
onAcceptAllCorrections,
|
|
86
|
+
onAcceptHighConfidenceCorrections,
|
|
87
|
+
onRevertAllCorrections,
|
|
73
88
|
}: HeaderProps) {
|
|
74
89
|
const theme = useTheme()
|
|
75
90
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
|
@@ -129,19 +144,69 @@ export default function Header({
|
|
|
129
144
|
</Box>
|
|
130
145
|
)}
|
|
131
146
|
|
|
132
|
-
{
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
<Box sx={{
|
|
148
|
+
display: 'flex',
|
|
149
|
+
flexDirection: isMobile ? 'column' : 'row',
|
|
150
|
+
gap: 1,
|
|
151
|
+
justifyContent: 'space-between',
|
|
152
|
+
alignItems: isMobile ? 'stretch' : 'center',
|
|
153
|
+
mb: 1
|
|
154
|
+
}}>
|
|
155
|
+
<Typography variant="h4" sx={{ fontSize: isMobile ? '1.3rem' : '1.5rem' }}>
|
|
156
|
+
Nomad Karaoke: Lyrics Transcription Review
|
|
157
|
+
</Typography>
|
|
158
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
159
|
+
{!isReadOnly && isAgenticMode && onReviewModeToggle && (
|
|
160
|
+
<Tooltip title={reviewMode
|
|
161
|
+
? "Hide inline correction actions"
|
|
162
|
+
: "Show inline actions on all corrections for quick review"
|
|
163
|
+
}>
|
|
164
|
+
<Chip
|
|
165
|
+
icon={<VisibilityIcon />}
|
|
166
|
+
label={reviewMode ? "Review Mode" : "Review Off"}
|
|
167
|
+
onClick={() => onReviewModeToggle(!reviewMode)}
|
|
168
|
+
color={reviewMode ? "secondary" : "default"}
|
|
169
|
+
variant={reviewMode ? "filled" : "outlined"}
|
|
170
|
+
size="small"
|
|
171
|
+
sx={{
|
|
172
|
+
cursor: 'pointer',
|
|
173
|
+
'& .MuiChip-icon': { fontSize: '1rem' }
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
</Tooltip>
|
|
177
|
+
)}
|
|
178
|
+
{!isReadOnly && onAnnotationsToggle && (
|
|
179
|
+
<Tooltip title={annotationsEnabled
|
|
180
|
+
? "Click to disable annotation prompts when editing"
|
|
181
|
+
: "Click to enable annotation prompts when editing"
|
|
182
|
+
}>
|
|
183
|
+
<Chip
|
|
184
|
+
icon={<RateReviewIcon />}
|
|
185
|
+
label={annotationsEnabled ? "Feedback On" : "Feedback Off"}
|
|
186
|
+
onClick={() => onAnnotationsToggle(!annotationsEnabled)}
|
|
187
|
+
color={annotationsEnabled ? "primary" : "default"}
|
|
188
|
+
variant={annotationsEnabled ? "filled" : "outlined"}
|
|
189
|
+
size="small"
|
|
190
|
+
sx={{
|
|
191
|
+
cursor: 'pointer',
|
|
192
|
+
'& .MuiChip-icon': { fontSize: '1rem' }
|
|
193
|
+
}}
|
|
194
|
+
/>
|
|
195
|
+
</Tooltip>
|
|
196
|
+
)}
|
|
197
|
+
{isReadOnly && (
|
|
198
|
+
<Button
|
|
199
|
+
variant="outlined"
|
|
200
|
+
size="small"
|
|
201
|
+
startIcon={<UploadFileIcon />}
|
|
202
|
+
onClick={onFileLoad}
|
|
203
|
+
fullWidth={isMobile}
|
|
204
|
+
>
|
|
205
|
+
Load File
|
|
206
|
+
</Button>
|
|
207
|
+
)}
|
|
143
208
|
</Box>
|
|
144
|
-
|
|
209
|
+
</Box>
|
|
145
210
|
|
|
146
211
|
<Box sx={{
|
|
147
212
|
display: 'flex',
|
|
@@ -250,6 +315,65 @@ export default function Header({
|
|
|
250
315
|
</Box>
|
|
251
316
|
</Box>
|
|
252
317
|
|
|
318
|
+
{/* Batch Actions Panel - shown when review mode is enabled */}
|
|
319
|
+
{!isReadOnly && reviewMode && isAgenticMode && data.corrections?.length > 0 && (
|
|
320
|
+
<Paper sx={{ p: 0.8, mb: 1, backgroundColor: 'action.hover' }}>
|
|
321
|
+
<Box sx={{
|
|
322
|
+
display: 'flex',
|
|
323
|
+
flexDirection: isMobile ? 'column' : 'row',
|
|
324
|
+
gap: 1,
|
|
325
|
+
alignItems: isMobile ? 'stretch' : 'center',
|
|
326
|
+
justifyContent: 'space-between'
|
|
327
|
+
}}>
|
|
328
|
+
<Typography variant="subtitle2" sx={{ fontSize: '0.75rem', color: 'text.secondary' }}>
|
|
329
|
+
Batch Actions ({data.corrections.length} corrections)
|
|
330
|
+
</Typography>
|
|
331
|
+
<Box sx={{
|
|
332
|
+
display: 'flex',
|
|
333
|
+
gap: 1,
|
|
334
|
+
flexWrap: 'wrap'
|
|
335
|
+
}}>
|
|
336
|
+
{onAcceptHighConfidenceCorrections && (
|
|
337
|
+
<Button
|
|
338
|
+
variant="contained"
|
|
339
|
+
size="small"
|
|
340
|
+
color="success"
|
|
341
|
+
startIcon={<CheckCircleIcon />}
|
|
342
|
+
onClick={onAcceptHighConfidenceCorrections}
|
|
343
|
+
sx={{ fontSize: '0.75rem', py: 0.5 }}
|
|
344
|
+
>
|
|
345
|
+
Accept High Confidence ({data.corrections.filter(c => c.confidence >= 0.8).length})
|
|
346
|
+
</Button>
|
|
347
|
+
)}
|
|
348
|
+
{onAcceptAllCorrections && (
|
|
349
|
+
<Button
|
|
350
|
+
variant="outlined"
|
|
351
|
+
size="small"
|
|
352
|
+
color="success"
|
|
353
|
+
startIcon={<CheckCircleIcon />}
|
|
354
|
+
onClick={onAcceptAllCorrections}
|
|
355
|
+
sx={{ fontSize: '0.75rem', py: 0.5 }}
|
|
356
|
+
>
|
|
357
|
+
Accept All
|
|
358
|
+
</Button>
|
|
359
|
+
)}
|
|
360
|
+
{onRevertAllCorrections && (
|
|
361
|
+
<Button
|
|
362
|
+
variant="outlined"
|
|
363
|
+
size="small"
|
|
364
|
+
color="error"
|
|
365
|
+
startIcon={<HighlightOffIcon />}
|
|
366
|
+
onClick={onRevertAllCorrections}
|
|
367
|
+
sx={{ fontSize: '0.75rem', py: 0.5 }}
|
|
368
|
+
>
|
|
369
|
+
Revert All
|
|
370
|
+
</Button>
|
|
371
|
+
)}
|
|
372
|
+
</Box>
|
|
373
|
+
</Box>
|
|
374
|
+
</Paper>
|
|
375
|
+
)}
|
|
376
|
+
|
|
253
377
|
<Paper sx={{ p: 0.8, mb: 1 }}>
|
|
254
378
|
<Box sx={{
|
|
255
379
|
display: 'flex',
|
|
@@ -356,10 +480,10 @@ export default function Header({
|
|
|
356
480
|
Timing Offset
|
|
357
481
|
</Button>
|
|
358
482
|
{timingOffsetMs !== 0 && (
|
|
359
|
-
<Typography
|
|
360
|
-
variant="body2"
|
|
361
|
-
sx={{
|
|
362
|
-
ml: 1,
|
|
483
|
+
<Typography
|
|
484
|
+
variant="body2"
|
|
485
|
+
sx={{
|
|
486
|
+
ml: 1,
|
|
363
487
|
fontWeight: 'bold',
|
|
364
488
|
color: theme.palette.secondary.main
|
|
365
489
|
}}
|
|
@@ -369,23 +493,6 @@ export default function Header({
|
|
|
369
493
|
)}
|
|
370
494
|
</Box>
|
|
371
495
|
)}
|
|
372
|
-
{!isReadOnly && onAnnotationsToggle && (
|
|
373
|
-
<Tooltip title={annotationsEnabled
|
|
374
|
-
? "Click to disable annotation prompts when editing"
|
|
375
|
-
: "Click to enable annotation prompts when editing"
|
|
376
|
-
}>
|
|
377
|
-
<Button
|
|
378
|
-
variant="outlined"
|
|
379
|
-
size="small"
|
|
380
|
-
onClick={() => onAnnotationsToggle(!annotationsEnabled)}
|
|
381
|
-
startIcon={<RateReviewIcon />}
|
|
382
|
-
color={annotationsEnabled ? "primary" : "inherit"}
|
|
383
|
-
sx={{ minWidth: 'fit-content', height: '32px' }}
|
|
384
|
-
>
|
|
385
|
-
{annotationsEnabled ? "Feedback On" : "Feedback Off"}
|
|
386
|
-
</Button>
|
|
387
|
-
</Tooltip>
|
|
388
|
-
)}
|
|
389
496
|
<AudioPlayer
|
|
390
497
|
apiClient={apiClient}
|
|
391
498
|
onTimeUpdate={onTimeUpdate}
|
|
@@ -88,6 +88,12 @@ interface MemoizedTranscriptionViewProps {
|
|
|
88
88
|
anchors: AnchorSequence[]
|
|
89
89
|
disableHighlighting: boolean
|
|
90
90
|
onDataChange?: (updatedData: CorrectionData) => void
|
|
91
|
+
// Review mode props
|
|
92
|
+
reviewMode: boolean
|
|
93
|
+
onRevertCorrection: (wordId: string) => void
|
|
94
|
+
onEditCorrection: (wordId: string) => void
|
|
95
|
+
onAcceptCorrection: (wordId: string) => void
|
|
96
|
+
onShowCorrectionDetail: (wordId: string) => void
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
// Create a memoized TranscriptionView component
|
|
@@ -103,7 +109,12 @@ const MemoizedTranscriptionView = memo(function MemoizedTranscriptionView({
|
|
|
103
109
|
currentTime,
|
|
104
110
|
anchors,
|
|
105
111
|
disableHighlighting,
|
|
106
|
-
onDataChange
|
|
112
|
+
onDataChange,
|
|
113
|
+
reviewMode,
|
|
114
|
+
onRevertCorrection,
|
|
115
|
+
onEditCorrection,
|
|
116
|
+
onAcceptCorrection,
|
|
117
|
+
onShowCorrectionDetail
|
|
107
118
|
}: MemoizedTranscriptionViewProps) {
|
|
108
119
|
return (
|
|
109
120
|
<TranscriptionView
|
|
@@ -118,6 +129,11 @@ const MemoizedTranscriptionView = memo(function MemoizedTranscriptionView({
|
|
|
118
129
|
currentTime={disableHighlighting ? undefined : currentTime}
|
|
119
130
|
anchors={anchors}
|
|
120
131
|
onDataChange={onDataChange}
|
|
132
|
+
reviewMode={reviewMode}
|
|
133
|
+
onRevertCorrection={onRevertCorrection}
|
|
134
|
+
onEditCorrection={onEditCorrection}
|
|
135
|
+
onAcceptCorrection={onAcceptCorrection}
|
|
136
|
+
onShowCorrectionDetail={onShowCorrectionDetail}
|
|
121
137
|
/>
|
|
122
138
|
);
|
|
123
139
|
});
|
|
@@ -202,6 +218,13 @@ interface MemoizedHeaderProps {
|
|
|
202
218
|
onUnCorrectAll: () => void
|
|
203
219
|
annotationsEnabled: boolean
|
|
204
220
|
onAnnotationsToggle: (enabled: boolean) => void
|
|
221
|
+
// Review mode props
|
|
222
|
+
reviewMode: boolean
|
|
223
|
+
onReviewModeToggle: (enabled: boolean) => void
|
|
224
|
+
// Batch action props
|
|
225
|
+
onAcceptAllCorrections: () => void
|
|
226
|
+
onAcceptHighConfidenceCorrections: () => void
|
|
227
|
+
onRevertAllCorrections: () => void
|
|
205
228
|
}
|
|
206
229
|
|
|
207
230
|
// Create a memoized Header component
|
|
@@ -228,7 +251,12 @@ const MemoizedHeader = memo(function MemoizedHeader({
|
|
|
228
251
|
canRedo,
|
|
229
252
|
onUnCorrectAll,
|
|
230
253
|
annotationsEnabled,
|
|
231
|
-
onAnnotationsToggle
|
|
254
|
+
onAnnotationsToggle,
|
|
255
|
+
reviewMode,
|
|
256
|
+
onReviewModeToggle,
|
|
257
|
+
onAcceptAllCorrections,
|
|
258
|
+
onAcceptHighConfidenceCorrections,
|
|
259
|
+
onRevertAllCorrections
|
|
232
260
|
}: MemoizedHeaderProps) {
|
|
233
261
|
return (
|
|
234
262
|
<Header
|
|
@@ -255,6 +283,11 @@ const MemoizedHeader = memo(function MemoizedHeader({
|
|
|
255
283
|
onUnCorrectAll={onUnCorrectAll}
|
|
256
284
|
annotationsEnabled={annotationsEnabled}
|
|
257
285
|
onAnnotationsToggle={onAnnotationsToggle}
|
|
286
|
+
reviewMode={reviewMode}
|
|
287
|
+
onReviewModeToggle={onReviewModeToggle}
|
|
288
|
+
onAcceptAllCorrections={onAcceptAllCorrections}
|
|
289
|
+
onAcceptHighConfidenceCorrections={onAcceptHighConfidenceCorrections}
|
|
290
|
+
onRevertAllCorrections={onRevertAllCorrections}
|
|
258
291
|
/>
|
|
259
292
|
);
|
|
260
293
|
});
|
|
@@ -291,6 +324,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
|
291
324
|
const [isFindReplaceModalOpen, setIsFindReplaceModalOpen] = useState(false)
|
|
292
325
|
const [isTimingOffsetModalOpen, setIsTimingOffsetModalOpen] = useState(false)
|
|
293
326
|
const [timingOffsetMs, setTimingOffsetMs] = useState(0)
|
|
327
|
+
|
|
328
|
+
// Review mode state for agentic corrections
|
|
329
|
+
const [reviewMode, setReviewMode] = useState(false)
|
|
294
330
|
|
|
295
331
|
// Annotation collection state
|
|
296
332
|
const [annotations, setAnnotations] = useState<Omit<CorrectionAnnotation, 'annotation_id' | 'timestamp'>[]>([])
|
|
@@ -793,11 +829,82 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
|
793
829
|
// For now, just log acceptance
|
|
794
830
|
// In the future, this could be tracked in the annotation system
|
|
795
831
|
console.log('Accepted correction for word:', wordId)
|
|
796
|
-
|
|
832
|
+
|
|
797
833
|
// TODO: Track acceptance in annotation system
|
|
798
834
|
// This could be used to build confidence in the AI's corrections over time
|
|
799
835
|
}, [])
|
|
800
836
|
|
|
837
|
+
// Batch action handlers for review mode
|
|
838
|
+
const handleAcceptAllCorrections = useCallback(() => {
|
|
839
|
+
// Accept all corrections - for now just log, could track in annotation system
|
|
840
|
+
data.corrections?.forEach(c => {
|
|
841
|
+
console.log('Batch accepted correction:', c.corrected_word_id || c.word_id)
|
|
842
|
+
})
|
|
843
|
+
console.log(`Accepted all ${data.corrections?.length || 0} corrections`)
|
|
844
|
+
}, [data.corrections])
|
|
845
|
+
|
|
846
|
+
const handleAcceptHighConfidenceCorrections = useCallback(() => {
|
|
847
|
+
// Accept corrections with confidence >= 80%
|
|
848
|
+
const highConfidence = data.corrections?.filter(c => c.confidence >= 0.8) || []
|
|
849
|
+
highConfidence.forEach(c => {
|
|
850
|
+
console.log('Batch accepted high-confidence correction:', c.corrected_word_id || c.word_id)
|
|
851
|
+
})
|
|
852
|
+
console.log(`Accepted ${highConfidence.length} high-confidence corrections`)
|
|
853
|
+
}, [data.corrections])
|
|
854
|
+
|
|
855
|
+
const handleRevertAllCorrections = useCallback(() => {
|
|
856
|
+
if (!window.confirm(`Are you sure you want to revert all ${data.corrections?.length || 0} corrections? This cannot be undone.`)) {
|
|
857
|
+
return
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Revert all corrections by reverting each one
|
|
861
|
+
// Process in reverse order to avoid ID conflicts
|
|
862
|
+
const corrections = [...(data.corrections || [])].reverse()
|
|
863
|
+
|
|
864
|
+
let newData = data
|
|
865
|
+
for (const correction of corrections) {
|
|
866
|
+
const wordId = correction.corrected_word_id || correction.word_id
|
|
867
|
+
|
|
868
|
+
// Find the segment containing the corrected word
|
|
869
|
+
const segmentIndex = newData.corrected_segments.findIndex(segment =>
|
|
870
|
+
segment.words.some(w => w.id === wordId)
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
if (segmentIndex === -1) continue
|
|
874
|
+
|
|
875
|
+
const segment = newData.corrected_segments[segmentIndex]
|
|
876
|
+
|
|
877
|
+
// Replace the corrected word with the original
|
|
878
|
+
const newWords = segment.words.map(word => {
|
|
879
|
+
if (word.id === wordId) {
|
|
880
|
+
return {
|
|
881
|
+
...word,
|
|
882
|
+
text: correction.original_word,
|
|
883
|
+
id: correction.word_id
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return word
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
const newText = newWords.map(w => w.text).join(' ')
|
|
890
|
+
const newSegment = { ...segment, words: newWords, text: newText }
|
|
891
|
+
const newSegments = newData.corrected_segments.map((seg, idx) =>
|
|
892
|
+
idx === segmentIndex ? newSegment : seg
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
newData = {
|
|
896
|
+
...newData,
|
|
897
|
+
corrected_segments: newSegments,
|
|
898
|
+
corrections: newData.corrections?.filter(c =>
|
|
899
|
+
c.corrected_word_id !== wordId && c.word_id !== wordId
|
|
900
|
+
) || []
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
updateDataWithHistory(newData, 'revert all corrections')
|
|
905
|
+
console.log(`Reverted all ${corrections.length} corrections`)
|
|
906
|
+
}, [data, updateDataWithHistory])
|
|
907
|
+
|
|
801
908
|
const handleShowCorrectionDetail = useCallback((wordId: string) => {
|
|
802
909
|
// Find the correction for this word
|
|
803
910
|
const correction = data.corrections?.find(c =>
|
|
@@ -1177,6 +1284,11 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
|
1177
1284
|
onUnCorrectAll={handleUnCorrectAll}
|
|
1178
1285
|
annotationsEnabled={annotationsEnabled}
|
|
1179
1286
|
onAnnotationsToggle={handleAnnotationsToggle}
|
|
1287
|
+
reviewMode={reviewMode}
|
|
1288
|
+
onReviewModeToggle={setReviewMode}
|
|
1289
|
+
onAcceptAllCorrections={handleAcceptAllCorrections}
|
|
1290
|
+
onAcceptHighConfidenceCorrections={handleAcceptHighConfidenceCorrections}
|
|
1291
|
+
onRevertAllCorrections={handleRevertAllCorrections}
|
|
1180
1292
|
/>
|
|
1181
1293
|
|
|
1182
1294
|
<Grid container direction={isMobile ? 'column' : 'row'}>
|
|
@@ -1198,6 +1310,11 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
|
1198
1310
|
// needs to update history
|
|
1199
1311
|
updateDataWithHistory(updatedData, 'direct data change');
|
|
1200
1312
|
}}
|
|
1313
|
+
reviewMode={reviewMode}
|
|
1314
|
+
onRevertCorrection={handleRevertCorrection}
|
|
1315
|
+
onEditCorrection={handleEditCorrection}
|
|
1316
|
+
onAcceptCorrection={handleAcceptCorrection}
|
|
1317
|
+
onShowCorrectionDetail={handleShowCorrectionDetail}
|
|
1201
1318
|
/>
|
|
1202
1319
|
{!isReadOnly && apiClient && (
|
|
1203
1320
|
<Box sx={{
|
|
@@ -54,7 +54,12 @@ export default function TranscriptionView({
|
|
|
54
54
|
onPlaySegment,
|
|
55
55
|
currentTime = 0,
|
|
56
56
|
anchors = [],
|
|
57
|
-
onDataChange
|
|
57
|
+
onDataChange,
|
|
58
|
+
reviewMode = false,
|
|
59
|
+
onRevertCorrection,
|
|
60
|
+
onEditCorrection,
|
|
61
|
+
onAcceptCorrection,
|
|
62
|
+
onShowCorrectionDetail
|
|
58
63
|
}: TranscriptionViewProps) {
|
|
59
64
|
const [selectedSegmentIndex, setSelectedSegmentIndex] = useState<number | null>(null)
|
|
60
65
|
const [viewMode, setViewMode] = useState<'text' | 'duration'>('text')
|
|
@@ -237,6 +242,11 @@ export default function TranscriptionView({
|
|
|
237
242
|
currentTime={currentTime}
|
|
238
243
|
gaps={data.gap_sequences}
|
|
239
244
|
corrections={data.corrections}
|
|
245
|
+
reviewMode={reviewMode}
|
|
246
|
+
onRevertCorrection={onRevertCorrection}
|
|
247
|
+
onEditCorrection={onEditCorrection}
|
|
248
|
+
onAcceptCorrection={onAcceptCorrection}
|
|
249
|
+
onShowCorrectionDetail={onShowCorrectionDetail}
|
|
240
250
|
/>
|
|
241
251
|
</TextContainer>
|
|
242
252
|
</Box>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Typography, Box } from '@mui/material'
|
|
1
|
+
import { Typography, Box, useMediaQuery, useTheme } from '@mui/material'
|
|
2
2
|
import { WordComponent } from './Word'
|
|
3
3
|
import { useWordClick } from '../hooks/useWordClick'
|
|
4
4
|
import {
|
|
@@ -16,6 +16,7 @@ import React from 'react'
|
|
|
16
16
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
|
17
17
|
import IconButton from '@mui/material/IconButton'
|
|
18
18
|
import { getWordsFromIds } from '../utils/wordUtils'
|
|
19
|
+
import CorrectedWordWithActions from '../../CorrectedWordWithActions'
|
|
19
20
|
|
|
20
21
|
export interface HighlightedTextProps {
|
|
21
22
|
text?: string
|
|
@@ -36,6 +37,12 @@ export interface HighlightedTextProps {
|
|
|
36
37
|
gaps?: GapSequence[]
|
|
37
38
|
flashingHandler?: string | null
|
|
38
39
|
corrections?: WordCorrection[]
|
|
40
|
+
// Review mode props for agentic corrections
|
|
41
|
+
reviewMode?: boolean
|
|
42
|
+
onRevertCorrection?: (wordId: string) => void
|
|
43
|
+
onEditCorrection?: (wordId: string) => void
|
|
44
|
+
onAcceptCorrection?: (wordId: string) => void
|
|
45
|
+
onShowCorrectionDetail?: (wordId: string) => void
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
export function HighlightedText({
|
|
@@ -57,7 +64,15 @@ export function HighlightedText({
|
|
|
57
64
|
gaps = [],
|
|
58
65
|
flashingHandler,
|
|
59
66
|
corrections = [],
|
|
67
|
+
reviewMode = false,
|
|
68
|
+
onRevertCorrection,
|
|
69
|
+
onEditCorrection,
|
|
70
|
+
onAcceptCorrection,
|
|
71
|
+
onShowCorrectionDetail,
|
|
60
72
|
}: HighlightedTextProps) {
|
|
73
|
+
const theme = useTheme()
|
|
74
|
+
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
|
75
|
+
|
|
61
76
|
const { handleWordClick } = useWordClick({
|
|
62
77
|
mode,
|
|
63
78
|
onElementClick,
|
|
@@ -157,43 +172,83 @@ export function HighlightedText({
|
|
|
157
172
|
|
|
158
173
|
const renderContent = () => {
|
|
159
174
|
if (wordPositions && !segments) {
|
|
160
|
-
return wordPositions.map((wordPos, index) =>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
175
|
+
return wordPositions.map((wordPos, index) => {
|
|
176
|
+
// Find correction for this word
|
|
177
|
+
const correction = corrections?.find(c =>
|
|
178
|
+
c.corrected_word_id === wordPos.word.id ||
|
|
179
|
+
c.word_id === wordPos.word.id
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Use CorrectedWordWithActions for agentic corrections
|
|
183
|
+
if (correction && correction.handler === 'AgenticCorrector') {
|
|
184
|
+
return (
|
|
185
|
+
<React.Fragment key={wordPos.word.id}>
|
|
186
|
+
<CorrectedWordWithActions
|
|
187
|
+
word={wordPos.word.text}
|
|
188
|
+
originalWord={correction.original_word}
|
|
189
|
+
correction={{
|
|
190
|
+
originalWord: correction.original_word,
|
|
191
|
+
handler: correction.handler,
|
|
192
|
+
confidence: correction.confidence,
|
|
193
|
+
source: correction.source,
|
|
194
|
+
reason: correction.reason
|
|
195
|
+
}}
|
|
196
|
+
shouldFlash={shouldWordFlash(wordPos)}
|
|
197
|
+
showActions={reviewMode && !isMobile}
|
|
198
|
+
onRevert={() => onRevertCorrection?.(wordPos.word.id)}
|
|
199
|
+
onEdit={() => onEditCorrection?.(wordPos.word.id)}
|
|
200
|
+
onAccept={() => onAcceptCorrection?.(wordPos.word.id)}
|
|
201
|
+
onClick={() => {
|
|
202
|
+
if (isMobile) {
|
|
203
|
+
onShowCorrectionDetail?.(wordPos.word.id)
|
|
204
|
+
} else {
|
|
205
|
+
handleWordClick(
|
|
206
|
+
wordPos.word.text,
|
|
207
|
+
wordPos.word.id,
|
|
208
|
+
wordPos.type === 'anchor' ? wordPos.sequence as AnchorSequence : undefined,
|
|
209
|
+
wordPos.type === 'gap' ? wordPos.sequence as GapSequence : undefined
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
}}
|
|
213
|
+
/>
|
|
214
|
+
{index < wordPositions.length - 1 && ' '}
|
|
215
|
+
</React.Fragment>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Default rendering with WordComponent
|
|
220
|
+
return (
|
|
221
|
+
<React.Fragment key={wordPos.word.id}>
|
|
222
|
+
<WordComponent
|
|
223
|
+
key={`${wordPos.word.id}-${index}`}
|
|
224
|
+
word={wordPos.word.text}
|
|
225
|
+
shouldFlash={shouldWordFlash(wordPos)}
|
|
226
|
+
isAnchor={wordPos.type === 'anchor'}
|
|
227
|
+
isCorrectedGap={wordPos.isCorrected}
|
|
228
|
+
isUncorrectedGap={wordPos.type === 'gap' && !wordPos.isCorrected}
|
|
229
|
+
isCurrentlyPlaying={shouldHighlightWord(wordPos)}
|
|
230
|
+
onClick={() => handleWordClick(
|
|
231
|
+
wordPos.word.text,
|
|
232
|
+
wordPos.word.id,
|
|
233
|
+
wordPos.type === 'anchor' ? wordPos.sequence as AnchorSequence : undefined,
|
|
234
|
+
wordPos.type === 'gap' ? wordPos.sequence as GapSequence : undefined
|
|
235
|
+
)}
|
|
236
|
+
correction={correction ? {
|
|
182
237
|
originalWord: correction.original_word,
|
|
183
238
|
handler: correction.handler,
|
|
184
239
|
confidence: correction.confidence,
|
|
185
240
|
source: correction.source,
|
|
186
241
|
reason: correction.reason
|
|
187
|
-
} : null
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
)
|
|
242
|
+
} : null}
|
|
243
|
+
/>
|
|
244
|
+
{index < wordPositions.length - 1 && ' '}
|
|
245
|
+
</React.Fragment>
|
|
246
|
+
);
|
|
247
|
+
})
|
|
193
248
|
} else if (segments) {
|
|
194
249
|
return segments.map((segment) => (
|
|
195
|
-
<Box key={segment.id} sx={{
|
|
196
|
-
display: 'flex',
|
|
250
|
+
<Box key={segment.id} sx={{
|
|
251
|
+
display: 'flex',
|
|
197
252
|
alignItems: 'flex-start',
|
|
198
253
|
mb: 0
|
|
199
254
|
}}>
|
|
@@ -212,12 +267,44 @@ export function HighlightedText({
|
|
|
212
267
|
|
|
213
268
|
const sequence = wordPos?.type === 'gap' ? wordPos.sequence as GapSequence : undefined;
|
|
214
269
|
|
|
215
|
-
// Find correction information
|
|
216
|
-
const correction = corrections?.find(c =>
|
|
217
|
-
c.corrected_word_id === word.id ||
|
|
270
|
+
// Find correction information
|
|
271
|
+
const correction = corrections?.find(c =>
|
|
272
|
+
c.corrected_word_id === word.id ||
|
|
218
273
|
c.word_id === word.id
|
|
219
274
|
);
|
|
220
|
-
|
|
275
|
+
|
|
276
|
+
// Use CorrectedWordWithActions for agentic corrections
|
|
277
|
+
if (correction && correction.handler === 'AgenticCorrector') {
|
|
278
|
+
return (
|
|
279
|
+
<React.Fragment key={word.id}>
|
|
280
|
+
<CorrectedWordWithActions
|
|
281
|
+
word={word.text}
|
|
282
|
+
originalWord={correction.original_word}
|
|
283
|
+
correction={{
|
|
284
|
+
originalWord: correction.original_word,
|
|
285
|
+
handler: correction.handler,
|
|
286
|
+
confidence: correction.confidence,
|
|
287
|
+
source: correction.source,
|
|
288
|
+
reason: correction.reason
|
|
289
|
+
}}
|
|
290
|
+
shouldFlash={shouldWordFlash(wordPos || { word: word.text, id: word.id })}
|
|
291
|
+
showActions={reviewMode && !isMobile}
|
|
292
|
+
onRevert={() => onRevertCorrection?.(word.id)}
|
|
293
|
+
onEdit={() => onEditCorrection?.(word.id)}
|
|
294
|
+
onAccept={() => onAcceptCorrection?.(word.id)}
|
|
295
|
+
onClick={() => {
|
|
296
|
+
if (isMobile) {
|
|
297
|
+
onShowCorrectionDetail?.(word.id)
|
|
298
|
+
} else {
|
|
299
|
+
handleWordClick(word.text, word.id, anchor, sequence)
|
|
300
|
+
}
|
|
301
|
+
}}
|
|
302
|
+
/>
|
|
303
|
+
{wordIndex < segment.words.length - 1 && ' '}
|
|
304
|
+
</React.Fragment>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
221
308
|
const correctionInfo = correction ? {
|
|
222
309
|
originalWord: correction.original_word,
|
|
223
310
|
handler: correction.handler,
|
|
@@ -85,6 +85,12 @@ export interface TranscriptionViewProps {
|
|
|
85
85
|
anchors?: AnchorSequence[]
|
|
86
86
|
flashingHandler?: string | null
|
|
87
87
|
onDataChange?: (updatedData: CorrectionData) => void
|
|
88
|
+
// Review mode props for agentic corrections
|
|
89
|
+
reviewMode?: boolean
|
|
90
|
+
onRevertCorrection?: (wordId: string) => void
|
|
91
|
+
onEditCorrection?: (wordId: string) => void
|
|
92
|
+
onAcceptCorrection?: (wordId: string) => void
|
|
93
|
+
onShowCorrectionDetail?: (wordId: string) => void
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
// Add LinePosition type here since it's used in multiple places
|