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.
- backend/.coveragerc +20 -0
- backend/.gitignore +37 -0
- backend/Dockerfile +43 -0
- backend/Dockerfile.base +74 -0
- backend/README.md +242 -0
- backend/__init__.py +0 -0
- backend/api/__init__.py +0 -0
- backend/api/dependencies.py +457 -0
- backend/api/routes/__init__.py +0 -0
- backend/api/routes/admin.py +742 -0
- backend/api/routes/audio_search.py +903 -0
- backend/api/routes/auth.py +348 -0
- backend/api/routes/file_upload.py +2076 -0
- backend/api/routes/health.py +344 -0
- backend/api/routes/internal.py +435 -0
- backend/api/routes/jobs.py +1610 -0
- backend/api/routes/review.py +652 -0
- backend/api/routes/themes.py +162 -0
- backend/api/routes/users.py +1014 -0
- backend/config.py +172 -0
- backend/main.py +133 -0
- backend/middleware/__init__.py +5 -0
- backend/middleware/audit_logging.py +124 -0
- backend/models/__init__.py +0 -0
- backend/models/job.py +519 -0
- backend/models/requests.py +123 -0
- backend/models/theme.py +153 -0
- backend/models/user.py +254 -0
- backend/models/worker_log.py +164 -0
- backend/pyproject.toml +29 -0
- backend/quick-check.sh +93 -0
- backend/requirements.txt +29 -0
- backend/run_tests.sh +60 -0
- backend/services/__init__.py +0 -0
- backend/services/audio_analysis_service.py +243 -0
- backend/services/audio_editing_service.py +278 -0
- backend/services/audio_search_service.py +702 -0
- backend/services/auth_service.py +630 -0
- backend/services/credential_manager.py +792 -0
- backend/services/discord_service.py +172 -0
- backend/services/dropbox_service.py +301 -0
- backend/services/email_service.py +1093 -0
- backend/services/encoding_interface.py +454 -0
- backend/services/encoding_service.py +405 -0
- backend/services/firestore_service.py +512 -0
- backend/services/flacfetch_client.py +573 -0
- backend/services/gce_encoding/README.md +72 -0
- backend/services/gce_encoding/__init__.py +22 -0
- backend/services/gce_encoding/main.py +589 -0
- backend/services/gce_encoding/requirements.txt +16 -0
- backend/services/gdrive_service.py +356 -0
- backend/services/job_logging.py +258 -0
- backend/services/job_manager.py +842 -0
- backend/services/job_notification_service.py +271 -0
- backend/services/local_encoding_service.py +590 -0
- backend/services/local_preview_encoding_service.py +407 -0
- backend/services/lyrics_cache_service.py +216 -0
- backend/services/metrics.py +413 -0
- backend/services/packaging_service.py +287 -0
- backend/services/rclone_service.py +106 -0
- backend/services/storage_service.py +209 -0
- backend/services/stripe_service.py +275 -0
- backend/services/structured_logging.py +254 -0
- backend/services/template_service.py +330 -0
- backend/services/theme_service.py +469 -0
- backend/services/tracing.py +543 -0
- backend/services/user_service.py +721 -0
- backend/services/worker_service.py +558 -0
- backend/services/youtube_service.py +112 -0
- backend/services/youtube_upload_service.py +445 -0
- backend/tests/__init__.py +4 -0
- backend/tests/conftest.py +224 -0
- backend/tests/emulator/__init__.py +7 -0
- backend/tests/emulator/conftest.py +88 -0
- backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
- backend/tests/emulator/test_emulator_integration.py +356 -0
- backend/tests/emulator/test_style_loading_direct.py +436 -0
- backend/tests/emulator/test_worker_logs_direct.py +229 -0
- backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
- backend/tests/requirements-test.txt +10 -0
- backend/tests/requirements.txt +6 -0
- backend/tests/test_admin_email_endpoints.py +411 -0
- backend/tests/test_api_integration.py +460 -0
- backend/tests/test_api_routes.py +93 -0
- backend/tests/test_audio_analysis_service.py +294 -0
- backend/tests/test_audio_editing_service.py +386 -0
- backend/tests/test_audio_search.py +1398 -0
- backend/tests/test_audio_services.py +378 -0
- backend/tests/test_auth_firestore.py +231 -0
- backend/tests/test_config_extended.py +68 -0
- backend/tests/test_credential_manager.py +377 -0
- backend/tests/test_dependencies.py +54 -0
- backend/tests/test_discord_service.py +244 -0
- backend/tests/test_distribution_services.py +820 -0
- backend/tests/test_dropbox_service.py +472 -0
- backend/tests/test_email_service.py +492 -0
- backend/tests/test_emulator_integration.py +322 -0
- backend/tests/test_encoding_interface.py +412 -0
- backend/tests/test_file_upload.py +1739 -0
- backend/tests/test_flacfetch_client.py +632 -0
- backend/tests/test_gdrive_service.py +524 -0
- backend/tests/test_instrumental_api.py +431 -0
- backend/tests/test_internal_api.py +343 -0
- backend/tests/test_job_creation_regression.py +583 -0
- backend/tests/test_job_manager.py +339 -0
- backend/tests/test_job_manager_notifications.py +329 -0
- backend/tests/test_job_notification_service.py +443 -0
- backend/tests/test_jobs_api.py +273 -0
- backend/tests/test_local_encoding_service.py +423 -0
- backend/tests/test_local_preview_encoding_service.py +567 -0
- backend/tests/test_main.py +87 -0
- backend/tests/test_models.py +918 -0
- backend/tests/test_packaging_service.py +382 -0
- backend/tests/test_requests.py +201 -0
- backend/tests/test_routes_jobs.py +282 -0
- backend/tests/test_routes_review.py +337 -0
- backend/tests/test_services.py +556 -0
- backend/tests/test_services_extended.py +112 -0
- backend/tests/test_storage_service.py +448 -0
- backend/tests/test_style_upload.py +261 -0
- backend/tests/test_template_service.py +295 -0
- backend/tests/test_theme_service.py +516 -0
- backend/tests/test_unicode_sanitization.py +522 -0
- backend/tests/test_upload_api.py +256 -0
- backend/tests/test_validate.py +156 -0
- backend/tests/test_video_worker_orchestrator.py +847 -0
- backend/tests/test_worker_log_subcollection.py +509 -0
- backend/tests/test_worker_logging.py +365 -0
- backend/tests/test_workers.py +1116 -0
- backend/tests/test_workers_extended.py +178 -0
- backend/tests/test_youtube_service.py +247 -0
- backend/tests/test_youtube_upload_service.py +568 -0
- backend/validate.py +173 -0
- backend/version.py +27 -0
- backend/workers/README.md +597 -0
- backend/workers/__init__.py +11 -0
- backend/workers/audio_worker.py +618 -0
- backend/workers/lyrics_worker.py +683 -0
- backend/workers/render_video_worker.py +483 -0
- backend/workers/screens_worker.py +525 -0
- backend/workers/style_helper.py +198 -0
- backend/workers/video_worker.py +1277 -0
- backend/workers/video_worker_orchestrator.py +701 -0
- backend/workers/worker_logging.py +278 -0
- karaoke_gen/instrumental_review/static/index.html +7 -4
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
- karaoke_gen/utils/__init__.py +163 -8
- karaoke_gen/video_background_processor.py +9 -4
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/METADATA +1 -1
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/RECORD +186 -41
- lyrics_transcriber/correction/agentic/providers/config.py +9 -5
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +1 -51
- lyrics_transcriber/correction/corrector.py +192 -130
- lyrics_transcriber/correction/operations.py +24 -9
- lyrics_transcriber/frontend/package-lock.json +2 -2
- lyrics_transcriber/frontend/package.json +1 -1
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
- lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
- lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
- lyrics_transcriber/frontend/src/theme.ts +42 -15
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/vite.config.js +5 -0
- lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
- lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/index.html +6 -2
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
- lyrics_transcriber/output/generator.py +17 -3
- lyrics_transcriber/output/video.py +60 -95
- lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 =
|
|
264
|
+
ctx.fillStyle = colors.timelineBg
|
|
253
265
|
ctx.fillRect(0, 0, canvasWidth, height)
|
|
254
266
|
|
|
255
267
|
// Draw time bar background
|
|
256
|
-
ctx.fillStyle =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
320
|
+
ctx.fillStyle = colors.wordBlockSelected
|
|
309
321
|
} else if (isCurrent) {
|
|
310
|
-
ctx.fillStyle =
|
|
322
|
+
ctx.fillStyle = colors.wordBlockCurrent
|
|
311
323
|
} else {
|
|
312
|
-
ctx.fillStyle =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ?
|
|
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 =
|
|
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 =
|
|
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 =
|
|
409
|
-
ctx.strokeStyle =
|
|
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 =
|
|
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 =
|
|
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
|
-
<
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
-
<
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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 =
|
|
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
|
|
734
|
-
|
|
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
|
|
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
|
|
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
|
|
752
|
-
<Box sx={{
|
|
840
|
+
{/* Controls section */}
|
|
841
|
+
<Box sx={{ minHeight: 88, flexShrink: 0 }}>
|
|
753
842
|
<SyncControls
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
43
|
+
backgroundColor: theme.palette.text.secondary,
|
|
41
44
|
'&.subsecond': {
|
|
42
45
|
top: '25px',
|
|
43
46
|
height: '13px',
|
|
44
|
-
backgroundColor: theme.palette.
|
|
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.
|
|
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=
|
|
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:
|
|
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: '
|
|
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: '
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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:
|
|
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}
|