karaoke-gen 0.90.1__py3-none-any.whl → 0.99.3__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 (197) 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 +835 -0
  11. backend/api/routes/audio_search.py +913 -0
  12. backend/api/routes/auth.py +348 -0
  13. backend/api/routes/file_upload.py +2112 -0
  14. backend/api/routes/health.py +409 -0
  15. backend/api/routes/internal.py +435 -0
  16. backend/api/routes/jobs.py +1629 -0
  17. backend/api/routes/review.py +652 -0
  18. backend/api/routes/themes.py +162 -0
  19. backend/api/routes/users.py +1513 -0
  20. backend/config.py +172 -0
  21. backend/main.py +157 -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 +502 -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 +853 -0
  54. backend/services/job_notification_service.py +271 -0
  55. backend/services/langfuse_preloader.py +98 -0
  56. backend/services/local_encoding_service.py +590 -0
  57. backend/services/local_preview_encoding_service.py +407 -0
  58. backend/services/lyrics_cache_service.py +216 -0
  59. backend/services/metrics.py +413 -0
  60. backend/services/nltk_preloader.py +122 -0
  61. backend/services/packaging_service.py +287 -0
  62. backend/services/rclone_service.py +106 -0
  63. backend/services/spacy_preloader.py +65 -0
  64. backend/services/storage_service.py +209 -0
  65. backend/services/stripe_service.py +371 -0
  66. backend/services/structured_logging.py +254 -0
  67. backend/services/template_service.py +330 -0
  68. backend/services/theme_service.py +469 -0
  69. backend/services/tracing.py +543 -0
  70. backend/services/user_service.py +721 -0
  71. backend/services/worker_service.py +558 -0
  72. backend/services/youtube_service.py +112 -0
  73. backend/services/youtube_upload_service.py +445 -0
  74. backend/tests/__init__.py +4 -0
  75. backend/tests/conftest.py +224 -0
  76. backend/tests/emulator/__init__.py +7 -0
  77. backend/tests/emulator/conftest.py +109 -0
  78. backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
  79. backend/tests/emulator/test_emulator_integration.py +356 -0
  80. backend/tests/emulator/test_style_loading_direct.py +436 -0
  81. backend/tests/emulator/test_worker_logs_direct.py +229 -0
  82. backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
  83. backend/tests/requirements-test.txt +10 -0
  84. backend/tests/requirements.txt +6 -0
  85. backend/tests/test_admin_email_endpoints.py +411 -0
  86. backend/tests/test_api_integration.py +460 -0
  87. backend/tests/test_api_routes.py +93 -0
  88. backend/tests/test_audio_analysis_service.py +294 -0
  89. backend/tests/test_audio_editing_service.py +386 -0
  90. backend/tests/test_audio_search.py +1398 -0
  91. backend/tests/test_audio_services.py +378 -0
  92. backend/tests/test_auth_firestore.py +231 -0
  93. backend/tests/test_config_extended.py +68 -0
  94. backend/tests/test_credential_manager.py +377 -0
  95. backend/tests/test_dependencies.py +54 -0
  96. backend/tests/test_discord_service.py +244 -0
  97. backend/tests/test_distribution_services.py +820 -0
  98. backend/tests/test_dropbox_service.py +472 -0
  99. backend/tests/test_email_service.py +492 -0
  100. backend/tests/test_emulator_integration.py +322 -0
  101. backend/tests/test_encoding_interface.py +412 -0
  102. backend/tests/test_file_upload.py +1739 -0
  103. backend/tests/test_flacfetch_client.py +632 -0
  104. backend/tests/test_gdrive_service.py +524 -0
  105. backend/tests/test_instrumental_api.py +431 -0
  106. backend/tests/test_internal_api.py +343 -0
  107. backend/tests/test_job_creation_regression.py +583 -0
  108. backend/tests/test_job_manager.py +356 -0
  109. backend/tests/test_job_manager_notifications.py +329 -0
  110. backend/tests/test_job_notification_service.py +443 -0
  111. backend/tests/test_jobs_api.py +283 -0
  112. backend/tests/test_local_encoding_service.py +423 -0
  113. backend/tests/test_local_preview_encoding_service.py +567 -0
  114. backend/tests/test_main.py +87 -0
  115. backend/tests/test_models.py +918 -0
  116. backend/tests/test_packaging_service.py +382 -0
  117. backend/tests/test_requests.py +201 -0
  118. backend/tests/test_routes_jobs.py +282 -0
  119. backend/tests/test_routes_review.py +337 -0
  120. backend/tests/test_services.py +556 -0
  121. backend/tests/test_services_extended.py +112 -0
  122. backend/tests/test_spacy_preloader.py +119 -0
  123. backend/tests/test_storage_service.py +448 -0
  124. backend/tests/test_style_upload.py +261 -0
  125. backend/tests/test_template_service.py +295 -0
  126. backend/tests/test_theme_service.py +516 -0
  127. backend/tests/test_unicode_sanitization.py +522 -0
  128. backend/tests/test_upload_api.py +256 -0
  129. backend/tests/test_validate.py +156 -0
  130. backend/tests/test_video_worker_orchestrator.py +847 -0
  131. backend/tests/test_worker_log_subcollection.py +509 -0
  132. backend/tests/test_worker_logging.py +365 -0
  133. backend/tests/test_workers.py +1116 -0
  134. backend/tests/test_workers_extended.py +178 -0
  135. backend/tests/test_youtube_service.py +247 -0
  136. backend/tests/test_youtube_upload_service.py +568 -0
  137. backend/utils/test_data.py +27 -0
  138. backend/validate.py +173 -0
  139. backend/version.py +27 -0
  140. backend/workers/README.md +597 -0
  141. backend/workers/__init__.py +11 -0
  142. backend/workers/audio_worker.py +618 -0
  143. backend/workers/lyrics_worker.py +683 -0
  144. backend/workers/render_video_worker.py +483 -0
  145. backend/workers/screens_worker.py +535 -0
  146. backend/workers/style_helper.py +198 -0
  147. backend/workers/video_worker.py +1277 -0
  148. backend/workers/video_worker_orchestrator.py +701 -0
  149. backend/workers/worker_logging.py +278 -0
  150. karaoke_gen/instrumental_review/static/index.html +7 -4
  151. karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
  152. karaoke_gen/utils/__init__.py +163 -8
  153. karaoke_gen/video_background_processor.py +9 -4
  154. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/METADATA +1 -1
  155. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/RECORD +196 -46
  156. lyrics_transcriber/correction/agentic/agent.py +17 -6
  157. lyrics_transcriber/correction/agentic/providers/config.py +9 -5
  158. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +96 -93
  159. lyrics_transcriber/correction/agentic/providers/model_factory.py +27 -6
  160. lyrics_transcriber/correction/anchor_sequence.py +151 -37
  161. lyrics_transcriber/correction/corrector.py +192 -130
  162. lyrics_transcriber/correction/handlers/syllables_match.py +44 -2
  163. lyrics_transcriber/correction/operations.py +24 -9
  164. lyrics_transcriber/correction/phrase_analyzer.py +18 -0
  165. lyrics_transcriber/frontend/package-lock.json +2 -2
  166. lyrics_transcriber/frontend/package.json +1 -1
  167. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
  168. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
  169. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
  170. lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
  171. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
  172. lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
  173. lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
  174. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
  175. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
  176. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
  177. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
  178. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
  179. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
  180. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
  181. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
  182. lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
  183. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
  184. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
  185. lyrics_transcriber/frontend/src/theme.ts +42 -15
  186. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  187. lyrics_transcriber/frontend/vite.config.js +5 -0
  188. lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
  189. lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
  190. lyrics_transcriber/frontend/web_assets/index.html +6 -2
  191. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
  192. lyrics_transcriber/output/generator.py +17 -3
  193. lyrics_transcriber/output/video.py +60 -95
  194. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
  195. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/WHEEL +0 -0
  196. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/entry_points.txt +0 -0
  197. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/licenses/LICENSE +0 -0
@@ -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
  })}
@@ -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