karaoke-gen 0.103.1__py3-none-any.whl → 0.107.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/Dockerfile.base +1 -0
- backend/api/routes/admin.py +226 -3
- backend/api/routes/push.py +238 -0
- backend/api/routes/users.py +14 -3
- backend/config.py +12 -1
- backend/main.py +2 -1
- backend/models/job.py +4 -0
- backend/models/user.py +20 -2
- backend/services/encoding_interface.py +4 -0
- backend/services/gce_encoding/main.py +22 -8
- backend/services/job_manager.py +68 -11
- backend/services/job_notification_service.py +4 -21
- backend/services/push_notification_service.py +409 -0
- backend/services/stripe_service.py +2 -2
- backend/tests/conftest.py +2 -1
- backend/tests/test_admin_delete_outputs.py +352 -0
- backend/tests/test_gce_encoding_worker.py +229 -0
- backend/tests/test_impersonation.py +18 -3
- backend/tests/test_job_notification_service.py +24 -58
- backend/tests/test_push_notification_service.py +460 -0
- backend/tests/test_push_routes.py +357 -0
- backend/tests/test_stripe_service.py +205 -0
- backend/tests/test_video_worker_orchestrator.py +189 -0
- backend/workers/video_worker_orchestrator.py +23 -0
- karaoke_gen/instrumental_review/server.py +145 -35
- karaoke_gen/nextjs_frontend/__init__.py +98 -0
- karaoke_gen/nextjs_frontend/out/404/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/404.html +1 -0
- karaoke_gen/nextjs_frontend/out/__next.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/__next._full.txt +22 -0
- karaoke_gen/nextjs_frontend/out/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/01a7f8fe40f1ff47.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/112f346e31f991df.js +4 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/16d1a4dd9d8a873a.js +3 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/1ab85c362b8b0e86.js +9 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/247eb132b7f7b574.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/2b80d15cc95e4818.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/32c7eba5cd46c1bc.js +7 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/483f26794eae53d0.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/550c3b02e85f196a.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/55c5ade44387bef8.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/5628d92b5893add2.css +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/56ebf7665e4341c8.js +7 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/5997132b61dec430.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/5ea55255bce3eb9e.js +5 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/5eda89a57490b3cd.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/692f5d9e0d700c76.js +3 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/71d7a05b14f9f0f4.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/81ac355749ef3302.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/95f7e5934dbb0e5d.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/9bce8f19eaa46940.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/a6dad97d9634a72d.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/a9ed54eed3e14c92.js +2 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/b35cd41238ecfb17.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/b5bc3c3d5ebd49eb.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/b5c078c08db5ae32.js +5 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/be9c44a178104187.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/c4c840e18cb4861c.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/c645af7d6b65f73e.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/d2c5e2575df784d4.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/d30af02b96d81462.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/d9bdf64f4ec1e9b7.js +7 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/dcde6ed684dacd0e.js +5 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/e422cbe931246000.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/e483af34fc792d38.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/e57422aad6b897da.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/ef02697fb404726a.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/ff1a16fafef87110.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/turbopack-2d9ca3017a9deedf.js +3 -0
- karaoke_gen/nextjs_frontend/out/_next/static/zpw_-rjFIDV5tlPPtnvRI/_buildManifest.js +11 -0
- karaoke_gen/nextjs_frontend/out/_next/static/zpw_-rjFIDV5tlPPtnvRI/_clientMiddlewareManifest.json +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/zpw_-rjFIDV5tlPPtnvRI/_ssgManifest.js +1 -0
- karaoke_gen/nextjs_frontend/out/_not-found/__next._full.txt +18 -0
- karaoke_gen/nextjs_frontend/out/_not-found/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/_not-found/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/_not-found/__next._not-found.__PAGE__.txt +5 -0
- karaoke_gen/nextjs_frontend/out/_not-found/__next._not-found.txt +4 -0
- karaoke_gen/nextjs_frontend/out/_not-found/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/_not-found/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/_not-found/index.txt +18 -0
- karaoke_gen/nextjs_frontend/out/admin/__next._full.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/admin/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/admin/__next.admin.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/__next.admin.txt +7 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/__next._full.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/__next.admin.beta.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/__next.admin.beta.txt +4 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/__next.admin.txt +7 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/index.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/admin/index.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/__next._full.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/__next.admin.jobs.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/__next.admin.jobs.txt +4 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/__next.admin.txt +7 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/index.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._full.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next.admin.rate-limits.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next.admin.rate-limits.txt +4 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next.admin.txt +7 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/index.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/__next._full.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/__next.admin.searches.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/__next.admin.searches.txt +4 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/__next.admin.txt +7 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/index.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/users/__next._full.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/users/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/admin/users/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/users/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/admin/users/__next.admin.txt +7 -0
- karaoke_gen/nextjs_frontend/out/admin/users/__next.admin.users.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/users/__next.admin.users.txt +4 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._full.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.txt +7 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.users.detail.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.users.detail.txt +4 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.users.txt +4 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/index.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/users/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/admin/users/index.txt +25 -0
- karaoke_gen/nextjs_frontend/out/app/__next._full.txt +22 -0
- karaoke_gen/nextjs_frontend/out/app/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/app/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/app/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/app/__next.app.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/app/__next.app.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/app/index.txt +22 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next._full.txt +19 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.jobs.$oc$slug.__PAGE__.txt +6 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.jobs.$oc$slug.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.jobs.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/index.txt +19 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._full.txt +19 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.jobs.$oc$slug.__PAGE__.txt +6 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.jobs.$oc$slug.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.jobs.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/index.txt +19 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._full.txt +19 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.jobs.$oc$slug.__PAGE__.txt +6 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.jobs.$oc$slug.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.jobs.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/index.txt +19 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/__next._full.txt +22 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/__next.auth.txt +4 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/__next.auth.verify.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/__next.auth.verify.txt +4 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/index.txt +22 -0
- karaoke_gen/nextjs_frontend/out/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/index.txt +22 -0
- karaoke_gen/nextjs_frontend/out/manifest.webmanifest +31 -0
- karaoke_gen/nextjs_frontend/out/order/success/__next._full.txt +22 -0
- karaoke_gen/nextjs_frontend/out/order/success/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/order/success/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/order/success/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/order/success/__next.order.success.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/order/success/__next.order.success.txt +4 -0
- karaoke_gen/nextjs_frontend/out/order/success/__next.order.txt +4 -0
- karaoke_gen/nextjs_frontend/out/order/success/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/order/success/index.txt +22 -0
- karaoke_gen/nextjs_frontend/out/payment/success/__next._full.txt +22 -0
- karaoke_gen/nextjs_frontend/out/payment/success/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/payment/success/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/payment/success/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/payment/success/__next.payment.success.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/payment/success/__next.payment.success.txt +4 -0
- karaoke_gen/nextjs_frontend/out/payment/success/__next.payment.txt +4 -0
- karaoke_gen/nextjs_frontend/out/payment/success/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/payment/success/index.txt +22 -0
- karaoke_gen/nextjs_frontend/out/screenshots/email-action_reminder.png +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/email-beta_welcome.png +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/email-job_completion.png +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/example-output.avif +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/homepage-full.png +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/homepage-hero.png +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/instrumental-review.avif +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/instrumental-review.png +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/job-dashboard.avif +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/lyrics-review.avif +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/lyrics-review.png +0 -0
- karaoke_gen/nextjs_frontend/out/sw.js +183 -0
- karaoke_gen/utils/cli_args.py +3 -3
- karaoke_gen/utils/gen_cli.py +4 -0
- karaoke_gen/utils/remote_cli.py +8 -40
- {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/METADATA +2 -1
- {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/RECORD +244 -131
- {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/WHEEL +1 -1
- lyrics_transcriber/correction/agentic/agent.py +83 -60
- lyrics_transcriber/correction/anchor_sequence.py +48 -3
- lyrics_transcriber/correction/corrector.py +92 -58
- lyrics_transcriber/review/server.py +165 -33
- lyrics_transcriber/utils/tracing.py +214 -0
- karaoke_gen/instrumental_review/static/index.html +0 -1695
- lyrics_transcriber/frontend/.gitignore +0 -24
- lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +0 -935
- lyrics_transcriber/frontend/.yarnrc.yml +0 -3
- lyrics_transcriber/frontend/README.md +0 -50
- lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +0 -210
- lyrics_transcriber/frontend/__init__.py +0 -25
- lyrics_transcriber/frontend/e2e/agentic-corrections.spec.ts +0 -207
- lyrics_transcriber/frontend/e2e/fixtures/agentic-correction-data.json +0 -226
- lyrics_transcriber/frontend/eslint.config.js +0 -28
- lyrics_transcriber/frontend/index.html +0 -22
- lyrics_transcriber/frontend/package-lock.json +0 -4553
- lyrics_transcriber/frontend/package.json +0 -48
- lyrics_transcriber/frontend/playwright.config.ts +0 -69
- lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/src/App.tsx +0 -243
- lyrics_transcriber/frontend/src/api.ts +0 -262
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +0 -111
- lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +0 -114
- lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +0 -204
- lyrics_transcriber/frontend/src/components/AppHeader.tsx +0 -65
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +0 -180
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +0 -175
- lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +0 -359
- lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +0 -281
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +0 -162
- lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +0 -257
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +0 -94
- lyrics_transcriber/frontend/src/components/EditModal.tsx +0 -720
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +0 -592
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +0 -431
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +0 -77
- lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +0 -467
- lyrics_transcriber/frontend/src/components/Header.tsx +0 -520
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +0 -1526
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +0 -216
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +0 -721
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +0 -80
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +0 -999
- lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +0 -51
- lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +0 -127
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +0 -67
- lyrics_transcriber/frontend/src/components/ModelSelector.tsx +0 -23
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +0 -177
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +0 -268
- lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +0 -336
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +0 -354
- lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +0 -64
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +0 -383
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +0 -131
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +0 -266
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +0 -191
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +0 -466
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +0 -56
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +0 -89
- lyrics_transcriber/frontend/src/components/shared/constants.ts +0 -30
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +0 -180
- lyrics_transcriber/frontend/src/components/shared/styles.ts +0 -13
- lyrics_transcriber/frontend/src/components/shared/types.js +0 -2
- lyrics_transcriber/frontend/src/components/shared/types.ts +0 -135
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +0 -177
- lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +0 -78
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +0 -75
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +0 -360
- lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +0 -110
- lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +0 -22
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +0 -537
- lyrics_transcriber/frontend/src/main.tsx +0 -11
- lyrics_transcriber/frontend/src/theme.ts +0 -406
- lyrics_transcriber/frontend/src/types/global.d.ts +0 -9
- lyrics_transcriber/frontend/src/types.js +0 -2
- lyrics_transcriber/frontend/src/types.ts +0 -199
- lyrics_transcriber/frontend/src/validation.ts +0 -132
- lyrics_transcriber/frontend/src/vite-env.d.ts +0 -1
- lyrics_transcriber/frontend/tsconfig.app.json +0 -26
- lyrics_transcriber/frontend/tsconfig.json +0 -25
- lyrics_transcriber/frontend/tsconfig.node.json +0 -23
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +0 -1
- lyrics_transcriber/frontend/update_version.js +0 -11
- lyrics_transcriber/frontend/vite.config.d.ts +0 -2
- lyrics_transcriber/frontend/vite.config.js +0 -15
- lyrics_transcriber/frontend/vite.config.ts +0 -16
- lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
- lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js +0 -44465
- lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +0 -1
- lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
- lyrics_transcriber/frontend/web_assets/index.html +0 -22
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +0 -5
- lyrics_transcriber/frontend/yarn.lock +0 -3711
- {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/apple-touch-icon.png +0 -0
- {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/favicon-16x16.png +0 -0
- {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/favicon-32x32.png +0 -0
- {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/favicon.ico +0 -0
- {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/nomad-karaoke-logo.svg +0 -0
- /lyrics_transcriber/frontend/public/nomad-karaoke-logo.png → /karaoke_gen/nextjs_frontend/out/nomad-logo.png +0 -0
- {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -845,3 +845,192 @@ class TestCreateOrchestratorConfigFromJob:
|
|
|
845
845
|
)
|
|
846
846
|
|
|
847
847
|
assert config.instrumental_audio_path == "/tmp/test/Test Artist - Test Title (Instrumental User).mp3"
|
|
848
|
+
|
|
849
|
+
def test_create_config_passes_instrumental_selection(self):
|
|
850
|
+
"""Test that instrumental_selection is passed through to OrchestratorConfig.
|
|
851
|
+
|
|
852
|
+
This is a REGRESSION TEST for the bug where orchestrator -> GCE encoding
|
|
853
|
+
path did not pass instrumental_selection, causing GCE worker to default
|
|
854
|
+
to 'clean' even when user selected 'with_backing'.
|
|
855
|
+
|
|
856
|
+
The bug was:
|
|
857
|
+
- PR #271 fixed GCE worker to READ instrumental_selection from config
|
|
858
|
+
- But the orchestrator path (encoding_interface.py) was never updated to SEND it
|
|
859
|
+
- The legacy path (video_worker.py _encode_via_gce) was already correct
|
|
860
|
+
- So the bug only manifested when USE_NEW_ORCHESTRATOR=true (the default)
|
|
861
|
+
|
|
862
|
+
See: fix(gce): Respect user's instrumental selection in GCE encoding worker (#271)
|
|
863
|
+
"""
|
|
864
|
+
job = MagicMock()
|
|
865
|
+
job.job_id = "test-123"
|
|
866
|
+
job.artist = "Test Artist"
|
|
867
|
+
job.title = "Test Title"
|
|
868
|
+
job.state_data = {"instrumental_selection": "with_backing"} # User selected backing vocals
|
|
869
|
+
job.enable_cdg = False
|
|
870
|
+
job.enable_txt = False
|
|
871
|
+
job.enable_youtube_upload = False
|
|
872
|
+
job.brand_prefix = None
|
|
873
|
+
job.discord_webhook_url = None
|
|
874
|
+
job.youtube_description_template = None
|
|
875
|
+
job.dropbox_path = None
|
|
876
|
+
job.gdrive_folder_id = None
|
|
877
|
+
job.keep_brand_code = None
|
|
878
|
+
job.existing_instrumental_gcs_path = None
|
|
879
|
+
|
|
880
|
+
config = create_orchestrator_config_from_job(
|
|
881
|
+
job=job,
|
|
882
|
+
temp_dir="/tmp/test",
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
# CRITICAL: instrumental_selection must be passed to OrchestratorConfig
|
|
886
|
+
# If this fails, the GCE worker will default to 'clean' and ignore user's selection
|
|
887
|
+
assert config.instrumental_selection == "with_backing", \
|
|
888
|
+
"instrumental_selection must be passed from job.state_data to OrchestratorConfig"
|
|
889
|
+
|
|
890
|
+
# Also verify the instrumental path uses "Backing" not "Clean"
|
|
891
|
+
assert "Backing" in config.instrumental_audio_path, \
|
|
892
|
+
"When with_backing is selected, instrumental path should contain 'Backing'"
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
class TestInstrumentalSelectionEndToEnd:
|
|
896
|
+
"""End-to-end tests for instrumental selection flow.
|
|
897
|
+
|
|
898
|
+
These tests verify that instrumental_selection flows correctly from:
|
|
899
|
+
job.state_data -> OrchestratorConfig -> EncodingInput -> GCE encoding_config
|
|
900
|
+
|
|
901
|
+
This test class was added after discovering that PR #271 only fixed the
|
|
902
|
+
GCE worker (receiving side) but not the orchestrator (sending side),
|
|
903
|
+
causing the bug to persist in production where USE_NEW_ORCHESTRATOR=true.
|
|
904
|
+
"""
|
|
905
|
+
|
|
906
|
+
def test_encoding_input_has_instrumental_selection_field(self):
|
|
907
|
+
"""Test that EncodingInput dataclass includes instrumental_selection.
|
|
908
|
+
|
|
909
|
+
Without this field, the orchestrator cannot pass the selection to
|
|
910
|
+
the encoding backend.
|
|
911
|
+
"""
|
|
912
|
+
from backend.services.encoding_interface import EncodingInput
|
|
913
|
+
|
|
914
|
+
# Test with explicit selection
|
|
915
|
+
input_with_backing = EncodingInput(
|
|
916
|
+
title_video_path="/path/title.mov",
|
|
917
|
+
karaoke_video_path="/path/karaoke.mov",
|
|
918
|
+
instrumental_audio_path="/path/audio.flac",
|
|
919
|
+
instrumental_selection="with_backing",
|
|
920
|
+
)
|
|
921
|
+
assert input_with_backing.instrumental_selection == "with_backing"
|
|
922
|
+
|
|
923
|
+
# Test default value
|
|
924
|
+
input_default = EncodingInput(
|
|
925
|
+
title_video_path="/path/title.mov",
|
|
926
|
+
karaoke_video_path="/path/karaoke.mov",
|
|
927
|
+
instrumental_audio_path="/path/audio.flac",
|
|
928
|
+
)
|
|
929
|
+
assert input_default.instrumental_selection == "clean", \
|
|
930
|
+
"Default instrumental_selection should be 'clean' for backward compatibility"
|
|
931
|
+
|
|
932
|
+
def test_orchestrator_config_has_instrumental_selection_field(self):
|
|
933
|
+
"""Test that OrchestratorConfig includes instrumental_selection."""
|
|
934
|
+
config = OrchestratorConfig(
|
|
935
|
+
job_id="test-job",
|
|
936
|
+
artist="Test Artist",
|
|
937
|
+
title="Test Title",
|
|
938
|
+
title_video_path="/path/title.mov",
|
|
939
|
+
karaoke_video_path="/path/karaoke.mov",
|
|
940
|
+
instrumental_audio_path="/path/audio.flac",
|
|
941
|
+
instrumental_selection="with_backing",
|
|
942
|
+
)
|
|
943
|
+
assert config.instrumental_selection == "with_backing"
|
|
944
|
+
|
|
945
|
+
# Test default
|
|
946
|
+
config_default = OrchestratorConfig(
|
|
947
|
+
job_id="test-job",
|
|
948
|
+
artist="Test Artist",
|
|
949
|
+
title="Test Title",
|
|
950
|
+
title_video_path="/path/title.mov",
|
|
951
|
+
karaoke_video_path="/path/karaoke.mov",
|
|
952
|
+
instrumental_audio_path="/path/audio.flac",
|
|
953
|
+
)
|
|
954
|
+
assert config_default.instrumental_selection == "clean"
|
|
955
|
+
|
|
956
|
+
def test_gce_encoding_config_includes_instrumental_selection(self):
|
|
957
|
+
"""Test that GCEEncodingBackend passes instrumental_selection to encoding_config.
|
|
958
|
+
|
|
959
|
+
This is the CRITICAL test that would have caught the bug in PR #271.
|
|
960
|
+
The GCE worker reads config.get("instrumental_selection", "clean"),
|
|
961
|
+
so if we don't send it, it defaults to 'clean' regardless of user selection.
|
|
962
|
+
"""
|
|
963
|
+
from backend.services.encoding_interface import EncodingInput, GCEEncodingBackend
|
|
964
|
+
|
|
965
|
+
backend = GCEEncodingBackend(dry_run=True)
|
|
966
|
+
|
|
967
|
+
# Create input with 'with_backing' selection
|
|
968
|
+
encoding_input = EncodingInput(
|
|
969
|
+
title_video_path="/path/title.mov",
|
|
970
|
+
karaoke_video_path="/path/karaoke.mov",
|
|
971
|
+
instrumental_audio_path="/path/audio.flac",
|
|
972
|
+
artist="Test Artist",
|
|
973
|
+
title="Test Title",
|
|
974
|
+
instrumental_selection="with_backing",
|
|
975
|
+
options={
|
|
976
|
+
"job_id": "test-123",
|
|
977
|
+
"input_gcs_path": "gs://bucket/jobs/test-123/",
|
|
978
|
+
"output_gcs_path": "gs://bucket/jobs/test-123/finals/",
|
|
979
|
+
},
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
# We can't easily test the actual encoding_config dict without mocking
|
|
983
|
+
# the service, but we can verify the input has the right value
|
|
984
|
+
assert encoding_input.instrumental_selection == "with_backing"
|
|
985
|
+
|
|
986
|
+
# The fix ensures GCEEncodingBackend.encode() includes this in encoding_config:
|
|
987
|
+
# encoding_config = {
|
|
988
|
+
# ...
|
|
989
|
+
# "instrumental_selection": input_config.instrumental_selection,
|
|
990
|
+
# }
|
|
991
|
+
|
|
992
|
+
@pytest.mark.asyncio
|
|
993
|
+
async def test_orchestrator_passes_instrumental_selection_to_encoding(self):
|
|
994
|
+
"""Test full flow: orchestrator creates EncodingInput with instrumental_selection.
|
|
995
|
+
|
|
996
|
+
This integration test verifies the complete path:
|
|
997
|
+
OrchestratorConfig.instrumental_selection -> EncodingInput.instrumental_selection
|
|
998
|
+
"""
|
|
999
|
+
config = OrchestratorConfig(
|
|
1000
|
+
job_id="test-job",
|
|
1001
|
+
artist="Test Artist",
|
|
1002
|
+
title="Test Title",
|
|
1003
|
+
title_video_path="/path/title.mov",
|
|
1004
|
+
karaoke_video_path="/path/karaoke.mov",
|
|
1005
|
+
instrumental_audio_path="/path/audio.flac",
|
|
1006
|
+
output_dir="/output",
|
|
1007
|
+
instrumental_selection="with_backing",
|
|
1008
|
+
)
|
|
1009
|
+
orchestrator = VideoWorkerOrchestrator(config)
|
|
1010
|
+
|
|
1011
|
+
# Capture the EncodingInput that gets passed to the backend
|
|
1012
|
+
captured_input = None
|
|
1013
|
+
|
|
1014
|
+
async def capture_encode(encoding_input):
|
|
1015
|
+
nonlocal captured_input
|
|
1016
|
+
captured_input = encoding_input
|
|
1017
|
+
from backend.services.encoding_interface import EncodingOutput
|
|
1018
|
+
return EncodingOutput(
|
|
1019
|
+
success=True,
|
|
1020
|
+
lossless_4k_mp4_path="/output/lossless.mp4",
|
|
1021
|
+
encoding_time_seconds=1.0,
|
|
1022
|
+
encoding_backend="mock",
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
with patch.object(orchestrator, "_get_encoding_backend") as mock_get:
|
|
1026
|
+
mock_backend = MagicMock()
|
|
1027
|
+
mock_backend.name = "mock"
|
|
1028
|
+
mock_backend.encode = capture_encode
|
|
1029
|
+
mock_get.return_value = mock_backend
|
|
1030
|
+
|
|
1031
|
+
await orchestrator._run_encoding()
|
|
1032
|
+
|
|
1033
|
+
# CRITICAL ASSERTION: instrumental_selection must be passed through
|
|
1034
|
+
assert captured_input is not None, "encode() should have been called"
|
|
1035
|
+
assert captured_input.instrumental_selection == "with_backing", \
|
|
1036
|
+
"Orchestrator must pass instrumental_selection to EncodingInput"
|
|
@@ -69,6 +69,9 @@ class OrchestratorConfig:
|
|
|
69
69
|
# Keep existing brand code (for re-processing)
|
|
70
70
|
keep_brand_code: Optional[str] = None
|
|
71
71
|
|
|
72
|
+
# Instrumental selection (clean, with_backing, or custom)
|
|
73
|
+
instrumental_selection: str = "clean"
|
|
74
|
+
|
|
72
75
|
# Encoding backend preference
|
|
73
76
|
encoding_backend: str = "auto" # "auto", "local", "gce"
|
|
74
77
|
|
|
@@ -347,6 +350,7 @@ class VideoWorkerOrchestrator:
|
|
|
347
350
|
title=self.config.title,
|
|
348
351
|
brand_code=self.config.keep_brand_code,
|
|
349
352
|
output_dir=self.config.output_dir,
|
|
353
|
+
instrumental_selection=self.config.instrumental_selection,
|
|
350
354
|
options={
|
|
351
355
|
"job_id": self.config.job_id,
|
|
352
356
|
"input_gcs_path": input_gcs_path,
|
|
@@ -479,6 +483,22 @@ class VideoWorkerOrchestrator:
|
|
|
479
483
|
if self.config.gdrive_folder_id:
|
|
480
484
|
await self._upload_to_gdrive()
|
|
481
485
|
|
|
486
|
+
# Clear outputs_deleted_at if set (job was re-processed after output deletion)
|
|
487
|
+
# Only clear if we actually uploaded something
|
|
488
|
+
uploads_happened = (
|
|
489
|
+
self.result.youtube_url or
|
|
490
|
+
self.result.dropbox_link or
|
|
491
|
+
self.result.gdrive_files
|
|
492
|
+
)
|
|
493
|
+
if uploads_happened and self.job_manager:
|
|
494
|
+
job = self.job_manager.get_job(self.config.job_id)
|
|
495
|
+
if job and job.outputs_deleted_at:
|
|
496
|
+
self.job_manager.update_job(self.config.job_id, {
|
|
497
|
+
"outputs_deleted_at": None,
|
|
498
|
+
"outputs_deleted_by": None,
|
|
499
|
+
})
|
|
500
|
+
self.job_log.info("Cleared outputs_deleted_at flag (job was re-processed)")
|
|
501
|
+
|
|
482
502
|
async def _upload_to_youtube(self):
|
|
483
503
|
"""Upload video to YouTube."""
|
|
484
504
|
self.job_log.info("Uploading to YouTube")
|
|
@@ -718,6 +738,9 @@ def create_orchestrator_config_from_job(
|
|
|
718
738
|
# Keep existing brand code
|
|
719
739
|
keep_brand_code=getattr(job, 'keep_brand_code', None),
|
|
720
740
|
|
|
741
|
+
# Instrumental selection (for GCE encoding)
|
|
742
|
+
instrumental_selection=instrumental_selection,
|
|
743
|
+
|
|
721
744
|
# Encoding backend - auto selects GCE if available
|
|
722
745
|
encoding_backend="auto",
|
|
723
746
|
|
|
@@ -10,7 +10,6 @@ Similar pattern to LyricsTranscriber's ReviewServer.
|
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
12
|
import os
|
|
13
|
-
from pathlib import Path
|
|
14
13
|
import socket
|
|
15
14
|
import threading
|
|
16
15
|
import webbrowser
|
|
@@ -18,7 +17,7 @@ from typing import List, Optional
|
|
|
18
17
|
|
|
19
18
|
from fastapi import FastAPI, HTTPException, UploadFile, File
|
|
20
19
|
from fastapi.middleware.cors import CORSMiddleware
|
|
21
|
-
from fastapi.responses import FileResponse
|
|
20
|
+
from fastapi.responses import FileResponse
|
|
22
21
|
from pydantic import BaseModel
|
|
23
22
|
import shutil
|
|
24
23
|
import tempfile
|
|
@@ -104,7 +103,7 @@ class InstrumentalReviewServer:
|
|
|
104
103
|
def _create_app(self) -> FastAPI:
|
|
105
104
|
"""Create and configure the FastAPI application."""
|
|
106
105
|
app = FastAPI(title="Instrumental Review", docs_url=None, redoc_url=None)
|
|
107
|
-
|
|
106
|
+
|
|
108
107
|
# Configure CORS
|
|
109
108
|
app.add_middleware(
|
|
110
109
|
CORSMiddleware,
|
|
@@ -113,19 +112,138 @@ class InstrumentalReviewServer:
|
|
|
113
112
|
allow_methods=["*"],
|
|
114
113
|
allow_headers=["*"],
|
|
115
114
|
)
|
|
116
|
-
|
|
115
|
+
|
|
116
|
+
# Determine which frontend to use
|
|
117
|
+
self._use_nextjs_frontend = self._setup_nextjs_frontend(app)
|
|
118
|
+
|
|
117
119
|
# Register routes
|
|
118
120
|
self._register_routes(app)
|
|
119
|
-
|
|
121
|
+
|
|
120
122
|
return app
|
|
123
|
+
|
|
124
|
+
def _setup_nextjs_frontend(self, app: FastAPI) -> bool:
|
|
125
|
+
"""Set up the unified Next.js frontend.
|
|
126
|
+
|
|
127
|
+
Returns True if Next.js frontend is set up successfully.
|
|
128
|
+
Raises FileNotFoundError if Next.js assets are not available.
|
|
129
|
+
"""
|
|
130
|
+
import os
|
|
131
|
+
from karaoke_gen.nextjs_frontend import get_nextjs_assets_dir, is_nextjs_frontend_available
|
|
132
|
+
|
|
133
|
+
if not is_nextjs_frontend_available():
|
|
134
|
+
raise FileNotFoundError(
|
|
135
|
+
"Next.js frontend assets not found. Please ensure the frontend is built "
|
|
136
|
+
"and copied to karaoke_gen/nextjs_frontend/out/"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
frontend_dir = str(get_nextjs_assets_dir())
|
|
140
|
+
logger.info(f"Using Next.js frontend from {frontend_dir}")
|
|
141
|
+
|
|
142
|
+
# Mount static files for Next.js assets
|
|
143
|
+
from fastapi.staticfiles import StaticFiles
|
|
144
|
+
app.mount("/_next", StaticFiles(directory=os.path.join(frontend_dir, "_next")), name="nextjs_static")
|
|
145
|
+
|
|
146
|
+
return True
|
|
121
147
|
|
|
122
148
|
def _register_routes(self, app: FastAPI) -> None:
|
|
123
149
|
"""Register API routes."""
|
|
124
|
-
|
|
150
|
+
|
|
125
151
|
@app.get("/")
|
|
126
152
|
async def serve_frontend():
|
|
127
|
-
"""
|
|
128
|
-
|
|
153
|
+
"""Redirect to Next.js instrumental review route."""
|
|
154
|
+
from fastapi.responses import RedirectResponse
|
|
155
|
+
return RedirectResponse(url="/app/jobs/local/instrumental", status_code=302)
|
|
156
|
+
|
|
157
|
+
# Local instrumental route - serve pre-rendered HTML for local mode
|
|
158
|
+
@app.get("/app/jobs/local/instrumental")
|
|
159
|
+
async def serve_local_instrumental():
|
|
160
|
+
"""Serve pre-rendered local instrumental page with patched chunk loading."""
|
|
161
|
+
from karaoke_gen.nextjs_frontend import get_nextjs_assets_dir
|
|
162
|
+
from fastapi.responses import HTMLResponse
|
|
163
|
+
import glob
|
|
164
|
+
frontend_dir = get_nextjs_assets_dir()
|
|
165
|
+
if frontend_dir:
|
|
166
|
+
import os
|
|
167
|
+
local_instrumental_html = os.path.join(str(frontend_dir), "app", "jobs", "local", "instrumental", "index.html")
|
|
168
|
+
if os.path.exists(local_instrumental_html):
|
|
169
|
+
# Read the HTML and inject the missing chunk script
|
|
170
|
+
# This works around a Turbopack static export issue
|
|
171
|
+
with open(local_instrumental_html, 'r', encoding='utf-8') as f:
|
|
172
|
+
html_content = f.read()
|
|
173
|
+
|
|
174
|
+
# Find the chunk containing module 78280 (JobRouterClient)
|
|
175
|
+
chunks_dir = os.path.join(str(frontend_dir), "_next", "static", "chunks")
|
|
176
|
+
for chunk_file in glob.glob(os.path.join(chunks_dir, "*.js")):
|
|
177
|
+
chunk_name = os.path.basename(chunk_file)
|
|
178
|
+
with open(chunk_file, 'r', encoding='utf-8') as cf:
|
|
179
|
+
chunk_content = cf.read(500)
|
|
180
|
+
if ",78280," in chunk_content:
|
|
181
|
+
script_tag = f'<script src="/_next/static/chunks/{chunk_name}" async=""></script>'
|
|
182
|
+
if chunk_name not in html_content:
|
|
183
|
+
html_content = html_content.replace('</head>', f'{script_tag}</head>')
|
|
184
|
+
break
|
|
185
|
+
|
|
186
|
+
return HTMLResponse(content=html_content, media_type="text/html")
|
|
187
|
+
# Fallback to jobs index
|
|
188
|
+
jobs_html = os.path.join(str(frontend_dir), "app", "jobs", "index.html")
|
|
189
|
+
if os.path.exists(jobs_html):
|
|
190
|
+
return FileResponse(jobs_html, media_type="text/html")
|
|
191
|
+
raise HTTPException(status_code=404, detail="Instrumental page not found")
|
|
192
|
+
|
|
193
|
+
# Job routes - serve the jobs page HTML for client-side routing
|
|
194
|
+
@app.get("/app/jobs/{full_path:path}")
|
|
195
|
+
async def serve_jobs_routes(full_path: str):
|
|
196
|
+
"""Serve jobs index.html for all /app/jobs/* routes (SPA routing)."""
|
|
197
|
+
from karaoke_gen.nextjs_frontend import get_nextjs_assets_dir
|
|
198
|
+
frontend_dir = get_nextjs_assets_dir()
|
|
199
|
+
if frontend_dir:
|
|
200
|
+
import os
|
|
201
|
+
jobs_html = os.path.join(str(frontend_dir), "app", "jobs", "index.html")
|
|
202
|
+
if os.path.exists(jobs_html):
|
|
203
|
+
return FileResponse(jobs_html, media_type="text/html")
|
|
204
|
+
raise HTTPException(status_code=404, detail="Jobs page not found")
|
|
205
|
+
|
|
206
|
+
# Other app routes - serve the app index.html
|
|
207
|
+
@app.get("/app/{full_path:path}")
|
|
208
|
+
async def serve_app_routes(full_path: str):
|
|
209
|
+
"""Serve app index.html for other /app/* routes."""
|
|
210
|
+
from karaoke_gen.nextjs_frontend import get_nextjs_assets_dir
|
|
211
|
+
frontend_dir = get_nextjs_assets_dir()
|
|
212
|
+
if frontend_dir:
|
|
213
|
+
import os
|
|
214
|
+
app_html = os.path.join(str(frontend_dir), "app", "index.html")
|
|
215
|
+
if os.path.exists(app_html):
|
|
216
|
+
return FileResponse(app_html, media_type="text/html")
|
|
217
|
+
# Fallback to root index.html
|
|
218
|
+
index_html = os.path.join(str(frontend_dir), "index.html")
|
|
219
|
+
if os.path.exists(index_html):
|
|
220
|
+
return FileResponse(index_html, media_type="text/html")
|
|
221
|
+
raise HTTPException(status_code=404, detail="Frontend not found")
|
|
222
|
+
|
|
223
|
+
# Tenant config endpoint (returns default config for local mode)
|
|
224
|
+
@app.get("/api/tenant/config")
|
|
225
|
+
async def get_tenant_config():
|
|
226
|
+
"""Get tenant configuration for local mode."""
|
|
227
|
+
return {
|
|
228
|
+
"tenant": None,
|
|
229
|
+
"is_default": True
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# Mock job endpoint for local mode (required by unified frontend)
|
|
233
|
+
@app.get("/api/jobs/local")
|
|
234
|
+
async def get_local_job():
|
|
235
|
+
"""Get mock job data for local mode."""
|
|
236
|
+
return {
|
|
237
|
+
"job_id": "local",
|
|
238
|
+
"status": "awaiting_instrumental_selection",
|
|
239
|
+
"progress": 50,
|
|
240
|
+
"created_at": None,
|
|
241
|
+
"updated_at": None,
|
|
242
|
+
"artist": self.base_name.split(" - ")[0] if " - " in self.base_name else "",
|
|
243
|
+
"title": self.base_name.split(" - ")[1] if " - " in self.base_name else self.base_name,
|
|
244
|
+
"user_email": "local@localhost",
|
|
245
|
+
"audio_hash": "local",
|
|
246
|
+
}
|
|
129
247
|
|
|
130
248
|
@app.get("/api/jobs/local/instrumental-analysis")
|
|
131
249
|
async def get_analysis():
|
|
@@ -188,9 +306,8 @@ class InstrumentalReviewServer:
|
|
|
188
306
|
logger.exception(f"Error generating waveform data: {e}")
|
|
189
307
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
190
308
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"""Stream audio file."""
|
|
309
|
+
def _get_audio_path(stem_type: str) -> Optional[str]:
|
|
310
|
+
"""Get the path for a given stem type."""
|
|
194
311
|
path_map = {
|
|
195
312
|
"clean_instrumental": self.clean_instrumental_path,
|
|
196
313
|
"backing_vocals": self.backing_vocals_path,
|
|
@@ -199,11 +316,14 @@ class InstrumentalReviewServer:
|
|
|
199
316
|
"uploaded_instrumental": self.uploaded_instrumental_path,
|
|
200
317
|
"original": self.original_audio_path,
|
|
201
318
|
}
|
|
202
|
-
|
|
203
|
-
|
|
319
|
+
return path_map.get(stem_type)
|
|
320
|
+
|
|
321
|
+
def _stream_audio_file(stem_type: str):
|
|
322
|
+
"""Stream an audio file by stem type."""
|
|
323
|
+
audio_path = _get_audio_path(stem_type)
|
|
204
324
|
if not audio_path or not os.path.exists(audio_path):
|
|
205
325
|
raise HTTPException(status_code=404, detail=f"Audio file not found: {stem_type}")
|
|
206
|
-
|
|
326
|
+
|
|
207
327
|
# Determine content type
|
|
208
328
|
ext = os.path.splitext(audio_path)[1].lower()
|
|
209
329
|
content_types = {
|
|
@@ -212,8 +332,18 @@ class InstrumentalReviewServer:
|
|
|
212
332
|
".wav": "audio/wav",
|
|
213
333
|
}
|
|
214
334
|
content_type = content_types.get(ext, "application/octet-stream")
|
|
215
|
-
|
|
335
|
+
|
|
216
336
|
return FileResponse(audio_path, media_type=content_type)
|
|
337
|
+
|
|
338
|
+
@app.get("/api/audio/{stem_type}")
|
|
339
|
+
async def stream_audio(stem_type: str):
|
|
340
|
+
"""Stream audio file (legacy route)."""
|
|
341
|
+
return _stream_audio_file(stem_type)
|
|
342
|
+
|
|
343
|
+
@app.get("/api/jobs/{job_id}/audio-stream/{stem_type}")
|
|
344
|
+
async def stream_audio_cloud(job_id: str, stem_type: str):
|
|
345
|
+
"""Stream audio file (cloud-compatible route)."""
|
|
346
|
+
return _stream_audio_file(stem_type)
|
|
217
347
|
|
|
218
348
|
@app.get("/api/waveform")
|
|
219
349
|
async def get_waveform_image():
|
|
@@ -338,26 +468,6 @@ class InstrumentalReviewServer:
|
|
|
338
468
|
|
|
339
469
|
return {"status": "success", "selection": request.selection}
|
|
340
470
|
|
|
341
|
-
@staticmethod
|
|
342
|
-
def _get_static_dir() -> Path:
|
|
343
|
-
"""Get the path to the static assets directory."""
|
|
344
|
-
return Path(__file__).parent / "static"
|
|
345
|
-
|
|
346
|
-
def _get_frontend_html(self) -> str:
|
|
347
|
-
"""Return the frontend HTML by reading from the static file."""
|
|
348
|
-
static_file = self._get_static_dir() / "index.html"
|
|
349
|
-
if static_file.exists():
|
|
350
|
-
return static_file.read_text(encoding="utf-8")
|
|
351
|
-
else:
|
|
352
|
-
# Fallback error message if file is missing
|
|
353
|
-
return """<!DOCTYPE html>
|
|
354
|
-
<html>
|
|
355
|
-
<head><title>Error</title></head>
|
|
356
|
-
<body style="background:#1a1a1a;color:#fff;font-family:sans-serif;padding:2rem;">
|
|
357
|
-
<h1>Frontend assets not found</h1>
|
|
358
|
-
<p>The static/index.html file is missing from the instrumental_review module.</p>
|
|
359
|
-
</body>
|
|
360
|
-
</html>"""
|
|
361
471
|
|
|
362
472
|
@staticmethod
|
|
363
473
|
def _is_port_available(host: str, port: int) -> bool:
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Next.js frontend module for karaoke-gen unified web interface.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for serving the consolidated Next.js frontend
|
|
4
|
+
for both lyrics review and instrumental selection in local CLI mode.
|
|
5
|
+
|
|
6
|
+
The Next.js frontend is built with `npm run build` in the frontend/ directory
|
|
7
|
+
and produces a static export in frontend/out/ which can be served by the
|
|
8
|
+
local review servers.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Get the directory containing this module
|
|
17
|
+
_MODULE_DIR = Path(__file__).parent.absolute()
|
|
18
|
+
|
|
19
|
+
# The Next.js static export location relative to the repo root
|
|
20
|
+
# When packaged, it will be at karaoke_gen/nextjs_frontend/out/
|
|
21
|
+
# In development, it's at frontend/out/
|
|
22
|
+
_PACKAGED_DIR = _MODULE_DIR / "out"
|
|
23
|
+
_DEV_DIR = _MODULE_DIR.parent.parent / "frontend" / "out"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_nextjs_assets_dir() -> Optional[Path]:
|
|
27
|
+
"""Get the path to the Next.js static export directory.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Path to the frontend/out directory containing the Next.js static export,
|
|
31
|
+
or None if the assets are not available.
|
|
32
|
+
"""
|
|
33
|
+
# Check packaged location first
|
|
34
|
+
if _PACKAGED_DIR.exists() and (_PACKAGED_DIR / "index.html").exists():
|
|
35
|
+
return _PACKAGED_DIR
|
|
36
|
+
|
|
37
|
+
# Check development location
|
|
38
|
+
if _DEV_DIR.exists() and (_DEV_DIR / "index.html").exists():
|
|
39
|
+
return _DEV_DIR
|
|
40
|
+
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_nextjs_frontend_available() -> bool:
|
|
45
|
+
"""Check if the Next.js frontend is available for serving."""
|
|
46
|
+
return get_nextjs_assets_dir() is not None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_spa_index_html(assets_dir: Path) -> Path:
|
|
50
|
+
"""Get the path to the SPA index.html file for routing."""
|
|
51
|
+
return assets_dir / "index.html"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_route_html_path(assets_dir: Path, route: str) -> Optional[Path]:
|
|
55
|
+
"""Get the path to a specific route's HTML file.
|
|
56
|
+
|
|
57
|
+
For Next.js static export, routes like /app/jobs/local/review
|
|
58
|
+
are pre-rendered to /app/jobs/[...slug].html or similar.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
assets_dir: Path to the Next.js static export directory
|
|
62
|
+
route: The URL route being requested (e.g., "/app/jobs/local/review")
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Path to the HTML file for this route, or None if not found.
|
|
66
|
+
"""
|
|
67
|
+
# Clean the route
|
|
68
|
+
route = route.strip("/")
|
|
69
|
+
if not route:
|
|
70
|
+
return assets_dir / "index.html"
|
|
71
|
+
|
|
72
|
+
# Try exact path match first
|
|
73
|
+
exact_path = assets_dir / route / "index.html"
|
|
74
|
+
if exact_path.exists():
|
|
75
|
+
return exact_path
|
|
76
|
+
|
|
77
|
+
# Try .html extension
|
|
78
|
+
html_path = assets_dir / f"{route}.html"
|
|
79
|
+
if html_path.exists():
|
|
80
|
+
return html_path
|
|
81
|
+
|
|
82
|
+
# For dynamic routes like /app/jobs/[[...slug]],
|
|
83
|
+
# Next.js creates /app/jobs.html or /app/jobs/[[...slug]].html
|
|
84
|
+
# We need to fall back to the catch-all route
|
|
85
|
+
parts = route.split("/")
|
|
86
|
+
for i in range(len(parts), 0, -1):
|
|
87
|
+
parent = "/".join(parts[:i])
|
|
88
|
+
# Try the [...slug] catch-all pattern
|
|
89
|
+
catch_all = assets_dir / parent / "[[...slug]].html"
|
|
90
|
+
if catch_all.exists():
|
|
91
|
+
return catch_all
|
|
92
|
+
# Try parent index
|
|
93
|
+
parent_index = assets_dir / parent / "index.html"
|
|
94
|
+
if parent_index.exists():
|
|
95
|
+
return parent_index
|
|
96
|
+
|
|
97
|
+
# Fallback to main index.html for SPA routing
|
|
98
|
+
return assets_dir / "index.html"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!DOCTYPE html><!--zpw__rjFIDV5tlPPtnvRI--><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/chunks/5628d92b5893add2.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/95f7e5934dbb0e5d.js"/><script src="/_next/static/chunks/112f346e31f991df.js" async=""></script><script src="/_next/static/chunks/a9ed54eed3e14c92.js" async=""></script><script src="/_next/static/chunks/c645af7d6b65f73e.js" async=""></script><script src="/_next/static/chunks/5997132b61dec430.js" async=""></script><script src="/_next/static/chunks/turbopack-2d9ca3017a9deedf.js" async=""></script><script src="/_next/static/chunks/ef02697fb404726a.js" async=""></script><script src="/_next/static/chunks/e483af34fc792d38.js" async=""></script><script src="/_next/static/chunks/247eb132b7f7b574.js" async=""></script><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><title>Nomad Karaoke: Generator</title><meta name="description" content="Generate professional karaoke videos with AI-powered vocal separation and synchronized lyrics"/><link rel="manifest" href="/manifest.webmanifest"/><link rel="icon" href="/favicon.ico" sizes="any"/><link rel="icon" href="/favicon-16x16.png" sizes="16x16" type="image/png"/><link rel="icon" href="/favicon-32x32.png" sizes="32x32" type="image/png"/><link rel="apple-touch-icon" href="/apple-touch-icon.png"/><script src="/_next/static/chunks/a6dad97d9634a72d.js" noModule=""></script></head><body class="font-sans antialiased" style="background:var(--bg);color:var(--text)"><div hidden=""><!--$--><!--/$--></div><script>((a,b,c,d,e,f,g,h)=>{let i=document.documentElement,j=["light","dark"];function k(b){var c;(Array.isArray(a)?a:[a]).forEach(a=>{let c="class"===a,d=c&&f?e.map(a=>f[a]||a):e;c?(i.classList.remove(...d),i.classList.add(f&&f[b]?f[b]:b)):i.setAttribute(a,b)}),c=b,h&&j.includes(c)&&(i.style.colorScheme=c)}if(d)k(d);else try{let a=localStorage.getItem(b)||c,d=g&&"system"===a?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":a;k(d)}catch(a){}})("class","theme","dark",null,["light","dark"],null,true,true)</script><div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--><script src="/_next/static/chunks/95f7e5934dbb0e5d.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[72285,[\"/_next/static/chunks/ef02697fb404726a.js\",\"/_next/static/chunks/e483af34fc792d38.js\",\"/_next/static/chunks/247eb132b7f7b574.js\"],\"ServiceWorkerRegistration\"]\n3:I[89554,[\"/_next/static/chunks/ef02697fb404726a.js\",\"/_next/static/chunks/e483af34fc792d38.js\",\"/_next/static/chunks/247eb132b7f7b574.js\"],\"ThemeProvider\"]\n4:I[57091,[\"/_next/static/chunks/ef02697fb404726a.js\",\"/_next/static/chunks/e483af34fc792d38.js\",\"/_next/static/chunks/247eb132b7f7b574.js\"],\"TenantProvider\"]\n5:I[48030,[\"/_next/static/chunks/ef02697fb404726a.js\",\"/_next/static/chunks/e483af34fc792d38.js\",\"/_next/static/chunks/247eb132b7f7b574.js\"],\"ImpersonationBannerWrapper\"]\n6:I[39756,[\"/_next/static/chunks/ef02697fb404726a.js\",\"/_next/static/chunks/e483af34fc792d38.js\",\"/_next/static/chunks/247eb132b7f7b574.js\"],\"default\"]\n7:I[37457,[\"/_next/static/chunks/ef02697fb404726a.js\",\"/_next/static/chunks/e483af34fc792d38.js\",\"/_next/static/chunks/247eb132b7f7b574.js\"],\"default\"]\n8:I[97367,[\"/_next/static/chunks/ef02697fb404726a.js\",\"/_next/static/chunks/e483af34fc792d38.js\",\"/_next/static/chunks/247eb132b7f7b574.js\"],\"OutletBoundary\"]\n9:\"$Sreact.suspense\"\nb:I[97367,[\"/_next/static/chunks/ef02697fb404726a.js\",\"/_next/static/chunks/e483af34fc792d38.js\",\"/_next/static/chunks/247eb132b7f7b574.js\"],\"ViewportBoundary\"]\nd:I[97367,[\"/_next/static/chunks/ef02697fb404726a.js\",\"/_next/static/chunks/e483af34fc792d38.js\",\"/_next/static/chunks/247eb132b7f7b574.js\"],\"MetadataBoundary\"]\nf:I[68027,[\"/_next/static/chunks/ef02697fb404726a.js\",\"/_next/static/chunks/e483af34fc792d38.js\",\"/_next/static/chunks/247eb132b7f7b574.js\"],\"default\"]\n:HL[\"/_next/static/chunks/5628d92b5893add2.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"zpw_-rjFIDV5tlPPtnvRI\",\"c\":[\"\",\"_not-found\",\"\"],\"q\":\"\",\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/5628d92b5893add2.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/chunks/ef02697fb404726a.js\",\"async\":true,\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-1\",{\"src\":\"/_next/static/chunks/e483af34fc792d38.js\",\"async\":true,\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-2\",{\"src\":\"/_next/static/chunks/247eb132b7f7b574.js\",\"async\":true,\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"suppressHydrationWarning\":true,\"children\":[\"$\",\"body\",null,{\"className\":\"font-sans antialiased\",\"style\":{\"background\":\"var(--bg)\",\"color\":\"var(--text)\"},\"children\":[[\"$\",\"$L2\",null,{}],[\"$\",\"$L3\",null,{\"attribute\":\"class\",\"defaultTheme\":\"dark\",\"enableSystem\":true,\"disableTransitionOnChange\":true,\"children\":[\"$\",\"$L4\",null,{\"children\":[[\"$\",\"$L5\",null,{}],[\"$\",\"$L6\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L7\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}]}]]}]}]]}],{\"children\":[[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L6\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L7\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":\"$0:f:0:1:0:props:children:1:props:children:props:children:1:props:children:props:children:1:props:notFound:0:1:props:style\",\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":\"$0:f:0:1:0:props:children:1:props:children:props:children:1:props:children:props:children:1:props:notFound:0:1:props:children:props:children:1:props:style\",\"children\":404}],[\"$\",\"div\",null,{\"style\":\"$0:f:0:1:0:props:children:1:props:children:props:children:1:props:children:props:children:1:props:notFound:0:1:props:children:props:children:2:props:style\",\"children\":[\"$\",\"h2\",null,{\"style\":\"$0:f:0:1:0:props:children:1:props:children:props:children:1:props:children:props:children:1:props:notFound:0:1:props:children:props:children:2:props:children:props:style\",\"children\":\"This page could not be found.\"}]}]]}]}]],null,[\"$\",\"$L8\",null,{\"children\":[\"$\",\"$9\",null,{\"name\":\"Next.MetadataOutlet\",\"children\":\"$@a\"}]}]]}],{},null,false,false]},null,false,false]},null,false,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[\"$\",\"$Lb\",null,{\"children\":\"$@c\"}],[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$Ld\",null,{\"children\":[\"$\",\"$9\",null,{\"name\":\"Next.Metadata\",\"children\":\"$@e\"}]}]}],null]}],false]],\"m\":\"$undefined\",\"G\":[\"$f\",\"$undefined\"],\"S\":true}\n"])</script><script>self.__next_f.push([1,"c:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n"])</script><script>self.__next_f.push([1,"10:I[27201,[\"/_next/static/chunks/ef02697fb404726a.js\",\"/_next/static/chunks/e483af34fc792d38.js\",\"/_next/static/chunks/247eb132b7f7b574.js\"],\"IconMark\"]\ne:[[\"$\",\"title\",\"0\",{\"children\":\"Nomad Karaoke: Generator\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Generate professional karaoke videos with AI-powered vocal separation and synchronized lyrics\"}],[\"$\",\"link\",\"2\",{\"rel\":\"manifest\",\"href\":\"/manifest.webmanifest\",\"crossOrigin\":\"$undefined\"}],[\"$\",\"link\",\"3\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"sizes\":\"any\"}],[\"$\",\"link\",\"4\",{\"rel\":\"icon\",\"href\":\"/favicon-16x16.png\",\"sizes\":\"16x16\",\"type\":\"image/png\"}],[\"$\",\"link\",\"5\",{\"rel\":\"icon\",\"href\":\"/favicon-32x32.png\",\"sizes\":\"32x32\",\"type\":\"image/png\"}],[\"$\",\"link\",\"6\",{\"rel\":\"apple-touch-icon\",\"href\":\"/apple-touch-icon.png\"}],[\"$\",\"$L10\",\"7\",{}]]\na:null\n"])</script></body></html>
|