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.
Files changed (339) hide show
  1. backend/Dockerfile.base +1 -0
  2. backend/api/routes/admin.py +226 -3
  3. backend/api/routes/push.py +238 -0
  4. backend/api/routes/users.py +14 -3
  5. backend/config.py +12 -1
  6. backend/main.py +2 -1
  7. backend/models/job.py +4 -0
  8. backend/models/user.py +20 -2
  9. backend/services/encoding_interface.py +4 -0
  10. backend/services/gce_encoding/main.py +22 -8
  11. backend/services/job_manager.py +68 -11
  12. backend/services/job_notification_service.py +4 -21
  13. backend/services/push_notification_service.py +409 -0
  14. backend/services/stripe_service.py +2 -2
  15. backend/tests/conftest.py +2 -1
  16. backend/tests/test_admin_delete_outputs.py +352 -0
  17. backend/tests/test_gce_encoding_worker.py +229 -0
  18. backend/tests/test_impersonation.py +18 -3
  19. backend/tests/test_job_notification_service.py +24 -58
  20. backend/tests/test_push_notification_service.py +460 -0
  21. backend/tests/test_push_routes.py +357 -0
  22. backend/tests/test_stripe_service.py +205 -0
  23. backend/tests/test_video_worker_orchestrator.py +189 -0
  24. backend/workers/video_worker_orchestrator.py +23 -0
  25. karaoke_gen/instrumental_review/server.py +145 -35
  26. karaoke_gen/nextjs_frontend/__init__.py +98 -0
  27. karaoke_gen/nextjs_frontend/out/404/index.html +1 -0
  28. karaoke_gen/nextjs_frontend/out/404.html +1 -0
  29. karaoke_gen/nextjs_frontend/out/__next.__PAGE__.txt +9 -0
  30. karaoke_gen/nextjs_frontend/out/__next._full.txt +22 -0
  31. karaoke_gen/nextjs_frontend/out/__next._head.txt +8 -0
  32. karaoke_gen/nextjs_frontend/out/__next._index.txt +9 -0
  33. karaoke_gen/nextjs_frontend/out/__next._tree.txt +2 -0
  34. karaoke_gen/nextjs_frontend/out/_next/static/chunks/01a7f8fe40f1ff47.js +1 -0
  35. karaoke_gen/nextjs_frontend/out/_next/static/chunks/112f346e31f991df.js +4 -0
  36. karaoke_gen/nextjs_frontend/out/_next/static/chunks/16d1a4dd9d8a873a.js +3 -0
  37. karaoke_gen/nextjs_frontend/out/_next/static/chunks/1ab85c362b8b0e86.js +9 -0
  38. karaoke_gen/nextjs_frontend/out/_next/static/chunks/247eb132b7f7b574.js +1 -0
  39. karaoke_gen/nextjs_frontend/out/_next/static/chunks/2b80d15cc95e4818.js +1 -0
  40. karaoke_gen/nextjs_frontend/out/_next/static/chunks/32c7eba5cd46c1bc.js +7 -0
  41. karaoke_gen/nextjs_frontend/out/_next/static/chunks/483f26794eae53d0.js +1 -0
  42. karaoke_gen/nextjs_frontend/out/_next/static/chunks/550c3b02e85f196a.js +1 -0
  43. karaoke_gen/nextjs_frontend/out/_next/static/chunks/55c5ade44387bef8.js +1 -0
  44. karaoke_gen/nextjs_frontend/out/_next/static/chunks/5628d92b5893add2.css +1 -0
  45. karaoke_gen/nextjs_frontend/out/_next/static/chunks/56ebf7665e4341c8.js +7 -0
  46. karaoke_gen/nextjs_frontend/out/_next/static/chunks/5997132b61dec430.js +1 -0
  47. karaoke_gen/nextjs_frontend/out/_next/static/chunks/5ea55255bce3eb9e.js +5 -0
  48. karaoke_gen/nextjs_frontend/out/_next/static/chunks/5eda89a57490b3cd.js +1 -0
  49. karaoke_gen/nextjs_frontend/out/_next/static/chunks/692f5d9e0d700c76.js +3 -0
  50. karaoke_gen/nextjs_frontend/out/_next/static/chunks/71d7a05b14f9f0f4.js +1 -0
  51. karaoke_gen/nextjs_frontend/out/_next/static/chunks/81ac355749ef3302.js +1 -0
  52. karaoke_gen/nextjs_frontend/out/_next/static/chunks/95f7e5934dbb0e5d.js +1 -0
  53. karaoke_gen/nextjs_frontend/out/_next/static/chunks/9bce8f19eaa46940.js +1 -0
  54. karaoke_gen/nextjs_frontend/out/_next/static/chunks/a6dad97d9634a72d.js +1 -0
  55. karaoke_gen/nextjs_frontend/out/_next/static/chunks/a9ed54eed3e14c92.js +2 -0
  56. karaoke_gen/nextjs_frontend/out/_next/static/chunks/b35cd41238ecfb17.js +1 -0
  57. karaoke_gen/nextjs_frontend/out/_next/static/chunks/b5bc3c3d5ebd49eb.js +1 -0
  58. karaoke_gen/nextjs_frontend/out/_next/static/chunks/b5c078c08db5ae32.js +5 -0
  59. karaoke_gen/nextjs_frontend/out/_next/static/chunks/be9c44a178104187.js +1 -0
  60. karaoke_gen/nextjs_frontend/out/_next/static/chunks/c4c840e18cb4861c.js +1 -0
  61. karaoke_gen/nextjs_frontend/out/_next/static/chunks/c645af7d6b65f73e.js +1 -0
  62. karaoke_gen/nextjs_frontend/out/_next/static/chunks/d2c5e2575df784d4.js +1 -0
  63. karaoke_gen/nextjs_frontend/out/_next/static/chunks/d30af02b96d81462.js +1 -0
  64. karaoke_gen/nextjs_frontend/out/_next/static/chunks/d9bdf64f4ec1e9b7.js +7 -0
  65. karaoke_gen/nextjs_frontend/out/_next/static/chunks/dcde6ed684dacd0e.js +5 -0
  66. karaoke_gen/nextjs_frontend/out/_next/static/chunks/e422cbe931246000.js +1 -0
  67. karaoke_gen/nextjs_frontend/out/_next/static/chunks/e483af34fc792d38.js +1 -0
  68. karaoke_gen/nextjs_frontend/out/_next/static/chunks/e57422aad6b897da.js +1 -0
  69. karaoke_gen/nextjs_frontend/out/_next/static/chunks/ef02697fb404726a.js +1 -0
  70. karaoke_gen/nextjs_frontend/out/_next/static/chunks/ff1a16fafef87110.js +1 -0
  71. karaoke_gen/nextjs_frontend/out/_next/static/chunks/turbopack-2d9ca3017a9deedf.js +3 -0
  72. karaoke_gen/nextjs_frontend/out/_next/static/zpw_-rjFIDV5tlPPtnvRI/_buildManifest.js +11 -0
  73. karaoke_gen/nextjs_frontend/out/_next/static/zpw_-rjFIDV5tlPPtnvRI/_clientMiddlewareManifest.json +1 -0
  74. karaoke_gen/nextjs_frontend/out/_next/static/zpw_-rjFIDV5tlPPtnvRI/_ssgManifest.js +1 -0
  75. karaoke_gen/nextjs_frontend/out/_not-found/__next._full.txt +18 -0
  76. karaoke_gen/nextjs_frontend/out/_not-found/__next._head.txt +8 -0
  77. karaoke_gen/nextjs_frontend/out/_not-found/__next._index.txt +9 -0
  78. karaoke_gen/nextjs_frontend/out/_not-found/__next._not-found.__PAGE__.txt +5 -0
  79. karaoke_gen/nextjs_frontend/out/_not-found/__next._not-found.txt +4 -0
  80. karaoke_gen/nextjs_frontend/out/_not-found/__next._tree.txt +2 -0
  81. karaoke_gen/nextjs_frontend/out/_not-found/index.html +1 -0
  82. karaoke_gen/nextjs_frontend/out/_not-found/index.txt +18 -0
  83. karaoke_gen/nextjs_frontend/out/admin/__next._full.txt +25 -0
  84. karaoke_gen/nextjs_frontend/out/admin/__next._head.txt +8 -0
  85. karaoke_gen/nextjs_frontend/out/admin/__next._index.txt +9 -0
  86. karaoke_gen/nextjs_frontend/out/admin/__next._tree.txt +2 -0
  87. karaoke_gen/nextjs_frontend/out/admin/__next.admin.__PAGE__.txt +9 -0
  88. karaoke_gen/nextjs_frontend/out/admin/__next.admin.txt +7 -0
  89. karaoke_gen/nextjs_frontend/out/admin/beta/__next._full.txt +25 -0
  90. karaoke_gen/nextjs_frontend/out/admin/beta/__next._head.txt +8 -0
  91. karaoke_gen/nextjs_frontend/out/admin/beta/__next._index.txt +9 -0
  92. karaoke_gen/nextjs_frontend/out/admin/beta/__next._tree.txt +2 -0
  93. karaoke_gen/nextjs_frontend/out/admin/beta/__next.admin.beta.__PAGE__.txt +9 -0
  94. karaoke_gen/nextjs_frontend/out/admin/beta/__next.admin.beta.txt +4 -0
  95. karaoke_gen/nextjs_frontend/out/admin/beta/__next.admin.txt +7 -0
  96. karaoke_gen/nextjs_frontend/out/admin/beta/index.html +1 -0
  97. karaoke_gen/nextjs_frontend/out/admin/beta/index.txt +25 -0
  98. karaoke_gen/nextjs_frontend/out/admin/index.html +1 -0
  99. karaoke_gen/nextjs_frontend/out/admin/index.txt +25 -0
  100. karaoke_gen/nextjs_frontend/out/admin/jobs/__next._full.txt +25 -0
  101. karaoke_gen/nextjs_frontend/out/admin/jobs/__next._head.txt +8 -0
  102. karaoke_gen/nextjs_frontend/out/admin/jobs/__next._index.txt +9 -0
  103. karaoke_gen/nextjs_frontend/out/admin/jobs/__next._tree.txt +2 -0
  104. karaoke_gen/nextjs_frontend/out/admin/jobs/__next.admin.jobs.__PAGE__.txt +9 -0
  105. karaoke_gen/nextjs_frontend/out/admin/jobs/__next.admin.jobs.txt +4 -0
  106. karaoke_gen/nextjs_frontend/out/admin/jobs/__next.admin.txt +7 -0
  107. karaoke_gen/nextjs_frontend/out/admin/jobs/index.html +1 -0
  108. karaoke_gen/nextjs_frontend/out/admin/jobs/index.txt +25 -0
  109. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._full.txt +25 -0
  110. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._head.txt +8 -0
  111. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._index.txt +9 -0
  112. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._tree.txt +2 -0
  113. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next.admin.rate-limits.__PAGE__.txt +9 -0
  114. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next.admin.rate-limits.txt +4 -0
  115. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next.admin.txt +7 -0
  116. karaoke_gen/nextjs_frontend/out/admin/rate-limits/index.html +1 -0
  117. karaoke_gen/nextjs_frontend/out/admin/rate-limits/index.txt +25 -0
  118. karaoke_gen/nextjs_frontend/out/admin/searches/__next._full.txt +25 -0
  119. karaoke_gen/nextjs_frontend/out/admin/searches/__next._head.txt +8 -0
  120. karaoke_gen/nextjs_frontend/out/admin/searches/__next._index.txt +9 -0
  121. karaoke_gen/nextjs_frontend/out/admin/searches/__next._tree.txt +2 -0
  122. karaoke_gen/nextjs_frontend/out/admin/searches/__next.admin.searches.__PAGE__.txt +9 -0
  123. karaoke_gen/nextjs_frontend/out/admin/searches/__next.admin.searches.txt +4 -0
  124. karaoke_gen/nextjs_frontend/out/admin/searches/__next.admin.txt +7 -0
  125. karaoke_gen/nextjs_frontend/out/admin/searches/index.html +1 -0
  126. karaoke_gen/nextjs_frontend/out/admin/searches/index.txt +25 -0
  127. karaoke_gen/nextjs_frontend/out/admin/users/__next._full.txt +25 -0
  128. karaoke_gen/nextjs_frontend/out/admin/users/__next._head.txt +8 -0
  129. karaoke_gen/nextjs_frontend/out/admin/users/__next._index.txt +9 -0
  130. karaoke_gen/nextjs_frontend/out/admin/users/__next._tree.txt +2 -0
  131. karaoke_gen/nextjs_frontend/out/admin/users/__next.admin.txt +7 -0
  132. karaoke_gen/nextjs_frontend/out/admin/users/__next.admin.users.__PAGE__.txt +9 -0
  133. karaoke_gen/nextjs_frontend/out/admin/users/__next.admin.users.txt +4 -0
  134. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._full.txt +25 -0
  135. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._head.txt +8 -0
  136. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._index.txt +9 -0
  137. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._tree.txt +2 -0
  138. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.txt +7 -0
  139. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.users.detail.__PAGE__.txt +9 -0
  140. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.users.detail.txt +4 -0
  141. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.users.txt +4 -0
  142. karaoke_gen/nextjs_frontend/out/admin/users/detail/index.html +1 -0
  143. karaoke_gen/nextjs_frontend/out/admin/users/detail/index.txt +25 -0
  144. karaoke_gen/nextjs_frontend/out/admin/users/index.html +1 -0
  145. karaoke_gen/nextjs_frontend/out/admin/users/index.txt +25 -0
  146. karaoke_gen/nextjs_frontend/out/app/__next._full.txt +22 -0
  147. karaoke_gen/nextjs_frontend/out/app/__next._head.txt +8 -0
  148. karaoke_gen/nextjs_frontend/out/app/__next._index.txt +9 -0
  149. karaoke_gen/nextjs_frontend/out/app/__next._tree.txt +2 -0
  150. karaoke_gen/nextjs_frontend/out/app/__next.app.__PAGE__.txt +9 -0
  151. karaoke_gen/nextjs_frontend/out/app/__next.app.txt +4 -0
  152. karaoke_gen/nextjs_frontend/out/app/index.html +1 -0
  153. karaoke_gen/nextjs_frontend/out/app/index.txt +22 -0
  154. karaoke_gen/nextjs_frontend/out/app/jobs/__next._full.txt +19 -0
  155. karaoke_gen/nextjs_frontend/out/app/jobs/__next._head.txt +8 -0
  156. karaoke_gen/nextjs_frontend/out/app/jobs/__next._index.txt +9 -0
  157. karaoke_gen/nextjs_frontend/out/app/jobs/__next._tree.txt +2 -0
  158. karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.jobs.$oc$slug.__PAGE__.txt +6 -0
  159. karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.jobs.$oc$slug.txt +4 -0
  160. karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.jobs.txt +4 -0
  161. karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.txt +4 -0
  162. karaoke_gen/nextjs_frontend/out/app/jobs/index.html +1 -0
  163. karaoke_gen/nextjs_frontend/out/app/jobs/index.txt +19 -0
  164. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._full.txt +19 -0
  165. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._head.txt +8 -0
  166. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._index.txt +9 -0
  167. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._tree.txt +2 -0
  168. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.jobs.$oc$slug.__PAGE__.txt +6 -0
  169. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.jobs.$oc$slug.txt +4 -0
  170. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.jobs.txt +4 -0
  171. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.txt +4 -0
  172. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/index.html +1 -0
  173. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/index.txt +19 -0
  174. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._full.txt +19 -0
  175. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._head.txt +8 -0
  176. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._index.txt +9 -0
  177. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._tree.txt +2 -0
  178. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.jobs.$oc$slug.__PAGE__.txt +6 -0
  179. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.jobs.$oc$slug.txt +4 -0
  180. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.jobs.txt +4 -0
  181. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.txt +4 -0
  182. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/index.html +1 -0
  183. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/index.txt +19 -0
  184. karaoke_gen/nextjs_frontend/out/auth/verify/__next._full.txt +22 -0
  185. karaoke_gen/nextjs_frontend/out/auth/verify/__next._head.txt +8 -0
  186. karaoke_gen/nextjs_frontend/out/auth/verify/__next._index.txt +9 -0
  187. karaoke_gen/nextjs_frontend/out/auth/verify/__next._tree.txt +2 -0
  188. karaoke_gen/nextjs_frontend/out/auth/verify/__next.auth.txt +4 -0
  189. karaoke_gen/nextjs_frontend/out/auth/verify/__next.auth.verify.__PAGE__.txt +9 -0
  190. karaoke_gen/nextjs_frontend/out/auth/verify/__next.auth.verify.txt +4 -0
  191. karaoke_gen/nextjs_frontend/out/auth/verify/index.html +1 -0
  192. karaoke_gen/nextjs_frontend/out/auth/verify/index.txt +22 -0
  193. karaoke_gen/nextjs_frontend/out/index.html +1 -0
  194. karaoke_gen/nextjs_frontend/out/index.txt +22 -0
  195. karaoke_gen/nextjs_frontend/out/manifest.webmanifest +31 -0
  196. karaoke_gen/nextjs_frontend/out/order/success/__next._full.txt +22 -0
  197. karaoke_gen/nextjs_frontend/out/order/success/__next._head.txt +8 -0
  198. karaoke_gen/nextjs_frontend/out/order/success/__next._index.txt +9 -0
  199. karaoke_gen/nextjs_frontend/out/order/success/__next._tree.txt +2 -0
  200. karaoke_gen/nextjs_frontend/out/order/success/__next.order.success.__PAGE__.txt +9 -0
  201. karaoke_gen/nextjs_frontend/out/order/success/__next.order.success.txt +4 -0
  202. karaoke_gen/nextjs_frontend/out/order/success/__next.order.txt +4 -0
  203. karaoke_gen/nextjs_frontend/out/order/success/index.html +1 -0
  204. karaoke_gen/nextjs_frontend/out/order/success/index.txt +22 -0
  205. karaoke_gen/nextjs_frontend/out/payment/success/__next._full.txt +22 -0
  206. karaoke_gen/nextjs_frontend/out/payment/success/__next._head.txt +8 -0
  207. karaoke_gen/nextjs_frontend/out/payment/success/__next._index.txt +9 -0
  208. karaoke_gen/nextjs_frontend/out/payment/success/__next._tree.txt +2 -0
  209. karaoke_gen/nextjs_frontend/out/payment/success/__next.payment.success.__PAGE__.txt +9 -0
  210. karaoke_gen/nextjs_frontend/out/payment/success/__next.payment.success.txt +4 -0
  211. karaoke_gen/nextjs_frontend/out/payment/success/__next.payment.txt +4 -0
  212. karaoke_gen/nextjs_frontend/out/payment/success/index.html +1 -0
  213. karaoke_gen/nextjs_frontend/out/payment/success/index.txt +22 -0
  214. karaoke_gen/nextjs_frontend/out/screenshots/email-action_reminder.png +0 -0
  215. karaoke_gen/nextjs_frontend/out/screenshots/email-beta_welcome.png +0 -0
  216. karaoke_gen/nextjs_frontend/out/screenshots/email-job_completion.png +0 -0
  217. karaoke_gen/nextjs_frontend/out/screenshots/example-output.avif +0 -0
  218. karaoke_gen/nextjs_frontend/out/screenshots/homepage-full.png +0 -0
  219. karaoke_gen/nextjs_frontend/out/screenshots/homepage-hero.png +0 -0
  220. karaoke_gen/nextjs_frontend/out/screenshots/instrumental-review.avif +0 -0
  221. karaoke_gen/nextjs_frontend/out/screenshots/instrumental-review.png +0 -0
  222. karaoke_gen/nextjs_frontend/out/screenshots/job-dashboard.avif +0 -0
  223. karaoke_gen/nextjs_frontend/out/screenshots/lyrics-review.avif +0 -0
  224. karaoke_gen/nextjs_frontend/out/screenshots/lyrics-review.png +0 -0
  225. karaoke_gen/nextjs_frontend/out/sw.js +183 -0
  226. karaoke_gen/utils/cli_args.py +3 -3
  227. karaoke_gen/utils/gen_cli.py +4 -0
  228. karaoke_gen/utils/remote_cli.py +8 -40
  229. {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/METADATA +2 -1
  230. {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/RECORD +244 -131
  231. {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/WHEEL +1 -1
  232. lyrics_transcriber/correction/agentic/agent.py +83 -60
  233. lyrics_transcriber/correction/anchor_sequence.py +48 -3
  234. lyrics_transcriber/correction/corrector.py +92 -58
  235. lyrics_transcriber/review/server.py +165 -33
  236. lyrics_transcriber/utils/tracing.py +214 -0
  237. karaoke_gen/instrumental_review/static/index.html +0 -1695
  238. lyrics_transcriber/frontend/.gitignore +0 -24
  239. lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +0 -935
  240. lyrics_transcriber/frontend/.yarnrc.yml +0 -3
  241. lyrics_transcriber/frontend/README.md +0 -50
  242. lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +0 -210
  243. lyrics_transcriber/frontend/__init__.py +0 -25
  244. lyrics_transcriber/frontend/e2e/agentic-corrections.spec.ts +0 -207
  245. lyrics_transcriber/frontend/e2e/fixtures/agentic-correction-data.json +0 -226
  246. lyrics_transcriber/frontend/eslint.config.js +0 -28
  247. lyrics_transcriber/frontend/index.html +0 -22
  248. lyrics_transcriber/frontend/package-lock.json +0 -4553
  249. lyrics_transcriber/frontend/package.json +0 -48
  250. lyrics_transcriber/frontend/playwright.config.ts +0 -69
  251. lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
  252. lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
  253. lyrics_transcriber/frontend/src/App.tsx +0 -243
  254. lyrics_transcriber/frontend/src/api.ts +0 -262
  255. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +0 -111
  256. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +0 -114
  257. lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +0 -204
  258. lyrics_transcriber/frontend/src/components/AppHeader.tsx +0 -65
  259. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +0 -180
  260. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +0 -175
  261. lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +0 -359
  262. lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +0 -281
  263. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +0 -162
  264. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +0 -257
  265. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +0 -94
  266. lyrics_transcriber/frontend/src/components/EditModal.tsx +0 -720
  267. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +0 -592
  268. lyrics_transcriber/frontend/src/components/EditWordList.tsx +0 -431
  269. lyrics_transcriber/frontend/src/components/FileUpload.tsx +0 -77
  270. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +0 -467
  271. lyrics_transcriber/frontend/src/components/Header.tsx +0 -520
  272. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +0 -1526
  273. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +0 -216
  274. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +0 -721
  275. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +0 -80
  276. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +0 -999
  277. lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +0 -51
  278. lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +0 -127
  279. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +0 -67
  280. lyrics_transcriber/frontend/src/components/ModelSelector.tsx +0 -23
  281. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +0 -177
  282. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +0 -268
  283. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +0 -336
  284. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +0 -354
  285. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +0 -64
  286. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +0 -383
  287. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +0 -131
  288. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +0 -266
  289. lyrics_transcriber/frontend/src/components/WordDivider.tsx +0 -191
  290. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +0 -466
  291. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +0 -56
  292. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +0 -89
  293. lyrics_transcriber/frontend/src/components/shared/constants.ts +0 -30
  294. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +0 -180
  295. lyrics_transcriber/frontend/src/components/shared/styles.ts +0 -13
  296. lyrics_transcriber/frontend/src/components/shared/types.js +0 -2
  297. lyrics_transcriber/frontend/src/components/shared/types.ts +0 -135
  298. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +0 -177
  299. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +0 -78
  300. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +0 -75
  301. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +0 -360
  302. lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +0 -110
  303. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +0 -22
  304. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +0 -537
  305. lyrics_transcriber/frontend/src/main.tsx +0 -11
  306. lyrics_transcriber/frontend/src/theme.ts +0 -406
  307. lyrics_transcriber/frontend/src/types/global.d.ts +0 -9
  308. lyrics_transcriber/frontend/src/types.js +0 -2
  309. lyrics_transcriber/frontend/src/types.ts +0 -199
  310. lyrics_transcriber/frontend/src/validation.ts +0 -132
  311. lyrics_transcriber/frontend/src/vite-env.d.ts +0 -1
  312. lyrics_transcriber/frontend/tsconfig.app.json +0 -26
  313. lyrics_transcriber/frontend/tsconfig.json +0 -25
  314. lyrics_transcriber/frontend/tsconfig.node.json +0 -23
  315. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +0 -1
  316. lyrics_transcriber/frontend/update_version.js +0 -11
  317. lyrics_transcriber/frontend/vite.config.d.ts +0 -2
  318. lyrics_transcriber/frontend/vite.config.js +0 -15
  319. lyrics_transcriber/frontend/vite.config.ts +0 -16
  320. lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
  321. lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
  322. lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
  323. lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js +0 -44465
  324. lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +0 -1
  325. lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
  326. lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
  327. lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
  328. lyrics_transcriber/frontend/web_assets/index.html +0 -22
  329. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
  330. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +0 -5
  331. lyrics_transcriber/frontend/yarn.lock +0 -3711
  332. {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/apple-touch-icon.png +0 -0
  333. {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/favicon-16x16.png +0 -0
  334. {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/favicon-32x32.png +0 -0
  335. {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/favicon.ico +0 -0
  336. {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/nomad-karaoke-logo.svg +0 -0
  337. /lyrics_transcriber/frontend/public/nomad-karaoke-logo.png → /karaoke_gen/nextjs_frontend/out/nomad-logo.png +0 -0
  338. {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/entry_points.txt +0 -0
  339. {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, HTMLResponse
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
- """Serve the frontend HTML."""
128
- return HTMLResponse(content=self._get_frontend_html())
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
- @app.get("/api/audio/{stem_type}")
192
- async def stream_audio(stem_type: str):
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
- audio_path = path_map.get(stem_type)
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,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;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>