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
@@ -39,6 +39,7 @@ interface HeaderProps {
39
39
  onFindReplace?: () => void
40
40
  onEditAll?: () => void
41
41
  onUnCorrectAll?: () => void
42
+ onResetCorrections?: () => void
42
43
  onTimingOffset?: () => void
43
44
  timingOffsetMs?: number
44
45
  onUndo: () => void
@@ -72,6 +73,7 @@ export default function Header({
72
73
  onFindReplace,
73
74
  onEditAll,
74
75
  onUnCorrectAll,
76
+ onResetCorrections,
75
77
  onTimingOffset,
76
78
  timingOffsetMs = 0,
77
79
  onUndo,
@@ -144,68 +146,44 @@ export default function Header({
144
146
  </Box>
145
147
  )}
146
148
 
149
+ {/* Review Mode toggle and Load File button */}
147
150
  <Box sx={{
148
151
  display: 'flex',
149
- flexDirection: isMobile ? 'column' : 'row',
150
152
  gap: 1,
151
- justifyContent: 'space-between',
152
- alignItems: isMobile ? 'stretch' : 'center',
153
+ justifyContent: 'flex-end',
154
+ alignItems: 'center',
153
155
  mb: 1
154
156
  }}>
155
- <Typography variant="h4" sx={{ fontSize: isMobile ? '1.3rem' : '1.5rem' }}>
156
- Nomad Karaoke: Lyrics Transcription Review
157
- </Typography>
158
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
159
- {!isReadOnly && isAgenticMode && onReviewModeToggle && (
160
- <Tooltip title={reviewMode
161
- ? "Hide inline correction actions"
162
- : "Show inline actions on all corrections for quick review"
163
- }>
164
- <Chip
165
- icon={<VisibilityIcon />}
166
- label={reviewMode ? "Review Mode" : "Review Off"}
167
- onClick={() => onReviewModeToggle(!reviewMode)}
168
- color={reviewMode ? "secondary" : "default"}
169
- variant={reviewMode ? "filled" : "outlined"}
170
- size="small"
171
- sx={{
172
- cursor: 'pointer',
173
- '& .MuiChip-icon': { fontSize: '1rem' }
174
- }}
175
- />
176
- </Tooltip>
177
- )}
178
- {!isReadOnly && onAnnotationsToggle && (
179
- <Tooltip title={annotationsEnabled
180
- ? "Click to disable annotation prompts when editing"
181
- : "Click to enable annotation prompts when editing"
182
- }>
183
- <Chip
184
- icon={<RateReviewIcon />}
185
- label={annotationsEnabled ? "Feedback On" : "Feedback Off"}
186
- onClick={() => onAnnotationsToggle(!annotationsEnabled)}
187
- color={annotationsEnabled ? "primary" : "default"}
188
- variant={annotationsEnabled ? "filled" : "outlined"}
189
- size="small"
190
- sx={{
191
- cursor: 'pointer',
192
- '& .MuiChip-icon': { fontSize: '1rem' }
193
- }}
194
- />
195
- </Tooltip>
196
- )}
197
- {isReadOnly && (
198
- <Button
199
- variant="outlined"
157
+ {!isReadOnly && isAgenticMode && onReviewModeToggle && (
158
+ <Tooltip title={reviewMode
159
+ ? "Hide inline correction actions"
160
+ : "Show inline actions on all corrections for quick review"
161
+ }>
162
+ <Chip
163
+ icon={<VisibilityIcon />}
164
+ label={reviewMode ? "Review Mode" : "Review Off"}
165
+ onClick={() => onReviewModeToggle(!reviewMode)}
166
+ color={reviewMode ? "secondary" : "default"}
167
+ variant={reviewMode ? "filled" : "outlined"}
200
168
  size="small"
201
- startIcon={<UploadFileIcon />}
202
- onClick={onFileLoad}
203
- fullWidth={isMobile}
204
- >
205
- Load File
206
- </Button>
207
- )}
208
- </Box>
169
+ sx={{
170
+ cursor: 'pointer',
171
+ '& .MuiChip-icon': { fontSize: '1rem' }
172
+ }}
173
+ />
174
+ </Tooltip>
175
+ )}
176
+ {isReadOnly && (
177
+ <Button
178
+ variant="outlined"
179
+ size="small"
180
+ startIcon={<UploadFileIcon />}
181
+ onClick={onFileLoad}
182
+ fullWidth={isMobile}
183
+ >
184
+ Load File
185
+ </Button>
186
+ )}
209
187
  </Box>
210
188
 
211
189
  <Box sx={{
@@ -213,12 +191,14 @@ export default function Header({
213
191
  gap: 1,
214
192
  mb: 1,
215
193
  flexDirection: isMobile ? 'column' : 'row',
216
- height: '140px'
194
+ height: isMobile ? 'auto' : '140px',
195
+ minHeight: isMobile ? 'auto' : '140px'
217
196
  }}>
218
197
  <Box sx={{
219
- width: '280px',
198
+ width: isMobile ? '100%' : '280px',
199
+ minWidth: isMobile ? '100%' : '280px',
220
200
  position: 'relative',
221
- height: '100%'
201
+ height: isMobile ? 'auto' : '100%'
222
202
  }}>
223
203
  {isAgenticMode ? (
224
204
  <AgenticCorrectionMetrics
@@ -385,15 +365,30 @@ export default function Header({
385
365
  }}>
386
366
  <Box sx={{
387
367
  display: 'flex',
388
- gap: 1,
389
- flexDirection: isMobile ? 'column' : 'row',
390
- alignItems: isMobile ? 'flex-start' : 'center',
391
- height: '32px'
368
+ gap: 0.5,
369
+ flexDirection: 'row',
370
+ flexWrap: 'wrap',
371
+ alignItems: 'center',
372
+ minHeight: '32px'
392
373
  }}>
393
374
  <ModeSelector
394
375
  effectiveMode={effectiveMode}
395
376
  onChange={onModeChange}
396
377
  />
378
+ {!isReadOnly && onResetCorrections && (
379
+ <Tooltip title="Undo all your changes">
380
+ <Button
381
+ variant="outlined"
382
+ size="small"
383
+ color="warning"
384
+ onClick={onResetCorrections}
385
+ startIcon={<UndoIcon />}
386
+ sx={{ minWidth: 'fit-content', height: '32px' }}
387
+ >
388
+ Undo All
389
+ </Button>
390
+ </Tooltip>
391
+ )}
397
392
  {!isReadOnly && (
398
393
  <Box sx={{ display: 'flex', height: '32px' }}>
399
394
  <Tooltip title="Undo">
@@ -457,15 +452,17 @@ export default function Header({
457
452
  </Button>
458
453
  )}
459
454
  {!isReadOnly && onUnCorrectAll && (
460
- <Button
461
- variant="outlined"
462
- size="small"
463
- onClick={onUnCorrectAll}
464
- startIcon={<RestoreIcon />}
465
- sx={{ minWidth: 'fit-content', height: '32px' }}
466
- >
467
- Un-Correct All
468
- </Button>
455
+ <Tooltip title="Revert only automatic AI corrections (keeps your manual edits)">
456
+ <Button
457
+ variant="outlined"
458
+ size="small"
459
+ onClick={onUnCorrectAll}
460
+ startIcon={<RestoreIcon />}
461
+ sx={{ minWidth: 'fit-content', height: '32px' }}
462
+ >
463
+ Undo Auto Corrections
464
+ </Button>
465
+ </Tooltip>
469
466
  )}
470
467
  {!isReadOnly && onTimingOffset && (
471
468
  <Box sx={{ display: 'flex', alignItems: 'center' }}>
@@ -480,10 +477,10 @@ export default function Header({
480
477
  Timing Offset
481
478
  </Button>
482
479
  {timingOffsetMs !== 0 && (
483
- <Typography
484
- variant="body2"
485
- sx={{
486
- ml: 1,
480
+ <Typography
481
+ variant="body2"
482
+ sx={{
483
+ ml: 1,
487
484
  fontWeight: 'bold',
488
485
  color: theme.palette.secondary.main
489
486
  }}
@@ -493,6 +490,23 @@ export default function Header({
493
490
  )}
494
491
  </Box>
495
492
  )}
493
+ {!isReadOnly && onAnnotationsToggle && (
494
+ <Tooltip title={annotationsEnabled
495
+ ? "Click to disable annotation prompts when editing"
496
+ : "Click to enable annotation prompts when editing"
497
+ }>
498
+ <Button
499
+ variant="outlined"
500
+ size="small"
501
+ onClick={() => onAnnotationsToggle(!annotationsEnabled)}
502
+ startIcon={<RateReviewIcon />}
503
+ color={annotationsEnabled ? "primary" : "inherit"}
504
+ sx={{ minWidth: 'fit-content', height: '32px' }}
505
+ >
506
+ {annotationsEnabled ? "Feedback On" : "Feedback Off"}
507
+ </Button>
508
+ </Tooltip>
509
+ )}
496
510
  <AudioPlayer
497
511
  apiClient={apiClient}
498
512
  onTimeUpdate={onTimeUpdate}
@@ -10,7 +10,7 @@ import {
10
10
  WordCorrection,
11
11
  CorrectionAnnotation
12
12
  } from '../types'
13
- import { Box, Button, Grid, useMediaQuery, useTheme } from '@mui/material'
13
+ import { Box, Button, Grid, Typography, useMediaQuery, useTheme } from '@mui/material'
14
14
  import { ApiClient } from '../api'
15
15
  import ReferenceView from './ReferenceView'
16
16
  import TranscriptionView from './TranscriptionView'
@@ -33,7 +33,7 @@ import { setupKeyboardHandlers, setModalHandler, getModalState } from './shared/
33
33
  import Header from './Header'
34
34
  import { getWordsFromIds } from './shared/utils/wordUtils'
35
35
  import AddLyricsModal from './AddLyricsModal'
36
- import { RestoreFromTrash, OndemandVideo } from '@mui/icons-material'
36
+ import { OndemandVideo } from '@mui/icons-material'
37
37
  import FindReplaceModal from './FindReplaceModal'
38
38
  import TimingOffsetModal from './TimingOffsetModal'
39
39
  import { applyOffsetToCorrectionData, applyOffsetToSegment } from './shared/utils/timingUtils'
@@ -216,6 +216,7 @@ interface MemoizedHeaderProps {
216
216
  canUndo: boolean
217
217
  canRedo: boolean
218
218
  onUnCorrectAll: () => void
219
+ onResetCorrections: () => void
219
220
  annotationsEnabled: boolean
220
221
  onAnnotationsToggle: (enabled: boolean) => void
221
222
  // Review mode props
@@ -250,6 +251,7 @@ const MemoizedHeader = memo(function MemoizedHeader({
250
251
  canUndo,
251
252
  canRedo,
252
253
  onUnCorrectAll,
254
+ onResetCorrections,
253
255
  annotationsEnabled,
254
256
  onAnnotationsToggle,
255
257
  reviewMode,
@@ -281,6 +283,7 @@ const MemoizedHeader = memo(function MemoizedHeader({
281
283
  canUndo={canUndo}
282
284
  canRedo={canRedo}
283
285
  onUnCorrectAll={onUnCorrectAll}
286
+ onResetCorrections={onResetCorrections}
284
287
  annotationsEnabled={annotationsEnabled}
285
288
  onAnnotationsToggle={onAnnotationsToggle}
286
289
  reviewMode={reviewMode}
@@ -1255,8 +1258,6 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
1255
1258
 
1256
1259
  return (
1257
1260
  <Box sx={{
1258
- p: 1,
1259
- pb: 3,
1260
1261
  maxWidth: '100%',
1261
1262
  overflowX: 'hidden'
1262
1263
  }}>
@@ -1282,6 +1283,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
1282
1283
  canUndo={canUndo}
1283
1284
  canRedo={canRedo}
1284
1285
  onUnCorrectAll={handleUnCorrectAll}
1286
+ onResetCorrections={handleResetCorrections}
1285
1287
  annotationsEnabled={annotationsEnabled}
1286
1288
  onAnnotationsToggle={handleAnnotationsToggle}
1287
1289
  reviewMode={reviewMode}
@@ -1291,7 +1293,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
1291
1293
  onRevertAllCorrections={handleRevertAllCorrections}
1292
1294
  />
1293
1295
 
1294
- <Grid container direction={isMobile ? 'column' : 'row'}>
1296
+ <Grid container direction={isMobile ? 'column' : 'row'} spacing={1}>
1295
1297
  <Grid item xs={12} md={6}>
1296
1298
  <MemoizedTranscriptionView
1297
1299
  data={displayData}
@@ -1316,32 +1318,6 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
1316
1318
  onAcceptCorrection={handleAcceptCorrection}
1317
1319
  onShowCorrectionDetail={handleShowCorrectionDetail}
1318
1320
  />
1319
- {!isReadOnly && apiClient && (
1320
- <Box sx={{
1321
- mt: 2,
1322
- mb: 3,
1323
- display: 'flex',
1324
- justifyContent: 'space-between',
1325
- width: '100%'
1326
- }}>
1327
- <Button
1328
- variant="outlined"
1329
- color="warning"
1330
- onClick={handleResetCorrections}
1331
- startIcon={<RestoreFromTrash />}
1332
- >
1333
- Reset Corrections
1334
- </Button>
1335
- <Button
1336
- variant="contained"
1337
- onClick={handleFinishReview}
1338
- disabled={isReviewComplete}
1339
- endIcon={<OndemandVideo />}
1340
- >
1341
- {isReviewComplete ? 'Review Complete' : 'Preview Video'}
1342
- </Button>
1343
- </Box>
1344
- )}
1345
1321
  </Grid>
1346
1322
  <Grid item xs={12} md={6}>
1347
1323
  <MemoizedReferenceView
@@ -1362,6 +1338,52 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
1362
1338
  </Grid>
1363
1339
  </Grid>
1364
1340
 
1341
+ {/* Spacer for sticky footer */}
1342
+ {!isReadOnly && apiClient && <Box sx={{ height: 64 }} />}
1343
+
1344
+ {/* Sticky footer bar with Preview Video button */}
1345
+ {!isReadOnly && apiClient && (
1346
+ <Box
1347
+ sx={{
1348
+ position: 'fixed',
1349
+ bottom: 0,
1350
+ left: 0,
1351
+ right: 0,
1352
+ bgcolor: 'background.paper',
1353
+ borderTop: 1,
1354
+ borderColor: 'divider',
1355
+ boxShadow: '0 -2px 10px rgba(0,0,0,0.1)',
1356
+ py: 1.5,
1357
+ px: 2,
1358
+ zIndex: 1100,
1359
+ display: 'flex',
1360
+ justifyContent: 'center',
1361
+ alignItems: 'center',
1362
+ gap: 2
1363
+ }}
1364
+ >
1365
+ <Typography
1366
+ variant="body2"
1367
+ sx={{ color: 'text.secondary' }}
1368
+ >
1369
+ Lyrics look good?
1370
+ </Typography>
1371
+ <Button
1372
+ variant="contained"
1373
+ onClick={handleFinishReview}
1374
+ disabled={isReviewComplete}
1375
+ endIcon={<OndemandVideo />}
1376
+ sx={{
1377
+ px: 2.5,
1378
+ py: 0.75,
1379
+ fontWeight: 500
1380
+ }}
1381
+ >
1382
+ {isReviewComplete ? 'Review Complete' : 'Preview Video'}
1383
+ </Button>
1384
+ </Box>
1385
+ )}
1386
+
1365
1387
 
1366
1388
 
1367
1389
  <EditModal
@@ -2,8 +2,7 @@ import { memo } from 'react'
2
2
  import {
3
3
  Box,
4
4
  Button,
5
- Divider,
6
- Stack
5
+ Divider
7
6
  } from '@mui/material'
8
7
  import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
9
8
  import StopIcon from '@mui/icons-material/Stop'
@@ -14,6 +13,7 @@ import EditNoteIcon from '@mui/icons-material/EditNote'
14
13
  import BlockIcon from '@mui/icons-material/Block'
15
14
  import EditIcon from '@mui/icons-material/Edit'
16
15
  import DeleteIcon from '@mui/icons-material/Delete'
16
+ import TouchAppIcon from '@mui/icons-material/TouchApp'
17
17
 
18
18
  interface SyncControlsProps {
19
19
  // Main buttons
@@ -24,21 +24,27 @@ interface SyncControlsProps {
24
24
  onResumeSync: () => void
25
25
  onClearSync: () => void
26
26
  onEditLyrics: () => void
27
-
27
+
28
28
  // Playback controls
29
29
  onPlay: () => void
30
30
  onStop: () => void
31
31
  isPlaying: boolean
32
-
32
+
33
33
  // Word action buttons
34
34
  hasSelectedWords: boolean
35
35
  selectedWordCount: number
36
36
  onUnsyncFromCursor: () => void
37
37
  onEditSelectedWord: () => void
38
38
  onDeleteSelected: () => void
39
-
39
+
40
40
  // Additional state
41
41
  canUnsyncFromCursor: boolean
42
+
43
+ // Mobile tap support
44
+ isMobile?: boolean
45
+ onTapStart?: () => void
46
+ onTapEnd?: () => void
47
+ isTapping?: boolean
42
48
  }
43
49
 
44
50
  const SyncControls = memo(function SyncControls({
@@ -57,12 +63,16 @@ const SyncControls = memo(function SyncControls({
57
63
  onUnsyncFromCursor,
58
64
  onEditSelectedWord,
59
65
  onDeleteSelected,
60
- canUnsyncFromCursor
66
+ canUnsyncFromCursor,
67
+ isMobile = false,
68
+ onTapStart,
69
+ onTapEnd,
70
+ isTapping = false
61
71
  }: SyncControlsProps) {
62
72
  return (
63
- <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
73
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
64
74
  {/* Row 1: Playback & Sync Mode Controls */}
65
- <Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap">
75
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center' }}>
66
76
  {/* Playback controls */}
67
77
  <Button
68
78
  variant="outlined"
@@ -85,7 +95,7 @@ const SyncControls = memo(function SyncControls({
85
95
  Stop
86
96
  </Button>
87
97
 
88
- <Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />
98
+ {!isMobile && <Divider orientation="vertical" flexItem />}
89
99
 
90
100
  {/* Sync Mode controls */}
91
101
  {isManualSyncing ? (
@@ -108,6 +118,27 @@ const SyncControls = memo(function SyncControls({
108
118
  >
109
119
  {isPaused ? 'Resume' : 'Pause'}
110
120
  </Button>
121
+ {/* TAP button for mobile - no spacebar on mobile */}
122
+ {isMobile && !isPaused && onTapStart && onTapEnd && (
123
+ <Button
124
+ variant="contained"
125
+ color={isTapping ? 'warning' : 'success'}
126
+ onTouchStart={(e) => {
127
+ e.preventDefault()
128
+ onTapStart()
129
+ }}
130
+ onTouchEnd={(e) => {
131
+ e.preventDefault()
132
+ onTapEnd()
133
+ }}
134
+ onMouseDown={onTapStart}
135
+ onMouseUp={onTapEnd}
136
+ startIcon={<TouchAppIcon />}
137
+ size="small"
138
+ >
139
+ {isTapping ? 'Release' : 'TAP'}
140
+ </Button>
141
+ )}
111
142
  </>
112
143
  ) : (
113
144
  <Button
@@ -120,10 +151,10 @@ const SyncControls = memo(function SyncControls({
120
151
  Start Sync
121
152
  </Button>
122
153
  )}
123
- </Stack>
154
+ </Box>
124
155
 
125
156
  {/* Row 2: Editing & Word Actions */}
126
- <Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap">
157
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center' }}>
127
158
  <Button
128
159
  variant="outlined"
129
160
  color="warning"
@@ -145,7 +176,7 @@ const SyncControls = memo(function SyncControls({
145
176
  Edit Lyrics
146
177
  </Button>
147
178
 
148
- <Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />
179
+ {!isMobile && <Divider orientation="vertical" flexItem />}
149
180
 
150
181
  <Button
151
182
  variant="outlined"
@@ -177,7 +208,7 @@ const SyncControls = memo(function SyncControls({
177
208
  >
178
209
  Delete{hasSelectedWords && selectedWordCount > 0 ? ` (${selectedWordCount})` : ''}
179
210
  </Button>
180
- </Stack>
211
+ </Box>
181
212
  </Box>
182
213
  )
183
214
  })