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.
Files changed (35) hide show
  1. karaoke_gen/instrumental_review/static/index.html +179 -16
  2. karaoke_gen/karaoke_gen.py +5 -4
  3. karaoke_gen/lyrics_processor.py +25 -6
  4. {karaoke_gen-0.76.20.dist-info → karaoke_gen-0.82.0.dist-info}/METADATA +79 -3
  5. {karaoke_gen-0.76.20.dist-info → karaoke_gen-0.82.0.dist-info}/RECORD +33 -31
  6. lyrics_transcriber/core/config.py +8 -0
  7. lyrics_transcriber/core/controller.py +43 -1
  8. lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +178 -5
  9. lyrics_transcriber/correction/agentic/prompts/__init__.py +23 -0
  10. lyrics_transcriber/correction/agentic/prompts/classifier.py +66 -6
  11. lyrics_transcriber/correction/agentic/prompts/langfuse_prompts.py +298 -0
  12. lyrics_transcriber/correction/agentic/providers/config.py +7 -0
  13. lyrics_transcriber/correction/agentic/providers/constants.py +1 -1
  14. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +22 -7
  15. lyrics_transcriber/correction/agentic/providers/model_factory.py +28 -13
  16. lyrics_transcriber/correction/agentic/router.py +18 -13
  17. lyrics_transcriber/correction/corrector.py +1 -45
  18. lyrics_transcriber/frontend/.gitignore +1 -0
  19. lyrics_transcriber/frontend/e2e/agentic-corrections.spec.ts +207 -0
  20. lyrics_transcriber/frontend/e2e/fixtures/agentic-correction-data.json +226 -0
  21. lyrics_transcriber/frontend/package.json +4 -1
  22. lyrics_transcriber/frontend/playwright.config.ts +1 -1
  23. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +34 -30
  24. lyrics_transcriber/frontend/src/components/Header.tsx +141 -34
  25. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +120 -3
  26. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +11 -1
  27. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +122 -35
  28. lyrics_transcriber/frontend/src/components/shared/types.ts +6 -0
  29. lyrics_transcriber/output/generator.py +50 -3
  30. lyrics_transcriber/transcribers/local_whisper.py +260 -0
  31. lyrics_transcriber/correction/handlers/llm.py +0 -293
  32. lyrics_transcriber/correction/handlers/llm_providers.py +0 -60
  33. {karaoke_gen-0.76.20.dist-info → karaoke_gen-0.82.0.dist-info}/WHEEL +0 -0
  34. {karaoke_gen-0.76.20.dist-info → karaoke_gen-0.82.0.dist-info}/entry_points.txt +0 -0
  35. {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
- {isReadOnly && (
133
- <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 1 }}>
134
- <Button
135
- variant="outlined"
136
- size="small"
137
- startIcon={<UploadFileIcon />}
138
- onClick={onFileLoad}
139
- fullWidth={isMobile}
140
- >
141
- Load File
142
- </Button>
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
- <React.Fragment key={wordPos.word.id}>
162
- <WordComponent
163
- key={`${wordPos.word.id}-${index}`}
164
- word={wordPos.word.text}
165
- shouldFlash={shouldWordFlash(wordPos)}
166
- isAnchor={wordPos.type === 'anchor'}
167
- isCorrectedGap={wordPos.isCorrected}
168
- isUncorrectedGap={wordPos.type === 'gap' && !wordPos.isCorrected}
169
- isCurrentlyPlaying={shouldHighlightWord(wordPos)}
170
- onClick={() => handleWordClick(
171
- wordPos.word.text,
172
- wordPos.word.id,
173
- wordPos.type === 'anchor' ? wordPos.sequence as AnchorSequence : undefined,
174
- wordPos.type === 'gap' ? wordPos.sequence as GapSequence : undefined
175
- )}
176
- correction={(() => {
177
- const correction = corrections?.find(c =>
178
- c.corrected_word_id === wordPos.word.id ||
179
- c.word_id === wordPos.word.id
180
- );
181
- return correction ? {
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
- {index < wordPositions.length - 1 && ' '}
191
- </React.Fragment>
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 for the tooltip
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