karaoke-gen 0.90.1__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 (187) 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/utils/__init__.py +163 -8
  148. karaoke_gen/video_background_processor.py +9 -4
  149. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/METADATA +1 -1
  150. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/RECORD +186 -41
  151. lyrics_transcriber/correction/agentic/providers/config.py +9 -5
  152. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +1 -51
  153. lyrics_transcriber/correction/corrector.py +192 -130
  154. lyrics_transcriber/correction/operations.py +24 -9
  155. lyrics_transcriber/frontend/package-lock.json +2 -2
  156. lyrics_transcriber/frontend/package.json +1 -1
  157. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
  158. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
  159. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
  160. lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
  161. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
  162. lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
  163. lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
  164. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
  165. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
  166. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
  167. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
  168. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
  169. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
  170. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
  171. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
  172. lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
  173. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
  174. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
  175. lyrics_transcriber/frontend/src/theme.ts +42 -15
  176. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  177. lyrics_transcriber/frontend/vite.config.js +5 -0
  178. lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
  179. lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
  180. lyrics_transcriber/frontend/web_assets/index.html +6 -2
  181. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
  182. lyrics_transcriber/output/generator.py +17 -3
  183. lyrics_transcriber/output/video.py +60 -95
  184. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
  185. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/WHEEL +0 -0
  186. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/entry_points.txt +0 -0
  187. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/licenses/LICENSE +0 -0
@@ -23,6 +23,7 @@ interface TimelineCanvasProps {
23
23
  audioDuration: number
24
24
  zoomSeconds: number
25
25
  height?: number
26
+ isDarkMode?: boolean
26
27
  }
27
28
 
28
29
  // Constants for rendering
@@ -33,17 +34,25 @@ const CANVAS_PADDING = 8
33
34
  const TEXT_ABOVE_BLOCK = 14
34
35
  const RESIZE_HANDLE_SIZE = 8
35
36
  const RESIZE_HANDLE_HITAREA = 12
36
- // Dark theme colors matching karaoke-gen
37
- const PLAYHEAD_COLOR = '#f97316' // orange-500 for better visibility
38
- const WORD_BLOCK_COLOR = '#dc2626' // red-600 for dark mode
39
- const WORD_BLOCK_SELECTED_COLOR = '#b91c1c' // red-700 for dark mode
40
- const WORD_BLOCK_CURRENT_COLOR = '#ef4444' // red-500 for dark mode
41
- const WORD_TEXT_CURRENT_COLOR = '#fca5a5' // red-300 for dark mode
42
- const UPCOMING_WORD_BG = '#2a2a2a' // slate-700 for dark mode
43
- const UPCOMING_WORD_TEXT = '#e5e5e5' // slate-50 for dark mode
44
- const TIME_BAR_BG = '#1a1a1a' // slate-800 for dark mode
45
- const TIME_BAR_TEXT = '#888888' // slate-400 for dark mode
46
- const TIMELINE_BG = '#0f0f0f' // slate-900 for dark mode
37
+
38
+ // Theme-aware colors
39
+ const getThemeColors = (isDarkMode: boolean) => ({
40
+ playhead: '#f97316', // orange-500 - same for both themes
41
+ wordBlock: isDarkMode ? '#dc2626' : '#ef4444', // red-600 dark, red-500 light
42
+ wordBlockSelected: isDarkMode ? '#b91c1c' : '#dc2626', // red-700 dark, red-600 light
43
+ wordBlockCurrent: isDarkMode ? '#ef4444' : '#f87171', // red-500 dark, red-400 light
44
+ wordTextCurrent: isDarkMode ? '#fca5a5' : '#991b1b', // red-300 dark, red-800 light
45
+ wordText: isDarkMode ? '#f8fafc' : '#1e293b', // slate-50 dark, slate-800 light
46
+ upcomingWordBg: isDarkMode ? '#2a2a2a' : '#e5e7eb', // slate-700 dark, gray-200 light
47
+ upcomingWordText: isDarkMode ? '#e5e5e5' : '#374151', // slate-50 dark, gray-700 light
48
+ timeBarBg: isDarkMode ? '#1a1a1a' : '#f3f4f6', // slate-800 dark, gray-100 light
49
+ timeBarText: isDarkMode ? '#888888' : '#6b7280', // slate-400 dark, gray-500 light
50
+ timelineBg: isDarkMode ? '#0f0f0f' : '#ffffff', // slate-900 dark, white light
51
+ gridLine: isDarkMode ? '#64748b' : '#94a3b8', // slate-500 dark, slate-400 light
52
+ borderLine: isDarkMode ? '#2a2a2a' : '#cbd5e1', // slate-700 dark, slate-300 light
53
+ handleStroke: isDarkMode ? '#0f0f0f' : '#ffffff', // stroke around handles
54
+ playheadShadow: isDarkMode ? 'rgba(0,0,0,0.6)' : 'rgba(0,0,0,0.3)', // shadow behind playhead line
55
+ })
47
56
 
