lyrics-transcriber 0.43.1__py3-none-any.whl → 0.45.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 (51) hide show
  1. lyrics_transcriber/core/controller.py +58 -24
  2. lyrics_transcriber/correction/anchor_sequence.py +22 -8
  3. lyrics_transcriber/correction/corrector.py +47 -3
  4. lyrics_transcriber/correction/handlers/llm.py +15 -12
  5. lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
  6. lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
  7. lyrics_transcriber/frontend/dist/assets/{index-D0Gr3Ep7.js → index-ZCT0s9MG.js} +10174 -6197
  8. lyrics_transcriber/frontend/dist/assets/index-ZCT0s9MG.js.map +1 -0
  9. lyrics_transcriber/frontend/dist/index.html +1 -1
  10. lyrics_transcriber/frontend/src/App.tsx +5 -5
  11. lyrics_transcriber/frontend/src/api.ts +37 -0
  12. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
  13. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +14 -10
  14. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +62 -56
  15. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
  16. lyrics_transcriber/frontend/src/components/EditModal.tsx +467 -399
  17. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +373 -0
  18. lyrics_transcriber/frontend/src/components/EditWordList.tsx +308 -0
  19. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  20. lyrics_transcriber/frontend/src/components/Header.tsx +141 -101
  21. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +569 -107
  22. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +22 -13
  23. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +1 -0
  24. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +29 -12
  25. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +21 -4
  26. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +29 -15
  27. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +36 -18
  28. lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
  29. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +89 -41
  30. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +9 -2
  31. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +27 -3
  32. lyrics_transcriber/frontend/src/components/shared/types.ts +17 -2
  33. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +90 -19
  34. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +192 -0
  35. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +267 -0
  36. lyrics_transcriber/frontend/src/main.tsx +7 -1
  37. lyrics_transcriber/frontend/src/theme.ts +177 -0
  38. lyrics_transcriber/frontend/src/types.ts +1 -1
  39. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  40. lyrics_transcriber/lyrics/base_lyrics_provider.py +2 -2
  41. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  42. lyrics_transcriber/output/generator.py +40 -12
  43. lyrics_transcriber/review/server.py +238 -8
  44. {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/METADATA +3 -2
  45. {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/RECORD +48 -40
  46. lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +0 -1
  47. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +0 -252
  48. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +0 -110
  49. {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/LICENSE +0 -0
  50. {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/WHEEL +0 -0
  51. {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,5 @@
1
1
  import { ToggleButton, ToggleButtonGroup, Box, Typography } from '@mui/material';
2
2
  import HighlightIcon from '@mui/icons-material/Highlight';
3
- import InfoIcon from '@mui/icons-material/Info';
4
3
  import EditIcon from '@mui/icons-material/Edit';
5
4
  import { InteractionMode } from '../types';
6
5
 
@@ -11,28 +10,38 @@ interface ModeSelectorProps {
11
10
 
12
11
  export default function ModeSelector({ effectiveMode, onChange }: ModeSelectorProps) {
13
12
  return (
14
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
15
- <Typography variant="body2" color="text.secondary">
16
- Click Mode:
13
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.2, height: '32px' }}>
14
+ <Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.8rem' }}>
15
+ Mode:
17
16
  </Typography>
18
17
  <ToggleButtonGroup
19
18
  value={effectiveMode}
20
19
  exclusive
21
20
  onChange={(_, newMode) => newMode && onChange(newMode)}
22
21
  size="small"
22
+ sx={{
23
+ height: '32px',
24
+ '& .MuiToggleButton-root': {
25
+ padding: '3px 8px',
26
+ fontSize: '0.75rem',
27
+ height: '32px'
28
+ }
29
+ }}
23
30
  >
24
- <ToggleButton value="details">
25
- <InfoIcon sx={{ mr: 1 }} />
26
- Details
31
+ <ToggleButton
32
+ value="edit"
33
+ title="Click to edit segments and make corrections in the transcription view"
34
+ >
35
+ <EditIcon sx={{ mr: 0.5, fontSize: '1rem' }} />
36
+ Edit
27
37
  </ToggleButton>
28
- <ToggleButton value="highlight">
29
- <HighlightIcon sx={{ mr: 1 }} />
38
+ <ToggleButton
39
+ value="highlight"
40
+ title="Click words in the transcription view to highlight the matching anchor sequence in the reference lyrics. You can also hold SHIFT to temporarily activate this mode."
41
+ >
42
+ <HighlightIcon sx={{ mr: 0.5, fontSize: '1rem' }} />
30
43
  Highlight
31
44
  </ToggleButton>
32
- <ToggleButton value="edit">
33
- <EditIcon sx={{ mr: 1 }} />
34
- Edit
35
- </ToggleButton>
36
45
  </ToggleButtonGroup>
37
46
  </Box>
38
47
  );
@@ -104,6 +104,7 @@ export default function PreviewVideoSection({
104
104
  <video
105
105
  ref={videoRef}
106
106
  controls
107
+ autoPlay
107
108
  src={previewState.videoUrl}
108
109
  style={{
109
110
  display: 'block',
@@ -12,9 +12,9 @@ import { styled } from '@mui/material/styles'
12
12
  const SegmentControls = styled(Box)({
13
13
  display: 'flex',
14
14
  alignItems: 'center',
15
- gap: '4px',
16
- paddingTop: '3px',
17
- paddingRight: '8px'
15
+ gap: '2px',
16
+ paddingTop: '1px',
17
+ paddingRight: '4px'
18
18
  })
19
19
 
20
20
  const TextContainer = styled(Box)({
@@ -174,27 +174,44 @@ export default function ReferenceView({
174
174
  };
175
175
 
176
176
  return (
177
- <Paper sx={{ p: 2 }}>
178
- <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
179
- <Typography variant="h6">
180
- Reference Text
177
+ <Paper sx={{ p: 0.8, position: 'relative' }}>
178
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.5 }}>
179
+ <Typography variant="h6" sx={{ fontSize: '0.9rem', mb: 0 }}>
180
+ Reference Lyrics
181
181
  </Typography>
182
182
  <SourceSelector
183
+ availableSources={availableSources}
183
184
  currentSource={effectiveCurrentSource}
184
185
  onSourceChange={onSourceChange}
185
- availableSources={availableSources}
186
186
  />
187
187
  </Box>
188
- <Box sx={{ display: 'flex', flexDirection: 'column' }}>
188
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.2 }}>
189
189
  {currentSourceSegments.map((segment, index) => (
190
- <Box key={index} sx={{ display: 'flex', alignItems: 'flex-start', width: '100%' }}>
190
+ <Box
191
+ key={index}
192
+ sx={{
193
+ display: 'flex',
194
+ alignItems: 'flex-start',
195
+ width: '100%',
196
+ mb: 0,
197
+ '&:hover': {
198
+ backgroundColor: 'rgba(0, 0, 0, 0.03)'
199
+ }
200
+ }}
201
+ >
191
202
  <SegmentControls>
192
203
  <IconButton
193
204
  size="small"
194
205
  onClick={() => copyToClipboard(segment.words.map(w => w.text).join(' '))}
195
- sx={{ padding: '2px' }}
206
+ sx={{
207
+ padding: '1px',
208
+ height: '18px',
209
+ width: '18px',
210
+ minHeight: '18px',
211
+ minWidth: '18px'
212
+ }}
196
213
  >
197
- <ContentCopyIcon fontSize="small" />
214
+ <ContentCopyIcon sx={{ fontSize: '0.9rem' }} />
198
215
  </IconButton>
199
216
  </SegmentControls>
200
217
  <TextContainer>
@@ -12,6 +12,7 @@ import { CorrectionData } from '../types'
12
12
  import { useMemo, useRef, useEffect } from 'react'
13
13
  import { ApiClient } from '../api'
14
14
  import PreviewVideoSection from './PreviewVideoSection'
15
+ import { CloudUpload, ArrowBack } from '@mui/icons-material'
15
16
 
16
17
  interface ReviewChangesModalProps {
17
18
  open: boolean
@@ -73,6 +74,13 @@ export default function ReviewChangesModal({
73
74
  // Add ref to video element
74
75
  const videoRef = useRef<HTMLVideoElement>(null)
75
76
 
77
+ // Stop audio playback when modal opens
78
+ useEffect(() => {
79
+ if (open && window.isAudioPlaying && window.toggleAudioPlayback) {
80
+ window.toggleAudioPlayback()
81
+ }
82
+ }, [open])
83
+
76
84
  // Add effect to handle spacebar
77
85
  useEffect(() => {
78
86
  if (open) {
@@ -269,7 +277,7 @@ export default function ReviewChangesModal({
269
277
  maxWidth="md"
270
278
  fullWidth
271
279
  >
272
- <DialogTitle>Review Changes</DialogTitle>
280
+ <DialogTitle>Preview Video (With Vocals)</DialogTitle>
273
281
  <DialogContent
274
282
  dividers
275
283
  sx={{
@@ -288,7 +296,7 @@ export default function ReviewChangesModal({
288
296
  {differences.length === 0 ? (
289
297
  <Box>
290
298
  <Typography color="text.secondary">
291
- No changes detected. You can still submit to continue processing.
299
+ No manual corrections detected. If everything looks good in the preview, click submit and the server will generate the final karaoke video.
292
300
  </Typography>
293
301
  <Typography variant="body2" color="text.secondary">
294
302
  Total segments: {updatedData.corrected_segments.length}
@@ -307,12 +315,21 @@ export default function ReviewChangesModal({
307
315
  </Box>
308
316
  </DialogContent>
309
317
  <DialogActions>
310
- <Button onClick={onClose}>Cancel</Button>
318
+ <Button
319
+ onClick={onClose}
320
+ color="warning"
321
+ startIcon={<ArrowBack />}
322
+ sx={{ mr: 'auto' }}
323
+ >
324
+ Cancel
325
+ </Button>
311
326
  <Button
312
327
  onClick={onSubmit}
313
328
  variant="contained"
329
+ color="success"
330
+ endIcon={<CloudUpload />}
314
331
  >
315
- Submit to Server
332
+ Complete Review
316
333
  </Button>
317
334
  </DialogActions>
318
335
  </Dialog>
@@ -9,14 +9,15 @@ interface TimelineEditorProps {
9
9
  onWordUpdate: (index: number, updates: Partial<Word>) => void
10
10
  currentTime?: number
11
11
  onPlaySegment?: (time: number) => void
12
+ showPlaybackIndicator?: boolean
12
13
  }
13
14
 
14
15
  const TimelineContainer = styled(Box)(({ theme }) => ({
15
16
  position: 'relative',
16
- height: '80px',
17
+ height: '75px',
17
18
  backgroundColor: theme.palette.grey[200],
18
19
  borderRadius: theme.shape.borderRadius,
19
- margin: theme.spacing(2, 0),
20
+ margin: theme.spacing(1, 0),
20
21
  padding: theme.spacing(0, 1),
21
22
  }))
22
23
 
@@ -68,6 +69,7 @@ const TimelineWord = styled(Box)(({ theme }) => ({
68
69
  fontSize: '0.875rem',
69
70
  fontFamily: 'sans-serif',
70
71
  transition: 'background-color 0.1s ease',
72
+ boxSizing: 'border-box',
71
73
  '&.highlighted': {
72
74
  backgroundColor: theme.palette.secondary.main,
73
75
  }
@@ -76,17 +78,27 @@ const TimelineWord = styled(Box)(({ theme }) => ({
76
78
  const ResizeHandle = styled(Box)(({ theme }) => ({
77
79
  position: 'absolute',
78
80
  top: 0,
79
- width: 8,
81
+ width: 10,
80
82
  height: '100%',
81
83
  cursor: 'col-resize',
82
84
  '&:hover': {
83
85
  backgroundColor: theme.palette.primary.light,
86
+ opacity: 0.8,
87
+ boxShadow: `0 0 0 1px ${theme.palette.primary.dark}`,
84
88
  },
85
89
  '&.left': {
86
- left: -4,
90
+ left: 0,
91
+ right: 'auto',
92
+ paddingRight: 0,
93
+ borderTopLeftRadius: theme.shape.borderRadius,
94
+ borderBottomLeftRadius: theme.shape.borderRadius,
87
95
  },
88
96
  '&.right': {
89
- right: -4,
97
+ right: 0,
98
+ left: 'auto',
99
+ paddingLeft: 0,
100
+ borderTopRightRadius: theme.shape.borderRadius,
101
+ borderBottomRightRadius: theme.shape.borderRadius,
90
102
  }
91
103
  }))
92
104
 
@@ -102,7 +114,7 @@ const TimelineCursor = styled(Box)(({ theme }) => ({
102
114
  zIndex: 1, // Ensure it's above other elements
103
115
  }))
104
116
 
105
- export default function TimelineEditor({ words, startTime, endTime, onWordUpdate, currentTime = 0, onPlaySegment }: TimelineEditorProps) {
117
+ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate, currentTime = 0, onPlaySegment, showPlaybackIndicator = true }: TimelineEditorProps) {
106
118
  const containerRef = useRef<HTMLDivElement>(null)
107
119
  const [dragState, setDragState] = useState<{
108
120
  wordIndex: number
@@ -297,12 +309,14 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
297
309
  </TimelineRuler>
298
310
 
299
311
  {/* Add cursor line */}
300
- <TimelineCursor
301
- sx={{
302
- left: `${timeToPosition(currentTime)}%`,
303
- display: currentTime >= startTime && currentTime <= endTime ? 'block' : 'none'
304
- }}
305
- />
312
+ {showPlaybackIndicator && (
313
+ <TimelineCursor
314
+ sx={{
315
+ left: `${timeToPosition(currentTime)}%`,
316
+ display: currentTime >= startTime && currentTime <= endTime ? 'block' : 'none'
317
+ }}
318
+ />
319
+ )}
306
320
 
307
321
  {words.map((word, index) => {
308
322
  // Skip words with null timestamps
@@ -311,8 +325,8 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
311
325
  const leftPosition = timeToPosition(word.start_time)
312
326
  const rightPosition = timeToPosition(word.end_time)
313
327
  const width = rightPosition - leftPosition
314
- const visualPadding = 2
315
- const adjustedWidth = Math.max(0, width - visualPadding)
328
+ // Remove the visual padding that creates gaps
329
+ const adjustedWidth = width
316
330
 
317
331
  return (
318
332
  <TimelineWord
@@ -321,7 +335,7 @@ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate
321
335
  sx={{
322
336
  left: `${leftPosition}%`,
323
337
  width: `${adjustedWidth}%`,
324
- maxWidth: `calc(${100 - leftPosition}% - 2px)`,
338
+ maxWidth: `calc(${100 - leftPosition}%)`,
325
339
  }}
326
340
  onMouseDown={(e) => {
327
341
  e.stopPropagation()
@@ -9,14 +9,16 @@ import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
9
9
 
10
10
  const SegmentIndex = styled(Typography)(({ theme }) => ({
11
11
  color: theme.palette.text.secondary,
12
- width: '2em',
13
- minWidth: '2em',
12
+ width: '1.8em',
13
+ minWidth: '1.8em',
14
14
  textAlign: 'right',
15
- marginRight: theme.spacing(1),
15
+ marginRight: theme.spacing(0.8),
16
16
  userSelect: 'none',
17
17
  fontFamily: 'monospace',
18
18
  cursor: 'pointer',
19
- paddingTop: '3px',
19
+ paddingTop: '1px',
20
+ fontSize: '0.8rem',
21
+ lineHeight: 1.2,
20
22
  '&:hover': {
21
23
  textDecoration: 'underline',
22
24
  },
@@ -30,10 +32,10 @@ const TextContainer = styled(Box)({
30
32
  const SegmentControls = styled(Box)({
31
33
  display: 'flex',
32
34
  alignItems: 'center',
33
- gap: '4px',
34
- minWidth: '3em',
35
- paddingTop: '3px',
36
- paddingRight: '8px'
35
+ gap: '2px',
36
+ minWidth: '2.5em',
37
+ paddingTop: '1px',
38
+ paddingRight: '4px'
37
39
  })
38
40
 
39
41
  export default function TranscriptionView({
@@ -51,16 +53,18 @@ export default function TranscriptionView({
51
53
  const [selectedSegmentIndex, setSelectedSegmentIndex] = useState<number | null>(null)
52
54
 
53
55
  return (
54
- <Paper sx={{ p: 2 }}>
55
- <Typography variant="h6" gutterBottom>
56
- Corrected Transcription
57
- </Typography>
58
- <Box sx={{ display: 'flex', flexDirection: 'column' }}>
56
+ <Paper sx={{ p: 0.8 }}>
57
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.5 }}>
58
+ <Typography variant="h6" sx={{ fontSize: '0.9rem', mb: 0 }}>
59
+ Corrected Transcription
60
+ </Typography>
61
+ </Box>
62
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.2 }}>
59
63
  {data.corrected_segments.map((segment, segmentIndex) => {
60
64
  const segmentWords: TranscriptionWordPosition[] = segment.words.map(word => {
61
65
  // Find if this word is part of a correction
62
- const correction = data.corrections?.find(c =>
63
- c.corrected_word_id === word.id ||
66
+ const correction = data.corrections?.find(c =>
67
+ c.corrected_word_id === word.id ||
64
68
  c.word_id === word.id
65
69
  )
66
70
 
@@ -105,7 +109,15 @@ export default function TranscriptionView({
105
109
  })
106
110
 
107
111
  return (
108
- <Box key={segment.id} sx={{ display: 'flex', alignItems: 'flex-start', width: '100%' }}>
112
+ <Box key={segment.id} sx={{
113
+ display: 'flex',
114
+ alignItems: 'flex-start',
115
+ width: '100%',
116
+ mb: 0,
117
+ '&:hover': {
118
+ backgroundColor: 'rgba(0, 0, 0, 0.03)'
119
+ }
120
+ }}>
109
121
  <SegmentControls>
110
122
  <SegmentIndex
111
123
  variant="body2"
@@ -117,9 +129,15 @@ export default function TranscriptionView({
117
129
  <IconButton
118
130
  size="small"
119
131
  onClick={() => onPlaySegment?.(segment.start_time!)}
120
- sx={{ padding: '2px' }}
132
+ sx={{
133
+ padding: '1px',
134
+ height: '18px',
135
+ width: '18px',
136
+ minHeight: '18px',
137
+ minWidth: '18px'
138
+ }}
121
139
  >
122
- <PlayCircleOutlineIcon fontSize="small" />
140
+ <PlayCircleOutlineIcon sx={{ fontSize: '0.9rem' }} />
123
141
  </IconButton>
124
142
  )}
125
143
  </SegmentControls>
@@ -0,0 +1,187 @@
1
+ import { Box, Button, Typography } from '@mui/material'
2
+ import AddIcon from '@mui/icons-material/Add'
3
+ import MergeIcon from '@mui/icons-material/CallMerge'
4
+ import CallSplitIcon from '@mui/icons-material/CallSplit'
5
+ import { SxProps, Theme } from '@mui/material/styles'
6
+
7
+ interface WordDividerProps {
8
+ onAddWord: () => void
9
+ onMergeWords?: () => void
10
+ onAddSegmentBefore?: () => void
11
+ onAddSegmentAfter?: () => void
12
+ onSplitSegment?: () => void
13
+ onMergeSegment?: () => void
14
+ canMerge?: boolean
15
+ isFirst?: boolean
16
+ isLast?: boolean
17
+ sx?: SxProps<Theme>
18
+ }
19
+
20
+ const buttonTextStyle = {
21
+ color: 'rgba(0, 0, 0, 0.6)',
22
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
23
+ fontWeight: 400,
24
+ fontSize: '0.7rem',
25
+ lineHeight: '1.4375em',
26
+ textTransform: 'none'
27
+ }
28
+
29
+ const buttonBaseStyle = {
30
+ minHeight: 0,
31
+ padding: '2px 8px',
32
+ '& .MuiButton-startIcon': {
33
+ marginRight: 0.5
34
+ },
35
+ '& .MuiSvgIcon-root': {
36
+ fontSize: '1.2rem'
37
+ }
38
+ }
39
+
40
+ export default function WordDivider({
41
+ onAddWord,
42
+ onMergeWords,
43
+ onAddSegmentBefore,
44
+ onAddSegmentAfter,
45
+ onSplitSegment,
46
+ onMergeSegment,
47
+ canMerge = false,
48
+ isFirst = false,
49
+ isLast = false,
50
+ sx = {}
51
+ }: WordDividerProps) {
52
+ return (
53
+ <Box
54
+ sx={{
55
+ display: 'flex',
56
+ alignItems: 'center',
57
+ justifyContent: 'center',
58
+ height: '20px',
59
+ my: -0.5,
60
+ width: '50%',
61
+ backgroundColor: '#fff',
62
+ ...sx
63
+ }}
64
+ >
65
+ <Box sx={{
66
+ display: 'flex',
67
+ alignItems: 'center',
68
+ gap: 1,
69
+ backgroundColor: '#fff',
70
+ padding: '0 8px',
71
+ zIndex: 1
72
+ }}>
73
+ <Button
74
+ onClick={onAddWord}
75
+ title="Add Word"
76
+ size="small"
77
+ startIcon={<AddIcon />}
78
+ sx={{
79
+ ...buttonBaseStyle,
80
+ color: 'primary.main',
81
+ }}
82
+ >
83
+ <Typography sx={buttonTextStyle}>
84
+ Add Word
85
+ </Typography>
86
+ </Button>
87
+ {isFirst && onAddSegmentBefore && onMergeSegment && (
88
+ <>
89
+ <Button
90
+ onClick={onAddSegmentBefore}
91
+ title="Add Segment"
92
+ size="small"
93
+ startIcon={<AddIcon sx={{ transform: 'rotate(90deg)' }} />}
94
+ sx={{
95
+ ...buttonBaseStyle,
96
+ color: 'success.main',
97
+ }}
98
+ >
99
+ <Typography sx={buttonTextStyle}>
100
+ Add Segment
101
+ </Typography>
102
+ </Button>
103
+ <Button
104
+ onClick={onMergeSegment}
105
+ title="Merge with Previous Segment"
106
+ size="small"
107
+ startIcon={<MergeIcon sx={{ transform: 'rotate(90deg)' }} />}
108
+ sx={{
109
+ ...buttonBaseStyle,
110
+ color: 'warning.main',
111
+ }}
112
+ >
113
+ <Typography sx={buttonTextStyle}>
114
+ Merge Segment
115
+ </Typography>
116
+ </Button>
117
+ </>
118
+ )}
119
+ {onMergeWords && !isLast && (
120
+ <Button
121
+ onClick={onMergeWords}
122
+ title="Merge Words"
123
+ size="small"
124
+ startIcon={<MergeIcon sx={{ transform: 'rotate(90deg)' }} />}
125
+ disabled={!canMerge}
126
+ sx={{
127
+ ...buttonBaseStyle,
128
+ color: 'primary.main',
129
+ }}
130
+ >
131
+ <Typography sx={buttonTextStyle}>
132
+ Merge Words
133
+ </Typography>
134
+ </Button>
135
+ )}
136
+ {onSplitSegment && !isLast && (
137
+ <Button
138
+ onClick={onSplitSegment}
139
+ title="Split Segment"
140
+ size="small"
141
+ startIcon={<CallSplitIcon sx={{ transform: 'rotate(90deg)' }} />}
142
+ sx={{
143
+ ...buttonBaseStyle,
144
+ color: 'warning.main',
145
+ }}
146
+ >
147
+ <Typography sx={buttonTextStyle}>
148
+ Split Segment
149
+ </Typography>
150
+ </Button>
151
+ )}
152
+ {isLast && onAddSegmentAfter && onMergeSegment && (
153
+ <>
154
+ <Button
155
+ onClick={onAddSegmentAfter}
156
+ title="Add Segment"
157
+ size="small"
158
+ startIcon={<AddIcon sx={{ transform: 'rotate(90deg)' }} />}
159
+ sx={{
160
+ ...buttonBaseStyle,
161
+ color: 'success.main',
162
+ }}
163
+ >
164
+ <Typography sx={buttonTextStyle}>
165
+ Add Segment
166
+ </Typography>
167
+ </Button>
168
+ <Button
169
+ onClick={onMergeSegment}
170
+ title="Merge with Next Segment"
171
+ size="small"
172
+ startIcon={<MergeIcon sx={{ transform: 'rotate(90deg)' }} />}
173
+ sx={{
174
+ ...buttonBaseStyle,
175
+ color: 'warning.main',
176
+ }}
177
+ >
178
+ <Typography sx={buttonTextStyle}>
179
+ Merge Segment
180
+ </Typography>
181
+ </Button>
182
+ </>
183
+ )}
184
+ </Box>
185
+ </Box>
186
+ )
187
+ }