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.
- lyrics_transcriber/core/controller.py +58 -24
- lyrics_transcriber/correction/anchor_sequence.py +22 -8
- lyrics_transcriber/correction/corrector.py +47 -3
- lyrics_transcriber/correction/handlers/llm.py +15 -12
- lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
- lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
- lyrics_transcriber/frontend/dist/assets/{index-D0Gr3Ep7.js → index-ZCT0s9MG.js} +10174 -6197
- lyrics_transcriber/frontend/dist/assets/index-ZCT0s9MG.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/App.tsx +5 -5
- lyrics_transcriber/frontend/src/api.ts +37 -0
- lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +14 -10
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +62 -56
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
- lyrics_transcriber/frontend/src/components/EditModal.tsx +467 -399
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +373 -0
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +308 -0
- lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
- lyrics_transcriber/frontend/src/components/Header.tsx +141 -101
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +569 -107
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +22 -13
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +1 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +29 -12
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +21 -4
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +29 -15
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +36 -18
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +89 -41
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +9 -2
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +27 -3
- lyrics_transcriber/frontend/src/components/shared/types.ts +17 -2
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +90 -19
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +192 -0
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +267 -0
- lyrics_transcriber/frontend/src/main.tsx +7 -1
- lyrics_transcriber/frontend/src/theme.ts +177 -0
- lyrics_transcriber/frontend/src/types.ts +1 -1
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/lyrics/base_lyrics_provider.py +2 -2
- lyrics_transcriber/lyrics/user_input_provider.py +44 -0
- lyrics_transcriber/output/generator.py +40 -12
- lyrics_transcriber/review/server.py +238 -8
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/METADATA +3 -2
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/RECORD +48 -40
- lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +0 -1
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +0 -252
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +0 -110
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.45.0.dist-info}/WHEEL +0 -0
- {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
|
-
|
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
|
25
|
-
|
26
|
-
|
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
|
29
|
-
|
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
|
);
|
@@ -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: '
|
16
|
-
paddingTop: '
|
17
|
-
paddingRight: '
|
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:
|
178
|
-
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb:
|
179
|
-
<Typography variant="h6">
|
180
|
-
Reference
|
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
|
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={{
|
206
|
+
sx={{
|
207
|
+
padding: '1px',
|
208
|
+
height: '18px',
|
209
|
+
width: '18px',
|
210
|
+
minHeight: '18px',
|
211
|
+
minWidth: '18px'
|
212
|
+
}}
|
196
213
|
>
|
197
|
-
<ContentCopyIcon
|
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>
|
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
|
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
|
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
|
-
|
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: '
|
17
|
+
height: '75px',
|
17
18
|
backgroundColor: theme.palette.grey[200],
|
18
19
|
borderRadius: theme.shape.borderRadius,
|
19
|
-
margin: theme.spacing(
|
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:
|
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:
|
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:
|
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
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
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
|
-
|
315
|
-
const adjustedWidth =
|
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}%
|
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: '
|
13
|
-
minWidth: '
|
12
|
+
width: '1.8em',
|
13
|
+
minWidth: '1.8em',
|
14
14
|
textAlign: 'right',
|
15
|
-
marginRight: theme.spacing(
|
15
|
+
marginRight: theme.spacing(0.8),
|
16
16
|
userSelect: 'none',
|
17
17
|
fontFamily: 'monospace',
|
18
18
|
cursor: 'pointer',
|
19
|
-
paddingTop: '
|
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: '
|
34
|
-
minWidth: '
|
35
|
-
paddingTop: '
|
36
|
-
paddingRight: '
|
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:
|
55
|
-
<
|
56
|
-
|
57
|
-
|
58
|
-
|
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={{
|
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={{
|
132
|
+
sx={{
|
133
|
+
padding: '1px',
|
134
|
+
height: '18px',
|
135
|
+
width: '18px',
|
136
|
+
minHeight: '18px',
|
137
|
+
minWidth: '18px'
|
138
|
+
}}
|
121
139
|
>
|
122
|
-
<PlayCircleOutlineIcon
|
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
|
+
}
|