48
57
  // Drag modes
49
58
  type DragMode = 'none' | 'selection' | 'resize' | 'move'
@@ -116,8 +125,11 @@ const TimelineCanvas = memo(function TimelineCanvas({
116
125
  onScrollChange,
117
126
  audioDuration,
118
127
  zoomSeconds,
119
- height = 200
128
+ height = 200,
129
+ isDarkMode = true
120
130
  }: TimelineCanvasProps) {
131
+ // Get theme colors
132
+ const colors = getThemeColors(isDarkMode)
121
133
  const canvasRef = useRef<HTMLCanvasElement>(null)
122
134
  const containerRef = useRef<HTMLDivElement>(null)
123
135
  const [canvasWidth, setCanvasWidth] = useState(800)
@@ -249,11 +261,11 @@ const TimelineCanvas = memo(function TimelineCanvas({
249
261
  ctx.scale(dpr, dpr)
250
262
 
251
263
  // Clear canvas
252
- ctx.fillStyle = TIMELINE_BG
264
+ ctx.fillStyle = colors.timelineBg
253
265
  ctx.fillRect(0, 0, canvasWidth, height)
254
266
 
255
267
  // Draw time bar background
256
- ctx.fillStyle = TIME_BAR_BG
268
+ ctx.fillStyle = colors.timeBarBg
257
269
  ctx.fillRect(0, 0, canvasWidth, TIME_BAR_HEIGHT)
258
270
 
259
271
  // Draw time markers
@@ -261,15 +273,15 @@ const TimelineCanvas = memo(function TimelineCanvas({
261
273
  const secondsPerTick = duration > 15 ? 2 : duration > 8 ? 1 : 0.5
262
274
  const startSecond = Math.ceil(visibleStartTime / secondsPerTick) * secondsPerTick
263
275
 
264
- ctx.fillStyle = TIME_BAR_TEXT
276
+ ctx.fillStyle = colors.timeBarText
265
277
  ctx.font = '11px system-ui, -apple-system, sans-serif'
266
278
  ctx.textAlign = 'center'
267
279
 
268
280
  for (let t = startSecond; t <= visibleEndTime; t += secondsPerTick) {
269
281
  const x = timeToX(t)
270
-
282
+
271
283
  ctx.beginPath()
272
- ctx.strokeStyle = '#64748b' // slate-500 for dark mode
284
+ ctx.strokeStyle = colors.gridLine
273
285
  ctx.lineWidth = 1
274
286
  ctx.moveTo(x, TIME_BAR_HEIGHT - 6)
275
287
  ctx.lineTo(x, TIME_BAR_HEIGHT)
@@ -281,7 +293,7 @@ const TimelineCanvas = memo(function TimelineCanvas({
281
293
  }
282
294
 
283
295
  ctx.beginPath()
284
- ctx.strokeStyle = '#2a2a2a' // slate-700 for dark mode
296
+ ctx.strokeStyle = colors.borderLine
285
297
  ctx.lineWidth = 1
286
298
  ctx.moveTo(0, TIME_BAR_HEIGHT)
287
299
  ctx.lineTo(canvasWidth, TIME_BAR_HEIGHT)
@@ -305,17 +317,17 @@ const TimelineCanvas = memo(function TimelineCanvas({
305
317
 
306
318
  // Draw word block background
307
319
  if (isSelected) {
308
- ctx.fillStyle = WORD_BLOCK_SELECTED_COLOR
320
+ ctx.fillStyle = colors.wordBlockSelected
309
321
  } else if (isCurrent) {
310
- ctx.fillStyle = WORD_BLOCK_CURRENT_COLOR
322
+ ctx.fillStyle = colors.wordBlockCurrent
311
323
  } else {
312
- ctx.fillStyle = WORD_BLOCK_COLOR
324
+ ctx.fillStyle = colors.wordBlock
313
325
  }
314
326
  ctx.fillRect(bounds.startX, bounds.y, bounds.blockWidth, WORD_BLOCK_HEIGHT)
315
327
 
316
328
  // Draw selection border
317
329
  if (isSelected) {
318
- ctx.strokeStyle = '#f97316' // orange-500 for dark mode selection
330
+ ctx.strokeStyle = colors.playhead
319
331
  ctx.lineWidth = 2
320
332
  ctx.strokeRect(bounds.startX, bounds.y, bounds.blockWidth, WORD_BLOCK_HEIGHT)
321
333
 
@@ -325,10 +337,10 @@ const TimelineCanvas = memo(function TimelineCanvas({
325
337
  const handleY = bounds.y + WORD_BLOCK_HEIGHT / 2
326
338
 
327
339
  ctx.beginPath()
328
- ctx.fillStyle = '#f97316' // orange-500 for dark mode
340
+ ctx.fillStyle = colors.playhead
329
341
  ctx.arc(handleX, handleY, RESIZE_HANDLE_SIZE / 2, 0, Math.PI * 2)
330
342
  ctx.fill()
331
- ctx.strokeStyle = '#0f0f0f' // slate-900 for dark mode
343
+ ctx.strokeStyle = colors.handleStroke
332
344
  ctx.lineWidth = 1
333
345
  ctx.stroke()
334
346
  }
@@ -369,7 +381,7 @@ const TimelineCanvas = memo(function TimelineCanvas({
369
381
 
370
382
  if (textStartX < canvasWidth - 10) {
371
383
  const isCurrent = word.id === currentWordId
372
- ctx.fillStyle = isCurrent ? WORD_TEXT_CURRENT_COLOR : '#f8fafc' // slate-50 for dark mode
384
+ ctx.fillStyle = isCurrent ? colors.wordTextCurrent : colors.wordText
373
385
  ctx.fillText(word.text, textStartX, textY)
374
386
  rightmostTextEnd = textStartX + textWidth
375
387
  }
@@ -387,14 +399,14 @@ const TimelineCanvas = memo(function TimelineCanvas({
387
399
  for (let i = 0; i < Math.min(upcomingWords.length, 12); i++) {
388
400
  const word = upcomingWords[i]
389
401
  const textWidth = ctx.measureText(word.text).width + 10
390
-
391
- ctx.fillStyle = UPCOMING_WORD_BG
402
+
403
+ ctx.fillStyle = colors.upcomingWordBg
392
404
  ctx.fillRect(offsetX, TIME_BAR_HEIGHT + CANVAS_PADDING + WORD_LEVEL_SPACING + 60, textWidth, 20)
393
-
394
- ctx.fillStyle = UPCOMING_WORD_TEXT
405
+
406
+ ctx.fillStyle = colors.upcomingWordText
395
407
  ctx.textAlign = 'left'
396
408
  ctx.fillText(word.text, offsetX + 5, TIME_BAR_HEIGHT + CANVAS_PADDING + WORD_LEVEL_SPACING + 74)
397
-
409
+
398
410
  offsetX += textWidth + 3
399
411
  if (offsetX > canvasWidth - 20) break
400
412
  }
@@ -403,10 +415,10 @@ const TimelineCanvas = memo(function TimelineCanvas({
403
415
  // Draw playhead
404
416
  if (currentTime >= visibleStartTime && currentTime <= visibleEndTime) {
405
417
  const playheadX = timeToX(currentTime)
406
-
418
+
407
419
  ctx.beginPath()
408
- ctx.fillStyle = PLAYHEAD_COLOR
409
- ctx.strokeStyle = '#0f0f0f' // slate-900 for dark mode
420
+ ctx.fillStyle = colors.playhead
421
+ ctx.strokeStyle = colors.handleStroke
410
422
  ctx.lineWidth = 1
411
423
  ctx.moveTo(playheadX - 6, 2)
412
424
  ctx.lineTo(playheadX + 6, 2)
@@ -416,14 +428,14 @@ const TimelineCanvas = memo(function TimelineCanvas({
416
428
  ctx.stroke()
417
429
 
418
430
  ctx.beginPath()
419
- ctx.strokeStyle = PLAYHEAD_COLOR
431
+ ctx.strokeStyle = colors.playhead
420
432
  ctx.lineWidth = 2
421
433
  ctx.moveTo(playheadX, TIME_BAR_HEIGHT)
422
434
  ctx.lineTo(playheadX, height)
423
435
  ctx.stroke()
424
436
 
425
437
  ctx.beginPath()
426
- ctx.strokeStyle = 'rgba(0,0,0,0.6)' // Darker shadow for dark mode visibility
438
+ ctx.strokeStyle = colors.playheadShadow
427
439
  ctx.lineWidth = 1
428
440
  ctx.moveTo(playheadX + 1, TIME_BAR_HEIGHT)
429
441
  ctx.lineTo(playheadX + 1, height)
@@ -447,7 +459,7 @@ const TimelineCanvas = memo(function TimelineCanvas({
447
459
  }, [
448
460
  canvasWidth, height, visibleStartTime, visibleEndTime, currentTime,
449
461
  words, segments, selectedWordIds, selectionRect, hoveredWordId,
450
- syncWordIndex, isManualSyncing, timeToX, getWordBounds
462
+ syncWordIndex, isManualSyncing, timeToX, getWordBounds, colors
451
463
  ])
452
464
 
453
465
  // Animation frame
@@ -653,13 +665,15 @@ const TimelineCanvas = memo(function TimelineCanvas({
653
665
  <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
654
666
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
655
667
  <Tooltip title="Scroll Left">
656
- <IconButton
657
- size="small"
658
- onClick={handleScrollLeft}
659
- disabled={visibleStartTime <= 0}
660
- >
661
- <ArrowBackIcon fontSize="small" />
662
- </IconButton>
668
+ <span>
669
+ <IconButton
670
+ size="small"
671
+ onClick={handleScrollLeft}
672
+ disabled={visibleStartTime <= 0}
673
+ >
674
+ <ArrowBackIcon fontSize="small" />
675
+ </IconButton>
676
+ </span>
663
677
  </Tooltip>
664
678
 
665
679
  <Box
@@ -689,13 +703,15 @@ const TimelineCanvas = memo(function TimelineCanvas({
689
703
  </Box>
690
704
 
691
705
  <Tooltip title="Scroll Right">
692
- <IconButton
693
- size="small"
694
- onClick={handleScrollRight}
695
- disabled={visibleStartTime >= audioDuration - zoomSeconds}
696
- >
697
- <ArrowForwardIcon fontSize="small" />
698
- </IconButton>
706
+ <span>
707
+ <IconButton
708
+ size="small"
709
+ onClick={handleScrollRight}
710
+ disabled={visibleStartTime >= audioDuration - zoomSeconds}
711
+ >
712
+ <ArrowForwardIcon fontSize="small" />
713
+ </IconButton>
714
+ </span>
699
715
  </Tooltip>
700
716
  </Box>
701
717
  </Box>
@@ -10,7 +10,9 @@ import {
10
10
  TextField,
11
11
  Button,
12
12
  Paper,
13
- Alert
13
+ Alert,
14
+ useTheme,
15
+ useMediaQuery
14
16
  } from '@mui/material'
15
17
  import ZoomInIcon from '@mui/icons-material/ZoomIn'
16
18
  import ZoomOutIcon from '@mui/icons-material/ZoomOut'
@@ -38,7 +40,7 @@ interface LyricsSynchronizerProps {
38
40
  }
39
41
 
40
42
  // Constants for zoom
41
- const MIN_ZOOM_SECONDS = 4.5 // Most zoomed in - 4.5 seconds visible
43
+ const MIN_ZOOM_SECONDS = 2 // Most zoomed in - 2 seconds visible
42
44
  const MAX_ZOOM_SECONDS = 24 // Most zoomed out - 24 seconds visible
43
45
  const ZOOM_STEPS = 50 // Number of zoom levels
44
46
 
@@ -60,8 +62,12 @@ const LyricsSynchronizer = memo(function LyricsSynchronizer({
60
62
  onCancel,
61
63
  setModalSpacebarHandler
62
64
  }: LyricsSynchronizerProps) {
65
+ const theme = useTheme()
66
+ const isDarkMode = theme.palette.mode === 'dark'
67
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
68
+
63
69
  // Working copy of segments
64
- const [workingSegments, setWorkingSegments] = useState<LyricsSegment[]>(() =>
70
+ const [workingSegments, setWorkingSegments] = useState<LyricsSegment[]>(() =>
65
71
  cloneSegments(initialSegments)
66
72
  )
67
73
 
@@ -664,14 +670,80 @@ const LyricsSynchronizer = memo(function LyricsSynchronizer({
664
670
  spacebarHandlerRef.current(e)
665
671
  }
666
672
  }
667
-
673
+
668
674
  setModalSpacebarHandler(() => handler)
669
-
675
+
670
676
  return () => {
671
677
  setModalSpacebarHandler(undefined)
672
678
  }
673
679
  }, [setModalSpacebarHandler])
674
680
 
681
+ // Tap handlers for mobile (simulate spacebar press/release)
682
+ const handleTapStart = useCallback(() => {
683
+ if (!isManualSyncing || isPaused) return
684
+ if (syncWordIndex < 0 || syncWordIndex >= allWords.length) return
685
+ if (isSpacebarPressed) return
686
+
687
+ setIsSpacebarPressed(true)
688
+ wordStartTimeRef.current = currentTimeRef.current
689
+ spacebarPressTimeRef.current = Date.now()
690
+
691
+ // Set start time for current word
692
+ const newWords = [...allWords]
693
+ const currentWord = newWords[syncWordIndex]
694
+ currentWord.start_time = currentTimeRef.current
695
+
696
+ // Handle previous word's end time
697
+ if (syncWordIndex > 0) {
698
+ const prevWord = newWords[syncWordIndex - 1]
699
+ if (prevWord.start_time !== null && prevWord.end_time === null) {
700
+ const gap = currentTimeRef.current - prevWord.start_time
701
+ if (gap > 1.0) {
702
+ prevWord.end_time = prevWord.start_time + 0.5
703
+ } else {
704
+ prevWord.end_time = currentTimeRef.current - 0.005
705
+ }
706
+ }
707
+ }
708
+
709
+ updateWords(newWords)
710
+ }, [isManualSyncing, isPaused, syncWordIndex, allWords, isSpacebarPressed, updateWords])
711
+
712
+ const handleTapEnd = useCallback(() => {
713
+ if (!isManualSyncing || isPaused) return
714
+ if (!isSpacebarPressed) return
715
+
716
+ setIsSpacebarPressed(false)
717
+
718
+ const pressDuration = spacebarPressTimeRef.current
719
+ ? Date.now() - spacebarPressTimeRef.current
720
+ : 0
721
+ const isTap = pressDuration < 200
722
+
723
+ const newWords = [...allWords]
724
+ const currentWord = newWords[syncWordIndex]
725
+
726
+ if (isTap) {
727
+ currentWord.end_time = (wordStartTimeRef.current || currentTimeRef.current) + 0.5
728
+ } else {
729
+ currentWord.end_time = currentTimeRef.current
730
+ }
731
+
732
+ updateWords(newWords)
733
+
734
+ // Move to next word
735
+ if (syncWordIndex < allWords.length - 1) {
736
+ setSyncWordIndex(syncWordIndex + 1)
737
+ } else {
738
+ setIsManualSyncing(false)
739
+ setSyncWordIndex(-1)
740
+ handleStopAudio()
741
+ }
742
+
743
+ wordStartTimeRef.current = null
744
+ spacebarPressTimeRef.current = null
745
+ }, [isManualSyncing, isPaused, isSpacebarPressed, syncWordIndex, allWords, updateWords, handleStopAudio])
746
+
675
747
  // Handle save
676
748
  const handleSave = useCallback(() => {
677
749
  onSave(workingSegments)
@@ -726,12 +798,14 @@ const LyricsSynchronizer = memo(function LyricsSynchronizer({
726
798
  flexShrink: 0
727
799
  }}
728
800
  >
729
- <Paper
730
- sx={{
731
- p: 1.5,
801
+ <Paper
802
+ sx={{
803
+ p: 1.5,
732
804
  height: '100%',
733
- bgcolor: isManualSyncing ? 'info.main' : 'grey.100',
734
- color: isManualSyncing ? 'info.contrastText' : 'text.primary',
805
+ bgcolor: isManualSyncing
806
+ ? 'success.main'
807
+ : (isDarkMode ? 'grey.800' : 'grey.100'),
808
+ color: isManualSyncing ? 'common.white' : 'text.primary',
735
809
  display: 'flex',
736
810
  flexDirection: 'column',
737
811
  justifyContent: 'center',
@@ -739,34 +813,53 @@ const LyricsSynchronizer = memo(function LyricsSynchronizer({
739
813
  boxSizing: 'border-box'
740
814
  }}
741
815
  >
742
- <Typography variant="body2" sx={{ fontWeight: 500, lineHeight: 1.3 }}>
816
+ <Typography
817
+ variant="body2"
818
+ sx={{
819
+ fontWeight: 500,
820
+ lineHeight: 1.3,
821
+ color: isManualSyncing ? 'common.white' : 'text.primary'
822
+ }}
823
+ >
743
824
  {instruction.primary}
744
825
  </Typography>
745
- <Typography variant="caption" sx={{ opacity: 0.85, display: 'block', lineHeight: 1.3 }}>
826
+ <Typography
827
+ variant="caption"
828
+ sx={{
829
+ opacity: 0.9,
830
+ display: 'block',
831
+ lineHeight: 1.3,
832
+ color: isManualSyncing ? 'common.white' : 'text.secondary'
833
+ }}
834
+ >
746
835
  {instruction.secondary}
747
836
  </Typography>
748
837
  </Paper>
749
838
  </Box>
750
839
 
751
- {/* Controls - fixed height section */}
752
- <Box sx={{ height: 88, flexShrink: 0 }}>
840
+ {/* Controls section */}
841
+ <Box sx={{ minHeight: 88, flexShrink: 0 }}>
753
842
  <SyncControls
754
- isManualSyncing={isManualSyncing}
755
- isPaused={isPaused}
756
- onStartSync={handleStartSync}
757
- onPauseSync={handlePauseSync}
758
- onResumeSync={handleResumeSync}
759
- onClearSync={handleClearSync}
760
- onEditLyrics={handleEditLyrics}
761
- onPlay={handlePlayAudio}
762
- onStop={handleStopAudio}
763
- isPlaying={isPlaying}
764
- hasSelectedWords={selectedWordIds.size > 0}
765
- selectedWordCount={selectedWordIds.size}
766
- onUnsyncFromCursor={handleUnsyncFromCursor}
767
- onEditSelectedWord={handleEditSelectedWord}
768
- onDeleteSelected={handleDeleteSelected}
769
- canUnsyncFromCursor={canUnsyncFromCursor}
843
+ isManualSyncing={isManualSyncing}
844
+ isPaused={isPaused}
845
+ onStartSync={handleStartSync}
846
+ onPauseSync={handlePauseSync}
847
+ onResumeSync={handleResumeSync}
848
+ onClearSync={handleClearSync}
849
+ onEditLyrics={handleEditLyrics}
850
+ onPlay={handlePlayAudio}
851
+ onStop={handleStopAudio}
852
+ isPlaying={isPlaying}
853
+ hasSelectedWords={selectedWordIds.size > 0}
854
+ selectedWordCount={selectedWordIds.size}
855
+ onUnsyncFromCursor={handleUnsyncFromCursor}
856
+ onEditSelectedWord={handleEditSelectedWord}
857
+ onDeleteSelected={handleDeleteSelected}
858
+ canUnsyncFromCursor={canUnsyncFromCursor}
859
+ isMobile={isMobile}
860
+ onTapStart={handleTapStart}
861
+ onTapEnd={handleTapEnd}
862
+ isTapping={isSpacebarPressed}
770
863
  />
771
864
  </Box>
772
865
 
@@ -802,6 +895,7 @@ const LyricsSynchronizer = memo(function LyricsSynchronizer({
802
895
  audioDuration={audioDuration}
803
896
  zoomSeconds={zoomSeconds}
804
897
  height={200}
898
+ isDarkMode={isDarkMode}
805
899
  />
806
900
  </Box>
807
901
 
@@ -221,7 +221,7 @@ export default function ReferenceView({
221
221
  width: '100%',
222
222
  mb: 0,
223
223
  '&:hover': {
224
- backgroundColor: 'rgba(248, 250, 252, 0.04)' // slate-50 hover for dark mode
224
+ backgroundColor: (theme) => theme.palette.action.hover
225
225
  }
226
226
  }}
227
227
  >
@@ -16,10 +16,13 @@ interface TimelineEditorProps {
16
16
  const TimelineContainer = styled(Box)(({ theme }) => ({
17
17
  position: 'relative',
18
18
  height: '75px',
19
- backgroundColor: theme.palette.grey[200],
19
+ backgroundColor: theme.palette.mode === 'dark'
20
+ ? theme.palette.background.paper // Dark mode: use card background
21
+ : theme.palette.grey[100], // Light mode: subtle gray
20
22
  borderRadius: theme.shape.borderRadius,
21
23
  margin: theme.spacing(1, 0),
22
24
  padding: theme.spacing(0, 1),
25
+ border: `1px solid ${theme.palette.divider}`,
23
26
  }))
24
27
 
25
28
  const TimelineRuler = styled(Box)(({ theme }) => ({
@@ -28,7 +31,7 @@ const TimelineRuler = styled(Box)(({ theme }) => ({
28
31
  left: 0,
29
32
  right: 0,
30
33
  height: '40px',
31
- borderBottom: `1px solid ${theme.palette.grey[300]}`,
34
+ borderBottom: `1px solid ${theme.palette.divider}`,
32
35
  cursor: 'pointer',
33
36
  }))
34
37
 
@@ -37,11 +40,11 @@ const TimelineMark = styled(Box)(({ theme }) => ({
37
40
  top: '20px',
38
41
  width: '1px',
39
42
  height: '18px',
40
- backgroundColor: theme.palette.grey[700],
43
+ backgroundColor: theme.palette.text.secondary,
41
44
  '&.subsecond': {
42
45
  top: '25px',
43
46
  height: '13px',
44
- backgroundColor: theme.palette.grey[500],
47
+ backgroundColor: theme.palette.text.disabled,
45
48
  }
46
49
  }))
47
50
 
@@ -52,7 +55,11 @@ const TimelineLabel = styled(Box)(({ theme }) => ({
52
55
  fontSize: '0.8rem',
53
56
  color: theme.palette.text.primary,
54
57
  fontWeight: 700,
55
- backgroundColor: theme.palette.grey[200],
58
+ backgroundColor: theme.palette.mode === 'dark'
59
+ ? theme.palette.background.paper
60
+ : theme.palette.grey[100],
61
+ padding: '0 4px',
62
+ borderRadius: '2px',
56
63
  }))
57
64
 
58
65
  const TimelineWord = styled(Box)(({ theme }) => ({
@@ -118,10 +118,10 @@ export default function TimingOffsetModal({
118
118
  </DialogContent>
119
119
  <DialogActions>
120
120
  <Button onClick={onClose}>Cancel</Button>
121
- <Button
122
- onClick={handleApply}
121
+ <Button
122
+ onClick={handleApply}
123
123
  variant="contained"
124
- color={offsetMs === 0 ? "warning" : "primary"}
124
+ color="primary"
125
125
  >
126
126
  {offsetMs === 0 ? "Remove Offset" : "Apply Offset"}
127
127
  </Button>
@@ -187,7 +187,7 @@ export default function TranscriptionView({
187
187
  width: '100%',
188
188
  mb: 0,
189
189
  '&:hover': {
190
- backgroundColor: 'rgba(248, 250, 252, 0.04)' // slate-50 hover for dark mode
190
+ backgroundColor: (theme) => theme.palette.action.hover
191
191
  }
192
192
  }}>
193
193
  <SegmentControls>
@@ -18,7 +18,7 @@ interface WordDividerProps {
18
18
  }
19
19
 
20
20
  const buttonTextStyle = {
21
- color: 'rgba(248, 250, 252, 0.8)', // slate-50 with opacity for dark mode
21
+ color: 'text.secondary',
22
22
  fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
23
23
  fontWeight: 400,
24
24
  fontSize: '0.7rem',
@@ -55,10 +55,12 @@ export default function WordDivider({
55
55
  display: 'flex',
56
56
  alignItems: 'center',
57
57
  justifyContent: 'center',
58
- height: '20px',
59
- my: -0.5,
60
- width: '50%',
61
- backgroundColor: '#1a1a1a', // slate-800 for dark mode
58
+ height: 'auto',
59
+ minHeight: '20px',
60
+ my: 0,
61
+ width: '100%',
62
+ bgcolor: 'background.paper',
63
+ overflow: 'hidden',
62
64
  ...sx
63
65
  }}
64
66
  >
@@ -66,7 +68,9 @@ export default function WordDivider({
66
68
  display: 'flex',
67
69
  alignItems: 'center',
68
70
  gap: 1,
69
- backgroundColor: '#1a1a1a', // slate-800 for dark mode
71
+ flexWrap: 'wrap',
72
+ justifyContent: 'center',
73
+ bgcolor: 'background.paper',
70
74
  padding: '0 8px',
71
75
  zIndex: 1
72
76
  }}>
@@ -184,4 +188,4 @@ export default function WordDivider({
184
188
  </Box>
185
189
  </Box>
186
190
  )
187
- }
191
+ }
@@ -41,14 +41,16 @@ export const WordComponent = React.memo(function Word({
41
41
  borderRadius: '2px',
42
42
  color: isCurrentlyPlaying ? '#ffffff' : 'inherit',
43
43
  textDecoration: correction ? 'underline dotted' : 'none',
44
- textDecorationColor: correction ? '#666666' : 'inherit', // slate-500 for dark mode
45
44
  textUnderlineOffset: '2px',
46
45
  fontSize: '0.85rem',
47
46
  lineHeight: 1.2
48
47
  }}
49
48
  sx={{
49
+ textDecorationColor: correction ? 'text.disabled' : 'inherit', // Theme-aware underline color
50
50
  '&:hover': {
51
- backgroundColor: 'rgba(248, 250, 252, 0.08)' // slate-50 hover for dark mode
51
+ backgroundColor: (theme) => theme.palette.mode === 'dark'
52
+ ? 'rgba(248, 250, 252, 0.08)' // light hover for dark mode
53
+ : 'rgba(30, 41, 59, 0.08)' // dark hover for light mode
52
54
  }
53
55
  }}
54
56
  onClick={onClick}