karaoke-gen 0.86.7__py3-none-any.whl → 0.96.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 (188) hide show
  1. backend/.coveragerc +20 -0
  2. backend/.gitignore +37 -0
  3. backend/Dockerfile +43 -0
  4. backend/Dockerfile.base +74 -0
  5. backend/README.md +242 -0
  6. backend/__init__.py +0 -0
  7. backend/api/__init__.py +0 -0
  8. backend/api/dependencies.py +457 -0
  9. backend/api/routes/__init__.py +0 -0
  10. backend/api/routes/admin.py +742 -0
  11. backend/api/routes/audio_search.py +903 -0
  12. backend/api/routes/auth.py +348 -0
  13. backend/api/routes/file_upload.py +2076 -0
  14. backend/api/routes/health.py +344 -0
  15. backend/api/routes/internal.py +435 -0
  16. backend/api/routes/jobs.py +1610 -0
  17. backend/api/routes/review.py +652 -0
  18. backend/api/routes/themes.py +162 -0
  19. backend/api/routes/users.py +1014 -0
  20. backend/config.py +172 -0
  21. backend/main.py +133 -0
  22. backend/middleware/__init__.py +5 -0
  23. backend/middleware/audit_logging.py +124 -0
  24. backend/models/__init__.py +0 -0
  25. backend/models/job.py +519 -0
  26. backend/models/requests.py +123 -0
  27. backend/models/theme.py +153 -0
  28. backend/models/user.py +254 -0
  29. backend/models/worker_log.py +164 -0
  30. backend/pyproject.toml +29 -0
  31. backend/quick-check.sh +93 -0
  32. backend/requirements.txt +29 -0
  33. backend/run_tests.sh +60 -0
  34. backend/services/__init__.py +0 -0
  35. backend/services/audio_analysis_service.py +243 -0
  36. backend/services/audio_editing_service.py +278 -0
  37. backend/services/audio_search_service.py +702 -0
  38. backend/services/auth_service.py +630 -0
  39. backend/services/credential_manager.py +792 -0
  40. backend/services/discord_service.py +172 -0
  41. backend/services/dropbox_service.py +301 -0
  42. backend/services/email_service.py +1093 -0
  43. backend/services/encoding_interface.py +454 -0
  44. backend/services/encoding_service.py +405 -0
  45. backend/services/firestore_service.py +512 -0
  46. backend/services/flacfetch_client.py +573 -0
  47. backend/services/gce_encoding/README.md +72 -0
  48. backend/services/gce_encoding/__init__.py +22 -0
  49. backend/services/gce_encoding/main.py +589 -0
  50. backend/services/gce_encoding/requirements.txt +16 -0
  51. backend/services/gdrive_service.py +356 -0
  52. backend/services/job_logging.py +258 -0
  53. backend/services/job_manager.py +842 -0
  54. backend/services/job_notification_service.py +271 -0
  55. backend/services/local_encoding_service.py +590 -0
  56. backend/services/local_preview_encoding_service.py +407 -0
  57. backend/services/lyrics_cache_service.py +216 -0
  58. backend/services/metrics.py +413 -0
  59. backend/services/packaging_service.py +287 -0
  60. backend/services/rclone_service.py +106 -0
  61. backend/services/storage_service.py +209 -0
  62. backend/services/stripe_service.py +275 -0
  63. backend/services/structured_logging.py +254 -0
  64. backend/services/template_service.py +330 -0
  65. backend/services/theme_service.py +469 -0
  66. backend/services/tracing.py +543 -0
  67. backend/services/user_service.py +721 -0
  68. backend/services/worker_service.py +558 -0
  69. backend/services/youtube_service.py +112 -0
  70. backend/services/youtube_upload_service.py +445 -0
  71. backend/tests/__init__.py +4 -0
  72. backend/tests/conftest.py +224 -0
  73. backend/tests/emulator/__init__.py +7 -0
  74. backend/tests/emulator/conftest.py +88 -0
  75. backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
  76. backend/tests/emulator/test_emulator_integration.py +356 -0
  77. backend/tests/emulator/test_style_loading_direct.py +436 -0
  78. backend/tests/emulator/test_worker_logs_direct.py +229 -0
  79. backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
  80. backend/tests/requirements-test.txt +10 -0
  81. backend/tests/requirements.txt +6 -0
  82. backend/tests/test_admin_email_endpoints.py +411 -0
  83. backend/tests/test_api_integration.py +460 -0
  84. backend/tests/test_api_routes.py +93 -0
  85. backend/tests/test_audio_analysis_service.py +294 -0
  86. backend/tests/test_audio_editing_service.py +386 -0
  87. backend/tests/test_audio_search.py +1398 -0
  88. backend/tests/test_audio_services.py +378 -0
  89. backend/tests/test_auth_firestore.py +231 -0
  90. backend/tests/test_config_extended.py +68 -0
  91. backend/tests/test_credential_manager.py +377 -0
  92. backend/tests/test_dependencies.py +54 -0
  93. backend/tests/test_discord_service.py +244 -0
  94. backend/tests/test_distribution_services.py +820 -0
  95. backend/tests/test_dropbox_service.py +472 -0
  96. backend/tests/test_email_service.py +492 -0
  97. backend/tests/test_emulator_integration.py +322 -0
  98. backend/tests/test_encoding_interface.py +412 -0
  99. backend/tests/test_file_upload.py +1739 -0
  100. backend/tests/test_flacfetch_client.py +632 -0
  101. backend/tests/test_gdrive_service.py +524 -0
  102. backend/tests/test_instrumental_api.py +431 -0
  103. backend/tests/test_internal_api.py +343 -0
  104. backend/tests/test_job_creation_regression.py +583 -0
  105. backend/tests/test_job_manager.py +339 -0
  106. backend/tests/test_job_manager_notifications.py +329 -0
  107. backend/tests/test_job_notification_service.py +443 -0
  108. backend/tests/test_jobs_api.py +273 -0
  109. backend/tests/test_local_encoding_service.py +423 -0
  110. backend/tests/test_local_preview_encoding_service.py +567 -0
  111. backend/tests/test_main.py +87 -0
  112. backend/tests/test_models.py +918 -0
  113. backend/tests/test_packaging_service.py +382 -0
  114. backend/tests/test_requests.py +201 -0
  115. backend/tests/test_routes_jobs.py +282 -0
  116. backend/tests/test_routes_review.py +337 -0
  117. backend/tests/test_services.py +556 -0
  118. backend/tests/test_services_extended.py +112 -0
  119. backend/tests/test_storage_service.py +448 -0
  120. backend/tests/test_style_upload.py +261 -0
  121. backend/tests/test_template_service.py +295 -0
  122. backend/tests/test_theme_service.py +516 -0
  123. backend/tests/test_unicode_sanitization.py +522 -0
  124. backend/tests/test_upload_api.py +256 -0
  125. backend/tests/test_validate.py +156 -0
  126. backend/tests/test_video_worker_orchestrator.py +847 -0
  127. backend/tests/test_worker_log_subcollection.py +509 -0
  128. backend/tests/test_worker_logging.py +365 -0
  129. backend/tests/test_workers.py +1116 -0
  130. backend/tests/test_workers_extended.py +178 -0
  131. backend/tests/test_youtube_service.py +247 -0
  132. backend/tests/test_youtube_upload_service.py +568 -0
  133. backend/validate.py +173 -0
  134. backend/version.py +27 -0
  135. backend/workers/README.md +597 -0
  136. backend/workers/__init__.py +11 -0
  137. backend/workers/audio_worker.py +618 -0
  138. backend/workers/lyrics_worker.py +683 -0
  139. backend/workers/render_video_worker.py +483 -0
  140. backend/workers/screens_worker.py +525 -0
  141. backend/workers/style_helper.py +198 -0
  142. backend/workers/video_worker.py +1277 -0
  143. backend/workers/video_worker_orchestrator.py +701 -0
  144. backend/workers/worker_logging.py +278 -0
  145. karaoke_gen/instrumental_review/static/index.html +7 -4
  146. karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
  147. karaoke_gen/style_loader.py +3 -1
  148. karaoke_gen/utils/__init__.py +163 -8
  149. karaoke_gen/video_background_processor.py +9 -4
  150. {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/METADATA +2 -1
  151. {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/RECORD +187 -42
  152. lyrics_transcriber/correction/agentic/providers/config.py +9 -5
  153. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +1 -51
  154. lyrics_transcriber/correction/corrector.py +192 -130
  155. lyrics_transcriber/correction/operations.py +24 -9
  156. lyrics_transcriber/frontend/package-lock.json +2 -2
  157. lyrics_transcriber/frontend/package.json +1 -1
  158. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
  159. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
  160. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
  161. lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
  162. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
  163. lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
  164. lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
  165. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
  166. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
  167. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
  168. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
  169. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
  170. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
  171. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
  172. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
  173. lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
  174. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
  175. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
  176. lyrics_transcriber/frontend/src/theme.ts +42 -15
  177. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  178. lyrics_transcriber/frontend/vite.config.js +5 -0
  179. lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
  180. lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
  181. lyrics_transcriber/frontend/web_assets/index.html +6 -2
  182. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
  183. lyrics_transcriber/output/generator.py +17 -3
  184. lyrics_transcriber/output/video.py +60 -95
  185. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
  186. {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/WHEEL +0 -0
  187. {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/entry_points.txt +0 -0
  188. {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,9 @@ import {
6
6
  IconButton,
7
7
  Box,
8
8
  CircularProgress,
9
- Typography
9
+ Typography,
10
+ useMediaQuery,
11
+ useTheme
10
12
  } from '@mui/material'
11
13
  import CloseIcon from '@mui/icons-material/Close'
12
14
  import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
@@ -33,6 +35,8 @@ interface TimelineSectionProps {
33
35
  onPlaySegment?: (startTime: number) => void
34
36
  startManualSync: () => void
35
37
  isGlobal: boolean
38
+ onTapStart?: () => void
39
+ onTapEnd?: () => void
36
40
  }
37
41
 
38
42
  const MemoizedTimelineSection = memo(function TimelineSection({
@@ -47,7 +51,9 @@ const MemoizedTimelineSection = memo(function TimelineSection({
47
51
  onWordUpdate,
48
52
  onPlaySegment,
49
53
  startManualSync,
50
- isGlobal
54
+ isGlobal,
55
+ onTapStart,
56
+ onTapEnd
51
57
  }: TimelineSectionProps) {
52
58
  return (
53
59
  <EditTimelineSection
@@ -66,6 +72,8 @@ const MemoizedTimelineSection = memo(function TimelineSection({
66
72
  onPlaySegment={onPlaySegment}
67
73
  startManualSync={startManualSync}
68
74
  isGlobal={isGlobal}
75
+ onTapStart={onTapStart}
76
+ onTapEnd={onTapEnd}
69
77
  />
70
78
  )
71
79
  })
@@ -195,6 +203,9 @@ export default function EditModal({
195
203
  // hasOriginalTranscribedSegment: !!originalTranscribedSegment
196
204
  // });
197
205
 
206
+ const theme = useTheme()
207
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
208
+
198
209
  const [editedSegment, setEditedSegment] = useState<LyricsSegment | null>(segment)
199
210
  const [isPlaying, setIsPlaying] = useState(false)
200
211
 
@@ -224,7 +235,9 @@ export default function EditModal({
224
235
  startManualSync,
225
236
  cleanupManualSync,
226
237
  handleSpacebar,
227
- isSpacebarPressed
238
+ isSpacebarPressed,
239
+ handleTapStart,
240
+ handleTapEnd
228
241
  } = useManualSync({
229
242
  editedSegment,
230
243
  currentTime,
@@ -588,6 +601,7 @@ export default function EditModal({
588
601
  onClose={handleClose}
589
602
  maxWidth="md"
590
603
  fullWidth
604
+ fullScreen={isMobile}
591
605
  onKeyDown={(e) => {
592
606
  if (e.key === 'Enter' && !e.shiftKey && !isLoading) {
593
607
  e.preventDefault()
@@ -596,8 +610,8 @@ export default function EditModal({
596
610
  }}
597
611
  PaperProps={{
598
612
  sx: {
599
- height: '90vh',
600
- margin: '5vh 0'
613
+ height: isMobile ? '100%' : '90vh',
614
+ margin: isMobile ? 0 : '5vh 0'
601
615
  }
602
616
  }}
603
617
  >
@@ -614,17 +628,19 @@ export default function EditModal({
614
628
  }}
615
629
  >
616
630
  {isLoading && (
617
- <Box sx={{
618
- display: 'flex',
631
+ <Box sx={{
632
+ display: 'flex',
619
633
  flexDirection: 'column',
620
- alignItems: 'center',
621
- justifyContent: 'center',
634
+ alignItems: 'center',
635
+ justifyContent: 'center',
622
636
  height: '100%',
623
637
  width: '100%',
624
638
  position: 'absolute',
625
639
  top: 0,
626
640
  left: 0,
627
- backgroundColor: 'rgba(30, 41, 59, 0.95)', // slate-800 with opacity for dark mode
641
+ backgroundColor: (theme) => theme.palette.mode === 'dark'
642
+ ? 'rgba(30, 41, 59, 0.95)' // slate-800 with opacity for dark mode
643
+ : 'rgba(248, 250, 252, 0.95)', // light background for light mode
628
644
  zIndex: 10
629
645
  }}>
630
646
  <CircularProgress size={60} thickness={4} />
@@ -652,6 +668,8 @@ export default function EditModal({
652
668
  onPlaySegment={onPlaySegment}
653
669
  startManualSync={startManualSync}
654
670
  isGlobal={isGlobal}
671
+ onTapStart={handleTapStart}
672
+ onTapEnd={handleTapEnd}
655
673
  />
656
674
 
657
675
  <MemoizedWordList
@@ -4,7 +4,9 @@ import {
4
4
  Typography,
5
5
  IconButton,
6
6
  Tooltip,
7
- Stack
7
+ Stack,
8
+ useMediaQuery,
9
+ useTheme
8
10
  } from '@mui/material'
9
11
  import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
10
12
  import CancelIcon from '@mui/icons-material/Cancel'
@@ -17,10 +19,68 @@ import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline'
17
19
  import PlayArrowIcon from '@mui/icons-material/PlayArrow'
18
20
  import StopIcon from '@mui/icons-material/Stop'
19
21
  import CenterFocusStrongIcon from '@mui/icons-material/CenterFocusStrong'
22
+ import TouchAppIcon from '@mui/icons-material/TouchApp'
20
23
  import TimelineEditor from './TimelineEditor'
21
24
  import { Word } from '../types'
22
25
  import { useState, useEffect, useCallback, useRef, useMemo, memo } from 'react'
23
26
 
27
+ // Separate TapButton component to properly track local press state
28
+ // This prevents issues where onMouseLeave fires before parent state updates
29
+ const TapButton = memo(function TapButton({
30
+ isSpacebarPressed,
31
+ onTapStart,
32
+ onTapEnd
33
+ }: {
34
+ isSpacebarPressed: boolean
35
+ onTapStart: () => void
36
+ onTapEnd: () => void
37
+ }) {
38
+ const isPressedRef = useRef(false)
39
+
40
+ const handleTapStart = useCallback(() => {
41
+ isPressedRef.current = true
42
+ onTapStart()
43
+ }, [onTapStart])
44
+
45
+ const handleTapEnd = useCallback(() => {
46
+ if (isPressedRef.current) {
47
+ isPressedRef.current = false
48
+ onTapEnd()
49
+ }
50
+ }, [onTapEnd])
51
+
52
+ return (
53
+ <Button
54
+ variant="contained"
55
+ color={isSpacebarPressed ? "secondary" : "primary"}
56
+ onTouchStart={(e) => {
57
+ e.preventDefault()
58
+ handleTapStart()
59
+ }}
60
+ onTouchEnd={(e) => {
61
+ e.preventDefault()
62
+ handleTapEnd()
63
+ }}
64
+ onMouseDown={handleTapStart}
65
+ onMouseUp={handleTapEnd}
66
+ onMouseLeave={handleTapEnd}
67
+ startIcon={<TouchAppIcon />}
68
+ sx={{
69
+ py: 2,
70
+ fontSize: '1.1rem',
71
+ fontWeight: 'bold',
72
+ width: '100%',
73
+ minHeight: '56px',
74
+ userSelect: 'none',
75
+ WebkitUserSelect: 'none',
76
+ touchAction: 'manipulation'
77
+ }}
78
+ >
79
+ {isSpacebarPressed ? "HOLD..." : "TAP"}
80
+ </Button>
81
+ )
82
+ })
83
+
24
84
  interface EditTimelineSectionProps {
25
85
  words: Word[]
26
86
  startTime: number
@@ -44,6 +104,8 @@ interface EditTimelineSectionProps {
44
104
  isGlobal?: boolean
45
105
  defaultZoomLevel?: number
46
106
  isReplaceAllMode?: boolean
107
+ onTapStart?: () => void
108
+ onTapEnd?: () => void
47
109
  }
48
110
 
49
111
  // Memoized control buttons to prevent unnecessary re-renders
@@ -91,16 +153,21 @@ const TimelineControls = memo(({
91
153
  onStopAudio?: () => void
92
154
  }) => {
93
155
  return (
94
- <Stack direction="row" spacing={1} alignItems="center">
156
+ <Stack
157
+ direction="row"
158
+ spacing={0.5}
159
+ alignItems="center"
160
+ sx={{ flexWrap: 'wrap', justifyContent: 'center', gap: 0.5 }}
161
+ >
95
162
  {isGlobal && (
96
- <>
163
+ <>
97
164
  <Tooltip title="Scroll Left">
98
165
  <IconButton
99
166
  onClick={onScrollLeft}
100
167
  disabled={visibleStartTime <= startTime}
101
168
  size="small"
102
169
  >
103
- <ArrowBackIcon />
170
+ <ArrowBackIcon fontSize="small" />
104
171
  </IconButton>
105
172
  </Tooltip>
106
173
  <Tooltip title="Zoom Out (Show More Time)">
@@ -109,7 +176,7 @@ const TimelineControls = memo(({
109
176
  disabled={zoomLevel >= (endTime - startTime) || (isReplaceAllMode && isManualSyncing && !isPaused)}
110
177
  size="small"
111
178
  >
112
- <ZoomOutIcon />
179
+ <ZoomOutIcon fontSize="small" />
113
180
  </IconButton>
114
181
  </Tooltip>
115
182
  <Tooltip title="Zoom In (Show Less Time)">
@@ -118,7 +185,7 @@ const TimelineControls = memo(({
118
185
  disabled={zoomLevel <= 2 || (isReplaceAllMode && isManualSyncing && !isPaused)}
119
186
  size="small"
120
187
  >
121
- <ZoomInIcon />
188
+ <ZoomInIcon fontSize="small" />
122
189
  </IconButton>
123
190
  </Tooltip>
124
191
  <Tooltip title="Scroll Right">
@@ -127,7 +194,7 @@ const TimelineControls = memo(({
127
194
  disabled={visibleEndTime >= endTime}
128
195
  size="small"
129
196
  >
130
- <ArrowForwardIcon />
197
+ <ArrowForwardIcon fontSize="small" />
131
198
  </IconButton>
132
199
  </Tooltip>
133
200
  <Tooltip
@@ -140,7 +207,7 @@ const TimelineControls = memo(({
140
207
  color={autoScrollEnabled ? "primary" : "default"}
141
208
  size="small"
142
209
  >
143
- {autoScrollEnabled ? <AutorenewIcon /> : <PauseCircleOutlineIcon />}
210
+ {autoScrollEnabled ? <AutorenewIcon fontSize="small" /> : <PauseCircleOutlineIcon fontSize="small" />}
144
211
  </IconButton>
145
212
  </Tooltip>
146
213
  <Tooltip title="Jump to Current Playback Position">
@@ -149,7 +216,7 @@ const TimelineControls = memo(({
149
216
  disabled={!currentTime}
150
217
  size="small"
151
218
  >
152
- <CenterFocusStrongIcon />
219
+ <CenterFocusStrongIcon fontSize="small" />
153
220
  </IconButton>
154
221
  </Tooltip>
155
222
  </>
@@ -158,26 +225,27 @@ const TimelineControls = memo(({
158
225
  <Button
159
226
  variant="outlined"
160
227
  onClick={onStopAudio}
161
- startIcon={<StopIcon />}
228
+ startIcon={<StopIcon fontSize="small" />}
162
229
  color="error"
163
230
  size="small"
164
231
  >
165
- Stop Audio
232
+ Stop
166
233
  </Button>
167
234
  )}
168
235
  <Button
169
236
  variant={isManualSyncing ? "outlined" : "contained"}
170
237
  onClick={onStartManualSync}
171
- startIcon={isManualSyncing ? <CancelIcon /> : <PlayCircleOutlineIcon />}
238
+ startIcon={isManualSyncing ? <CancelIcon fontSize="small" /> : <PlayCircleOutlineIcon fontSize="small" />}
172
239
  color={isManualSyncing ? "error" : "primary"}
240
+ size="small"
173
241
  >
174
- {isManualSyncing ? "Cancel Sync" : "Manual Sync"}
242
+ {isManualSyncing ? "Cancel" : "Tap To Sync"}
175
243
  </Button>
176
244
  {isManualSyncing && isReplaceAllMode && (
177
245
  <Button
178
246
  variant="outlined"
179
247
  onClick={onPauseResume}
180
- startIcon={isPaused ? <PlayArrowIcon /> : <PauseCircleOutlineIcon />}
248
+ startIcon={isPaused ? <PlayArrowIcon fontSize="small" /> : <PauseCircleOutlineIcon fontSize="small" />}
181
249
  color={isPaused ? "success" : "warning"}
182
250
  size="small"
183
251
  >
@@ -210,8 +278,12 @@ export default function EditTimelineSection({
210
278
  isPaused = false,
211
279
  isGlobal = false,
212
280
  defaultZoomLevel = 10,
213
- isReplaceAllMode = false
281
+ isReplaceAllMode = false,
282
+ onTapStart,
283
+ onTapEnd
214
284
  }: EditTimelineSectionProps) {
285
+ const theme = useTheme()
286
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
215
287
  // Add state for zoom level - use larger default for Replace All mode
216
288
  const [zoomLevel, setZoomLevel] = useState(defaultZoomLevel)
217
289
  const [visibleStartTime, setVisibleStartTime] = useState(startTime)
@@ -432,7 +504,7 @@ export default function EditTimelineSection({
432
504
  return (
433
505
  <>
434
506
  <Box
435
- sx={{ height: '120px', mb: 2 }}
507
+ sx={{ height: isMobile ? '80px' : '120px', mb: 0 }}
436
508
  ref={timelineRef}
437
509
  onWheel={handleScroll}
438
510
  >
@@ -447,14 +519,30 @@ export default function EditTimelineSection({
447
519
  />
448
520
  </Box>
449
521
 
450
- <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
451
- <Typography variant="body2" color="text.secondary">
452
- Original Time Range: {originalStartTime?.toFixed(2) ?? 'N/A'} - {originalEndTime?.toFixed(2) ?? 'N/A'}
453
- <br />
454
- Current Time Range: {currentStartTime?.toFixed(2) ?? 'N/A'} - {currentEndTime?.toFixed(2) ?? 'N/A'}
455
- </Typography>
456
-
457
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
522
+ <Box sx={{
523
+ display: 'flex',
524
+ flexDirection: isMobile ? 'column' : 'row',
525
+ alignItems: isMobile ? 'stretch' : 'center',
526
+ justifyContent: 'space-between',
527
+ gap: isMobile ? 1 : 0,
528
+ mt: isMobile ? 1.5 : 0,
529
+ mb: isMobile ? 2 : 0
530
+ }}>
531
+ {/* Time range info - hidden on mobile to save space */}
532
+ {!isMobile && (
533
+ <Typography variant="body2" color="text.secondary">
534
+ Original Time Range: {originalStartTime?.toFixed(2) ?? 'N/A'} - {originalEndTime?.toFixed(2) ?? 'N/A'}
535
+ <br />
536
+ Current Time Range: {currentStartTime?.toFixed(2) ?? 'N/A'} - {currentEndTime?.toFixed(2) ?? 'N/A'}
537
+ </Typography>
538
+ )}
539
+
540
+ <Box sx={{
541
+ display: 'flex',
542
+ flexDirection: isMobile ? 'column' : 'row',
543
+ alignItems: isMobile ? 'stretch' : 'center',
544
+ gap: isMobile ? 1 : 2
545
+ }}>
458
546
  <TimelineControls
459
547
  isGlobal={isGlobal}
460
548
  visibleStartTime={visibleStartTime}
@@ -478,17 +566,25 @@ export default function EditTimelineSection({
478
566
  onStopAudio={onStopAudio}
479
567
  />
480
568
  {currentWordInfo && (
481
- <Box>
569
+ <Box sx={{ textAlign: isMobile ? 'center' : 'left' }}>
482
570
  <Typography variant="body2">
483
571
  Word {currentWordInfo.index} of {currentWordInfo.total}: <strong>{currentWordInfo.text}</strong>
484
572
  </Typography>
485
573
  <Typography variant="caption" color="text.secondary">
486
574
  {isSpacebarPressed ?
487
- "Holding spacebar... Release when word ends" :
488
- "Press spacebar when word starts (tap for short words, hold for long words)"}
575
+ "Holding... Release when word ends" :
576
+ (isMobile ? "Tap the button when word starts" : "Press spacebar when word starts (tap for short words, hold for long words)")}
489
577
  </Typography>
490
578
  </Box>
491
579
  )}
580
+ {/* Mobile TAP button for manual sync */}
581
+ {isMobile && isManualSyncing && onTapStart && onTapEnd && (
582
+ <TapButton
583
+ isSpacebarPressed={isSpacebarPressed}
584
+ onTapStart={onTapStart}
585
+ onTapEnd={onTapEnd}
586
+ />
587
+ )}
492
588
  </Box>
493
589
  </Box>
494
590
  </>
@@ -4,7 +4,9 @@ import {
4
4
  IconButton,
5
5
  Button,
6
6
  Pagination,
7
- Typography
7
+ Typography,
8
+ useMediaQuery,
9
+ useTheme
8
10
  } from '@mui/material'
9
11
  import DeleteIcon from '@mui/icons-material/Delete'
10
12
  import SplitIcon from '@mui/icons-material/CallSplit'
@@ -34,7 +36,8 @@ const WordRow = memo(function WordRow({
34
36
  onSplitWord,
35
37
  onRemoveWord,
36
38
  wordsLength,
37
- onTabNavigation
39
+ onTabNavigation,
40
+ isMobile
38
41
  }: {
39
42
  word: Word
40
43
  index: number
@@ -43,11 +46,10 @@ const WordRow = memo(function WordRow({
43
46
  onRemoveWord: (index: number) => void
44
47
  wordsLength: number
45
48
  onTabNavigation: (currentIndex: number) => void
49
+ isMobile: boolean
46
50
  }) {
47
51
  const handleKeyDown = (e: React.KeyboardEvent) => {
48
- // console.log('KeyDown event:', e.key, 'Shift:', e.shiftKey, 'Index:', index);
49
52
  if (e.key === 'Tab' && !e.shiftKey) {
50
- // console.log('Tab key detected, preventing default and navigating');
51
53
  e.preventDefault();
52
54
  onTabNavigation(index);
53
55
  }
@@ -56,54 +58,99 @@ const WordRow = memo(function WordRow({
56
58
  return (
57
59
  <Box sx={{
58
60
  display: 'flex',
59
- gap: 2,
60
- alignItems: 'center',
61
+ flexDirection: isMobile ? 'column' : 'row',
62
+ gap: isMobile ? 1 : 2,
63
+ alignItems: isMobile ? 'stretch' : 'center',
61
64
  padding: '4px 0',
62
65
  }}>
63
- <TextField
64
- label={`Word ${index}`}
65
- value={word.text}
66
- onChange={(e) => onWordUpdate(index, { text: e.target.value })}
67
- onKeyDown={handleKeyDown}
68
- fullWidth
69
- size="small"
70
- id={`word-text-${index}`}
71
- />
72
- <TextField
73
- label="Start Time"
74
- value={word.start_time?.toFixed(2) ?? ''}
75
- onChange={(e) => onWordUpdate(index, { start_time: parseFloat(e.target.value) })}
76
- type="number"
77
- inputProps={{ step: 0.01 }}
78
- sx={{ width: '150px' }}
79
- size="small"
80
- />
81
- <TextField
82
- label="End Time"
83
- value={word.end_time?.toFixed(2) ?? ''}
84
- onChange={(e) => onWordUpdate(index, { end_time: parseFloat(e.target.value) })}
85
- type="number"
86
- inputProps={{ step: 0.01 }}
87
- sx={{ width: '150px' }}
88
- size="small"
89
- />
90
- <IconButton
91
- onClick={() => onSplitWord(index)}
92
- title="Split Word"
93
- sx={{ color: 'primary.main' }}
94
- size="small"
95
- >
96
- <SplitIcon fontSize="small" />
97
- </IconButton>
98
- <IconButton
99
- onClick={() => onRemoveWord(index)}
100
- disabled={wordsLength <= 1}
101
- title="Remove Word"
102
- sx={{ color: 'error.main' }}
103
- size="small"
104
- >
105
- <DeleteIcon fontSize="small" />
106
- </IconButton>
66
+ {/* Word text field - full width on mobile */}
67
+ <Box sx={{
68
+ display: 'flex',
69
+ gap: 1,
70
+ alignItems: 'center',
71
+ flex: isMobile ? 'none' : 1
72
+ }}>
73
+ <TextField
74
+ label={`Word ${index}`}
75
+ value={word.text}
76
+ onChange={(e) => onWordUpdate(index, { text: e.target.value })}
77
+ onKeyDown={handleKeyDown}
78
+ fullWidth
79
+ size="small"
80
+ id={`word-text-${index}`}
81
+ />
82
+ {/* Action buttons inline with word on mobile */}
83
+ {isMobile && (
84
+ <>
85
+ <IconButton
86
+ onClick={() => onSplitWord(index)}
87
+ title="Split Word"
88
+ sx={{ color: 'primary.main' }}
89
+ size="small"
90
+ >
91
+ <SplitIcon fontSize="small" />
92
+ </IconButton>
93
+ <IconButton
94
+ onClick={() => onRemoveWord(index)}
95
+ disabled={wordsLength <= 1}
96
+ title="Remove Word"
97
+ sx={{ color: 'error.main' }}
98
+ size="small"
99
+ >
100
+ <DeleteIcon fontSize="small" />
101
+ </IconButton>
102
+ </>
103
+ )}
104
+ </Box>
105
+
106
+ {/* Time fields - row on desktop, separate row on mobile */}
107
+ <Box sx={{
108
+ display: 'flex',
109
+ gap: 1,
110
+ alignItems: 'center',
111
+ justifyContent: isMobile ? 'flex-start' : 'flex-end'
112
+ }}>
113
+ <TextField
114
+ label="Start"
115
+ value={word.start_time?.toFixed(2) ?? ''}
116
+ onChange={(e) => onWordUpdate(index, { start_time: parseFloat(e.target.value) })}
117
+ type="number"
118
+ inputProps={{ step: 0.01 }}
119
+ sx={{ width: isMobile ? '80px' : '100px' }}
120
+ size="small"
121
+ />
122
+ <TextField
123
+ label="End"
124
+ value={word.end_time?.toFixed(2) ?? ''}
125
+ onChange={(e) => onWordUpdate(index, { end_time: parseFloat(e.target.value) })}
126
+ type="number"
127
+ inputProps={{ step: 0.01 }}
128
+ sx={{ width: isMobile ? '80px' : '100px' }}
129
+ size="small"
130
+ />
131
+ {/* Action buttons on desktop only */}
132
+ {!isMobile && (
133
+ <>
134
+ <IconButton
135
+ onClick={() => onSplitWord(index)}
136
+ title="Split Word"
137
+ sx={{ color: 'primary.main' }}
138
+ size="small"
139
+ >
140
+ <SplitIcon fontSize="small" />
141
+ </IconButton>
142
+ <IconButton
143
+ onClick={() => onRemoveWord(index)}
144
+ disabled={wordsLength <= 1}
145
+ title="Remove Word"
146
+ sx={{ color: 'error.main' }}
147
+ size="small"
148
+ >
149
+ <DeleteIcon fontSize="small" />
150
+ </IconButton>
151
+ </>
152
+ )}
153
+ </Box>
107
154
  </Box>
108
155
  );
109
156
  });
@@ -122,7 +169,8 @@ const WordItem = memo(function WordItem({
122
169
  onMergeSegment,
123
170
  wordsLength,
124
171
  isGlobal,
125
- onTabNavigation
172
+ onTabNavigation,
173
+ isMobile
126
174
  }: {
127
175
  word: Word
128
176
  index: number
@@ -137,6 +185,7 @@ const WordItem = memo(function WordItem({
137
185
  wordsLength: number
138
186
  isGlobal: boolean
139
187
  onTabNavigation: (currentIndex: number) => void
188
+ isMobile: boolean
140
189
  }) {
141
190
  return (
142
191
  <Box key={word.id}>
@@ -148,6 +197,7 @@ const WordItem = memo(function WordItem({
148
197
  onRemoveWord={onRemoveWord}
149
198
  wordsLength={wordsLength}
150
199
  onTabNavigation={onTabNavigation}
200
+ isMobile={isMobile}
151
201
  />
152
202
 
153
203
  {/* Word divider with merge/split functionality */}
@@ -168,16 +218,14 @@ const WordItem = memo(function WordItem({
168
218
  }
169
219
  canMerge={index < wordsLength - 1}
170
220
  isLast={index === wordsLength - 1}
171
- sx={{ ml: 15 }}
172
- />
221
+ />
173
222
  )}
174
223
  {isGlobal && (
175
224
  <WordDivider
176
225
  onAddWord={() => onAddWord(index)}
177
226
  onMergeWords={index < wordsLength - 1 ? () => onMergeWords(index) : undefined}
178
227
  canMerge={index < wordsLength - 1}
179
- sx={{ ml: 15 }}
180
- />
228
+ />
181
229
  )}
182
230
  </Box>
183
231
  );
@@ -195,6 +243,9 @@ export default function EditWordList({
195
243
  onMergeSegment,
196
244
  isGlobal = false
197
245
  }: EditWordListProps) {
246
+ const theme = useTheme()
247
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
248
+
198
249
  const [replacementText, setReplacementText] = useState('')
199
250
  const [page, setPage] = useState(1)
200
251
  const pageSize = isGlobal ? 50 : words.length // Use pagination only in global mode
@@ -289,14 +340,12 @@ export default function EditWordList({
289
340
  onAddSegmentBefore={() => onAddSegment?.(0)}
290
341
  onMergeSegment={() => onMergeSegment?.(false)}
291
342
  isFirst={true}
292
- sx={{ ml: 15 }}
293
- />
343
+ />
294
344
  )}
295
345
  {isGlobal && (
296
346
  <WordDivider
297
347
  onAddWord={() => onAddWord(-1)}
298
- sx={{ ml: 15 }}
299
- />
348
+ />
300
349
  )}
301
350
 
302
351
  {/* Word list with scrolling */}
@@ -312,7 +361,9 @@ export default function EditWordList({
312
361
  width: '8px',
313
362
  },
314
363
  '&::-webkit-scrollbar-thumb': {
315
- backgroundColor: 'rgba(248, 250, 252, 0.2)', // slate-50 for dark mode
364
+ backgroundColor: (theme) => theme.palette.mode === 'dark'
365
+ ? 'rgba(248, 250, 252, 0.2)' // light scrollbar for dark mode
366
+ : 'rgba(30, 41, 59, 0.3)', // dark scrollbar for light mode
316
367
  borderRadius: '4px',
317
368
  },
318
369
  scrollbarWidth: 'thin',
@@ -336,6 +387,7 @@ export default function EditWordList({
336
387
  wordsLength={words.length}
337
388
  isGlobal={isGlobal}
338
389
  onTabNavigation={handleTabNavigation}
390
+ isMobile={isMobile}
339
391
  />
340
392
  );
341
393
  })}