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
@@ -1,1695 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Instrumental Review</title>
7
- <style>
8
- :root {
9
- /* Nomad Karaoke brand colors - see docs/BRAND-STYLE-GUIDE.md */
10
- --bg: #0f0f0f;
11
- --card: #1a1a1a;
12
- --card-border: #2a2a2a;
13
- --waveform-bg: #0d1117;
14
- --text: #e5e5e5;
15
- --text-muted: #888;
16
- --primary: #ff7acc;
17
- --primary-hover: #ff5bb8;
18
- --secondary: #252525;
19
- --secondary-hover: #333;
20
- --success: #22c55e;
21
- --warning: #f59e0b;
22
- --danger: #ef4444;
23
- --brand-pink: #ff7acc;
24
- --brand-gold: #ffdf6b;
25
- --brand-purple: #8b5cf6;
26
- --blue: #3b82f6;
27
- }
28
-
29
- * { box-sizing: border-box; margin: 0; padding: 0; }
30
-
31
- body {
32
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
33
- background: var(--bg);
34
- color: var(--text);
35
- line-height: 1.5;
36
- height: 100vh;
37
- overflow: hidden;
38
- }
39
-
40
- .app {
41
- display: flex;
42
- flex-direction: column;
43
- height: 100vh;
44
- padding: 16px 24px;
45
- gap: 12px;
46
- }
47
-
48
- /* Header - compact */
49
- .header {
50
- display: flex;
51
- align-items: center;
52
- justify-content: space-between;
53
- gap: 16px;
54
- flex-shrink: 0;
55
- }
56
-
57
- .header-left {
58
- display: flex;
59
- align-items: center;
60
- gap: 12px;
61
- }
62
-
63
- .logo {
64
- font-size: 1.25rem;
65
- font-weight: 600;
66
- display: inline-flex;
67
- align-items: center;
68
- gap: 8px;
69
- }
70
-
71
- .logo-img {
72
- height: 40px;
73
- width: auto;
74
- }
75
-
76
- .track-info {
77
- font-size: 0.9rem;
78
- color: var(--text-muted);
79
- }
80
-
81
- .header-right {
82
- display: flex;
83
- align-items: center;
84
- gap: 8px;
85
- }
86
-
87
- .badge {
88
- display: inline-flex;
89
- align-items: center;
90
- padding: 4px 10px;
91
- border-radius: 12px;
92
- font-size: 0.7rem;
93
- font-weight: 500;
94
- background: var(--secondary);
95
- color: var(--text-muted);
96
- }
97
-
98
- .badge-warning { background: rgba(245, 158, 11, 0.15); color: var(--warning); }
99
- .badge-success { background: rgba(34, 197, 94, 0.15); color: var(--success); }
100
-
101
- /* Main waveform player */
102
- .waveform-player {
103
- background: var(--card);
104
- border: 1px solid var(--card-border);
105
- border-radius: 12px;
106
- overflow: hidden;
107
- flex: 1;
108
- display: flex;
109
- flex-direction: column;
110
- min-height: 0;
111
- }
112
-
113
- .waveform-toolbar {
114
- display: flex;
115
- align-items: center;
116
- justify-content: space-between;
117
- padding: 10px 16px;
118
- background: var(--secondary);
119
- border-bottom: 1px solid var(--card-border);
120
- gap: 12px;
121
- flex-shrink: 0;
122
- }
123
-
124
- .toolbar-left {
125
- display: flex;
126
- align-items: center;
127
- gap: 8px;
128
- }
129
-
130
- .toolbar-center {
131
- display: flex;
132
- align-items: center;
133
- gap: 6px;
134
- }
135
-
136
- .toolbar-right {
137
- display: flex;
138
- align-items: center;
139
- gap: 12px;
140
- }
141
-
142
- .time-display {
143
- font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
144
- font-size: 0.85rem;
145
- color: var(--text);
146
- min-width: 90px;
147
- }
148
-
149
- .btn {
150
- display: inline-flex;
151
- align-items: center;
152
- justify-content: center;
153
- padding: 6px 12px;
154
- border-radius: 6px;
155
- font-size: 0.8rem;
156
- font-weight: 500;
157
- cursor: pointer;
158
- border: none;
159
- transition: all 0.15s;
160
- gap: 6px;
161
- }
162
-
163
- .btn-icon {
164
- width: 32px;
165
- height: 32px;
166
- padding: 0;
167
- border-radius: 50%;
168
- font-size: 1rem;
169
- }
170
-
171
- .btn-primary { background: var(--primary); color: white; }
172
- .btn-primary:hover { background: var(--primary-hover); }
173
- .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
174
-
175
- .btn-secondary { background: var(--secondary); color: var(--text); border: 1px solid var(--card-border); }
176
- .btn-secondary:hover { background: var(--secondary-hover); }
177
- .btn-secondary.active { background: var(--primary); border-color: var(--primary); }
178
-
179
- .btn-ghost { background: transparent; color: var(--text-muted); }
180
- .btn-ghost:hover { background: var(--secondary); color: var(--text); }
181
- .btn-ghost.active { background: var(--primary); color: white; }
182
-
183
- .btn-sm { padding: 4px 8px; font-size: 0.75rem; }
184
-
185
- .btn-danger { background: var(--danger); color: white; }
186
- .btn-success { background: var(--success); color: white; }
187
-
188
- .audio-toggle-group {
189
- display: flex;
190
- background: var(--bg);
191
- border-radius: 6px;
192
- padding: 2px;
193
- }
194
-
195
- .audio-toggle {
196
- padding: 5px 10px;
197
- border-radius: 4px;
198
- font-size: 0.75rem;
199
- font-weight: 500;
200
- color: var(--text-muted);
201
- background: transparent;
202
- border: none;
203
- cursor: pointer;
204
- transition: all 0.15s;
205
- }
206
-
207
- .audio-toggle:hover { color: var(--text); }
208
- .audio-toggle.active { background: var(--primary); color: white; }
209
-
210
- /* Waveform canvas area */
211
- .waveform-container {
212
- flex: 1;
213
- position: relative;
214
- overflow-x: auto;
215
- overflow-y: hidden;
216
- background: var(--waveform-bg);
217
- min-height: 120px;
218
- }
219
-
220
- .waveform-area {
221
- position: relative;
222
- height: 100%;
223
- min-width: 100%;
224
- }
225
-
226
- #waveform-canvas {
227
- display: block;
228
- height: 100%;
229
- cursor: pointer;
230
- }
231
-
232
- /* Zoom controls */
233
- .zoom-controls {
234
- display: flex;
235
- align-items: center;
236
- gap: 4px;
237
- background: var(--bg);
238
- border-radius: 6px;
239
- padding: 2px;
240
- }
241
-
242
- .zoom-btn {
243
- width: 28px;
244
- height: 28px;
245
- display: flex;
246
- align-items: center;
247
- justify-content: center;
248
- background: transparent;
249
- border: none;
250
- color: var(--text-muted);
251
- cursor: pointer;
252
- border-radius: 4px;
253
- font-size: 0.9rem;
254
- }
255
-
256
- .zoom-btn:hover { background: var(--secondary); color: var(--text); }
257
- .zoom-btn.active { background: var(--primary); color: white; }
258
-
259
- .zoom-label {
260
- font-size: 0.7rem;
261
- color: var(--text-muted);
262
- min-width: 28px;
263
- text-align: center;
264
- }
265
-
266
- /* Upload button */
267
- .upload-btn {
268
- position: relative;
269
- overflow: hidden;
270
- }
271
-
272
- .upload-btn input[type="file"] {
273
- position: absolute;
274
- top: 0;
275
- left: 0;
276
- width: 100%;
277
- height: 100%;
278
- opacity: 0;
279
- cursor: pointer;
280
- }
281
-
282
- .upload-progress {
283
- position: fixed;
284
- top: 50%;
285
- left: 50%;
286
- transform: translate(-50%, -50%);
287
- background: var(--card);
288
- border: 1px solid var(--card-border);
289
- border-radius: 12px;
290
- padding: 24px 32px;
291
- z-index: 1000;
292
- text-align: center;
293
- }
294
-
295
- .upload-overlay {
296
- position: fixed;
297
- top: 0;
298
- left: 0;
299
- width: 100%;
300
- height: 100%;
301
- background: rgba(0,0,0,0.6);
302
- z-index: 999;
303
- }
304
-
305
- .playhead {
306
- position: absolute;
307
- top: 0;
308
- width: 2px;
309
- height: 100%;
310
- background: var(--primary);
311
- pointer-events: none;
312
- z-index: 10;
313
- box-shadow: 0 0 8px var(--primary);
314
- }
315
-
316
- .playhead::after {
317
- content: '';
318
- position: absolute;
319
- top: 0;
320
- left: -4px;
321
- width: 10px;
322
- height: 10px;
323
- background: var(--primary);
324
- border-radius: 50%;
325
- }
326
-
327
- .selection-overlay {
328
- position: absolute;
329
- top: 0;
330
- height: 100%;
331
- background: rgba(59, 130, 246, 0.3);
332
- border: 1px dashed var(--primary);
333
- pointer-events: none;
334
- z-index: 5;
335
- }
336
-
337
- .time-axis {
338
- display: flex;
339
- justify-content: space-between;
340
- padding: 4px 12px;
341
- font-size: 0.7rem;
342
- color: var(--text-muted);
343
- background: rgba(0,0,0,0.4);
344
- flex-shrink: 0;
345
- }
346
-
347
- /* Hidden audio element */
348
- #audio-player { display: none; }
349
-
350
- /* Bottom section */
351
- .bottom-section {
352
- display: flex;
353
- gap: 12px;
354
- flex-shrink: 0;
355
- }
356
-
357
- /* Mute regions panel */
358
- .mute-panel {
359
- flex: 1;
360
- background: var(--card);
361
- border: 1px solid var(--card-border);
362
- border-radius: 10px;
363
- padding: 12px;
364
- display: flex;
365
- flex-direction: column;
366
- gap: 8px;
367
- max-height: 140px;
368
- }
369
-
370
- .mute-panel-header {
371
- display: flex;
372
- align-items: center;
373
- justify-content: space-between;
374
- }
375
-
376
- .mute-panel-title {
377
- font-size: 0.85rem;
378
- font-weight: 600;
379
- }
380
-
381
- .mute-regions-list {
382
- display: flex;
383
- flex-wrap: wrap;
384
- gap: 6px;
385
- overflow-y: auto;
386
- flex: 1;
387
- }
388
-
389
- .mute-region-tag {
390
- display: inline-flex;
391
- align-items: center;
392
- gap: 4px;
393
- padding: 4px 8px;
394
- background: var(--secondary);
395
- border-radius: 4px;
396
- font-size: 0.75rem;
397
- }
398
-
399
- .mute-region-tag button {
400
- background: none;
401
- border: none;
402
- color: var(--text-muted);
403
- cursor: pointer;
404
- padding: 0;
405
- font-size: 0.8rem;
406
- line-height: 1;
407
- }
408
-
409
- .mute-region-tag button:hover { color: var(--danger); }
410
-
411
- .quick-segments {
412
- display: flex;
413
- flex-wrap: wrap;
414
- gap: 4px;
415
- }
416
-
417
- .quick-segment-btn {
418
- padding: 3px 6px;
419
- background: var(--bg);
420
- border: 1px solid var(--card-border);
421
- border-radius: 4px;
422
- font-size: 0.7rem;
423
- color: var(--text-muted);
424
- cursor: pointer;
425
- }
426
-
427
- .quick-segment-btn:hover {
428
- border-color: var(--pink);
429
- color: var(--pink);
430
- }
431
-
432
- /* Selection panel */
433
- .selection-panel {
434
- width: 340px;
435
- background: var(--card);
436
- border: 1px solid var(--card-border);
437
- border-radius: 10px;
438
- padding: 12px;
439
- display: flex;
440
- flex-direction: column;
441
- gap: 8px;
442
- }
443
-
444
- .selection-panel-title {
445
- font-size: 0.85rem;
446
- font-weight: 600;
447
- }
448
-
449
- .selection-options {
450
- display: flex;
451
- flex-direction: column;
452
- gap: 6px;
453
- }
454
-
455
- .selection-option {
456
- display: flex;
457
- align-items: center;
458
- gap: 10px;
459
- padding: 10px 12px;
460
- background: var(--secondary);
461
- border-radius: 8px;
462
- cursor: pointer;
463
- border: 2px solid transparent;
464
- transition: border-color 0.15s;
465
- }
466
-
467
- .selection-option:hover { border-color: var(--card-border); }
468
- .selection-option.selected { border-color: var(--primary); background: rgba(59, 130, 246, 0.1); }
469
-
470
- .selection-option input { display: none; }
471
-
472
- .selection-radio {
473
- width: 16px;
474
- height: 16px;
475
- border: 2px solid var(--text-muted);
476
- border-radius: 50%;
477
- display: flex;
478
- align-items: center;
479
- justify-content: center;
480
- flex-shrink: 0;
481
- }
482
-
483
- .selection-option.selected .selection-radio {
484
- border-color: var(--primary);
485
- }
486
-
487
- .selection-option.selected .selection-radio::after {
488
- content: '';
489
- width: 8px;
490
- height: 8px;
491
- background: var(--primary);
492
- border-radius: 50%;
493
- }
494
-
495
- .selection-label {
496
- flex: 1;
497
- font-size: 0.8rem;
498
- }
499
-
500
- .selection-label-title { font-weight: 500; }
501
- .selection-label-desc { font-size: 0.7rem; color: var(--text-muted); }
502
-
503
- .submit-btn {
504
- margin-top: auto;
505
- width: 100%;
506
- padding: 10px;
507
- font-size: 0.85rem;
508
- }
509
-
510
- /* Keyboard hints */
511
- .kbd {
512
- display: inline-block;
513
- padding: 2px 6px;
514
- background: var(--bg);
515
- border: 1px solid var(--card-border);
516
- border-radius: 4px;
517
- font-family: 'SF Mono', Monaco, monospace;
518
- font-size: 0.65rem;
519
- color: var(--text-muted);
520
- }
521
-
522
- /* Alert */
523
- .alert {
524
- padding: 1rem;
525
- border-radius: 8px;
526
- text-align: center;
527
- }
528
-
529
- .alert-success {
530
- background: rgba(34, 197, 94, 0.15);
531
- border: 1px solid var(--success);
532
- color: var(--success);
533
- }
534
-
535
- .alert-error {
536
- position: fixed;
537
- top: 16px;
538
- left: 50%;
539
- transform: translateX(-50%);
540
- background: rgba(239, 68, 68, 0.95);
541
- color: white;
542
- padding: 10px 20px;
543
- border-radius: 8px;
544
- z-index: 1000;
545
- }
546
-
547
- /* Loading */
548
- .loading {
549
- display: flex;
550
- align-items: center;
551
- justify-content: center;
552
- height: 100%;
553
- }
554
-
555
- .spinner {
556
- width: 32px;
557
- height: 32px;
558
- border: 3px solid var(--card-border);
559
- border-top-color: var(--primary);
560
- border-radius: 50%;
561
- animation: spin 1s linear infinite;
562
- }
563
-
564
- @keyframes spin { to { transform: rotate(360deg); } }
565
-
566
- .hidden { display: none !important; }
567
-
568
- /* Success screen */
569
- .success-screen {
570
- display: flex;
571
- flex-direction: column;
572
- align-items: center;
573
- justify-content: center;
574
- height: 100%;
575
- gap: 16px;
576
- }
577
-
578
- .success-screen h2 {
579
- font-size: 1.5rem;
580
- color: var(--success);
581
- }
582
-
583
- /* Mobile responsiveness */
584
- @media (max-width: 768px) {
585
- .app {
586
- padding: 12px;
587
- gap: 8px;
588
- height: auto;
589
- min-height: 100vh;
590
- overflow-y: auto;
591
- }
592
-
593
- body {
594
- overflow: auto;
595
- }
596
-
597
- .header {
598
- flex-direction: column;
599
- align-items: flex-start;
600
- gap: 8px;
601
- }
602
-
603
- .header-left {
604
- width: 100%;
605
- }
606
-
607
- .header-right {
608
- width: 100%;
609
- justify-content: flex-start;
610
- flex-wrap: wrap;
611
- }
612
-
613
- .logo {
614
- font-size: 1rem;
615
- }
616
-
617
- .logo-img {
618
- height: 32px;
619
- }
620
-
621
- .waveform-player {
622
- flex: none;
623
- min-height: 200px;
624
- }
625
-
626
- .waveform-toolbar {
627
- flex-wrap: wrap;
628
- padding: 8px 12px;
629
- gap: 8px;
630
- }
631
-
632
- .toolbar-left,
633
- .toolbar-center,
634
- .toolbar-right {
635
- flex-wrap: wrap;
636
- }
637
-
638
- .audio-toggle-group {
639
- order: 10;
640
- width: 100%;
641
- justify-content: center;
642
- }
643
-
644
- .bottom-section {
645
- flex-direction: column;
646
- }
647
-
648
- .mute-panel {
649
- max-height: none;
650
- }
651
-
652
- .selection-panel {
653
- width: 100%;
654
- }
655
-
656
- .selection-option {
657
- padding: 12px;
658
- }
659
-
660
- .btn {
661
- min-height: 44px;
662
- padding: 8px 12px;
663
- }
664
-
665
- .btn-icon {
666
- width: 44px;
667
- height: 44px;
668
- }
669
-
670
- .audio-toggle {
671
- padding: 8px 12px;
672
- min-height: 40px;
673
- }
674
-
675
- .zoom-btn {
676
- width: 40px;
677
- height: 40px;
678
- }
679
-
680
- .time-display {
681
- font-size: 0.9rem;
682
- }
683
- }
684
-
685
- @media (max-width: 480px) {
686
- .app {
687
- padding: 8px;
688
- }
689
-
690
- .header-left {
691
- flex-wrap: wrap;
692
- }
693
-
694
- .track-info {
695
- width: 100%;
696
- margin-top: 4px;
697
- }
698
-
699
- .waveform-toolbar {
700
- padding: 6px 8px;
701
- }
702
-
703
- .toolbar-center {
704
- width: 100%;
705
- justify-content: center;
706
- order: -1;
707
- }
708
-
709
- .toolbar-left {
710
- order: 1;
711
- }
712
-
713
- .toolbar-right {
714
- order: 2;
715
- width: 100%;
716
- justify-content: space-between;
717
- }
718
- }
719
- </style>
720
- </head>
721
- <body>
722
- <div class="app" id="app">
723
- <div class="loading">
724
- <div class="spinner"></div>
725
- </div>
726
- </div>
727
-
728
- <script>
729
- // State
730
- let analysisData = null;
731
- let waveformData = null;
732
- let muteRegions = [];
733
- let currentTime = 0;
734
- let duration = 0;
735
- let isPlaying = false;
736
- let isDragging = false;
737
- let dragStartTime = 0;
738
- let selectionStartX = 0;
739
- let activeAudio = 'backing';
740
- let selectedOption = 'with_backing';
741
- let hasCustom = false;
742
- let hasUploaded = false;
743
- let uploadedFilename = '';
744
- let hasOriginal = false;
745
- let zoomLevel = 1;
746
- let animationFrameId = null;
747
- let currentAudioElement = null; // Track audio element reference for listener management
748
-
749
- // Parse URL parameters for cloud mode
750
- const urlParams = new URLSearchParams(window.location.search);
751
- const encodedBaseApiUrl = urlParams.get('baseApiUrl');
752
- const instrumentalToken = urlParams.get('instrumentalToken');
753
-
754
- // Determine API base URL - cloud mode uses provided URL, local mode uses default
755
- const API_BASE = encodedBaseApiUrl
756
- ? decodeURIComponent(encodedBaseApiUrl)
757
- : '/api/jobs/local';
758
-
759
- // Helper to add token to URL if available
760
- function addTokenToUrl(url) {
761
- if (!instrumentalToken) return url;
762
- const separator = url.includes('?') ? '&' : '?';
763
- return `${url}${separator}instrumental_token=${encodeURIComponent(instrumentalToken)}`;
764
- }
765
-
766
- // HTML escape helper to prevent XSS
767
- function escapeHtml(str) {
768
- if (!str) return '';
769
- const div = document.createElement('div');
770
- div.textContent = str;
771
- return div.innerHTML;
772
- }
773
-
774
- // Named event handlers for audio (so they can be added once)
775
- function onAudioPlay() { isPlaying = true; updatePlayButton(); startPlayheadAnimation(); }
776
- function onAudioPause() { isPlaying = false; updatePlayButton(); stopPlayheadAnimation(); }
777
- function onAudioEnded() { isPlaying = false; updatePlayButton(); stopPlayheadAnimation(); }
778
-
779
- // Initialize
780
- async function init() {
781
- try {
782
- const [analysisRes, waveformRes] = await Promise.all([
783
- fetch(addTokenToUrl(`${API_BASE}/instrumental-analysis`)),
784
- fetch(addTokenToUrl(`${API_BASE}/waveform-data?num_points=1000`))
785
- ]);
786
-
787
- if (!analysisRes.ok) throw new Error('Failed to load analysis');
788
- analysisData = await analysisRes.json();
789
-
790
- if (waveformRes.ok) {
791
- waveformData = await waveformRes.json();
792
- // API may return duration_seconds (cloud) or duration (local)
793
- duration = waveformData.duration_seconds || waveformData.duration || 0;
794
- }
795
-
796
- // Set initial selection based on recommendation
797
- selectedOption = analysisData.analysis.recommended_selection === 'clean' ? 'clean' : 'with_backing';
798
-
799
- // Check if there's already an uploaded instrumental
800
- if (analysisData.has_uploaded_instrumental) {
801
- hasUploaded = true;
802
- }
803
-
804
- // Check if original audio is available
805
- if (analysisData.has_original) {
806
- hasOriginal = true;
807
- }
808
-
809
- render();
810
- setupKeyboardShortcuts();
811
- } catch (error) {
812
- showError(error.message);
813
- }
814
- }
815
-
816
- function render() {
817
- // Pause any existing audio before rebuilding DOM to avoid AbortError
818
- const existingAudio = document.getElementById('audio-player');
819
- const wasPlaying = isPlaying;
820
- if (existingAudio && !existingAudio.paused) {
821
- existingAudio.pause();
822
- }
823
-
824
- const app = document.getElementById('app');
825
- const segments = analysisData.analysis.audible_segments;
826
- const hasSegments = segments.length > 0;
827
-
828
- app.innerHTML = `
829
- <div class="header">
830
- <div class="header-left">
831
- <span class="logo"><img src="https://gen.nomadkaraoke.com/nomad-karaoke-logo.svg" alt="Nomad Karaoke" class="logo-img" onerror="this.style.display='none'"> Instrumental Review</span>
832
- <span class="track-info">${escapeHtml(analysisData.artist) || ''} ${analysisData.artist && analysisData.title ? '–' : ''} ${escapeHtml(analysisData.title) || ''}</span>
833
- </div>
834
- <div class="header-right">
835
- ${hasSegments ? `
836
- <span class="badge">${segments.length} segments</span>
837
- <span class="badge">${analysisData.analysis.audible_percentage.toFixed(0)}% backing vocals</span>
838
- ` : ''}
839
- <span class="badge ${analysisData.analysis.recommended_selection === 'clean' ? 'badge-success' : 'badge-warning'}">
840
- ${analysisData.analysis.recommended_selection === 'clean' ? '✓ Clean recommended' : '⚠ Review needed'}
841
- </span>
842
- </div>
843
- </div>
844
-
845
- <div id="error-container"></div>
846
-
847
- <div class="waveform-player">
848
- <div class="waveform-toolbar">
849
- <div class="toolbar-left">
850
- <button class="btn btn-icon btn-primary" id="play-btn" onclick="togglePlayPause()">
851
- ${isPlaying ? '⏸' : '▶'}
852
- </button>
853
- <span class="time-display">
854
- <span id="current-time">${formatTime(currentTime)}</span>
855
- <span style="color: var(--text-muted)"> / ${formatTime(duration)}</span>
856
- </span>
857
- </div>
858
-
859
- <div class="toolbar-center">
860
- <div class="audio-toggle-group">
861
- ${hasOriginal ? `
862
- <button class="audio-toggle ${activeAudio === 'original' ? 'active' : ''}" data-audio-type="original" onclick="setActiveAudio('original')">Original</button>
863
- ` : ''}
864
- <button class="audio-toggle ${activeAudio === 'backing' ? 'active' : ''}" data-audio-type="backing" onclick="setActiveAudio('backing')">Backing Vocals Only</button>
865
- <button class="audio-toggle ${activeAudio === 'clean' ? 'active' : ''}" data-audio-type="clean" onclick="setActiveAudio('clean')">Pure Instrumental</button>
866
- ${analysisData.audio_urls.with_backing ? `
867
- <button class="audio-toggle ${activeAudio === 'with_backing' ? 'active' : ''}" data-audio-type="with_backing" onclick="setActiveAudio('with_backing')">Instrumental + Backing</button>
868
- ` : ''}
869
- ${hasCustom ? `
870
- <button class="audio-toggle ${activeAudio === 'custom' ? 'active' : ''}" data-audio-type="custom" onclick="setActiveAudio('custom')">Custom</button>
871
- ` : ''}
872
- ${hasUploaded ? `
873
- <button class="audio-toggle ${activeAudio === 'uploaded' ? 'active' : ''}" data-audio-type="uploaded" onclick="setActiveAudio('uploaded')" title="${escapeHtml(uploadedFilename)}">Uploaded</button>
874
- ` : ''}
875
- </div>
876
- <label class="btn btn-sm btn-secondary upload-btn" title="Upload custom instrumental">
877
- 📁 Upload
878
- <input type="file" accept=".flac,.mp3,.wav,.m4a,.ogg" onchange="handleUpload(event)">
879
- </label>
880
- </div>
881
-
882
- <div class="toolbar-right">
883
- <div class="zoom-controls">
884
- <button class="zoom-btn ${zoomLevel === 1 ? 'active' : ''}" onclick="setZoom(1)" title="1x zoom">1x</button>
885
- <button class="zoom-btn ${zoomLevel === 2 ? 'active' : ''}" onclick="setZoom(2)" title="2x zoom">2x</button>
886
- <button class="zoom-btn ${zoomLevel === 4 ? 'active' : ''}" onclick="setZoom(4)" title="4x zoom">4x</button>
887
- </div>
888
- <span style="font-size: 0.7rem; color: var(--text-muted);">
889
- <span class="kbd">Shift</span>+drag
890
- </span>
891
- <span class="kbd">Space</span>
892
- </div>
893
- </div>
894
-
895
- <div class="waveform-container" id="waveform-container">
896
- <div class="waveform-area" id="waveform-area" style="width: ${zoomLevel * 100}%;">
897
- <canvas id="waveform-canvas"></canvas>
898
- <div class="playhead hidden" id="playhead"></div>
899
- <div class="selection-overlay hidden" id="selection-overlay"></div>
900
- </div>
901
- </div>
902
-
903
- <div class="time-axis">
904
- <span>0:00</span>
905
- <span>${formatTime(duration * 0.25)}</span>
906
- <span>${formatTime(duration * 0.5)}</span>
907
- <span>${formatTime(duration * 0.75)}</span>
908
- <span>${formatTime(duration)}</span>
909
- </div>
910
- </div>
911
-
912
- <audio id="audio-player" src="${getAudioUrl()}"></audio>
913
-
914
- <div class="bottom-section">
915
- <div class="mute-panel">
916
- <div class="mute-panel-header">
917
- <span class="mute-panel-title">Mute Regions ${muteRegions.length > 0 ? `(${muteRegions.length})` : ''}</span>
918
- ${muteRegions.length > 0 ? `
919
- <div style="display: flex; gap: 6px;">
920
- <button class="btn btn-sm btn-secondary" onclick="clearAllRegions()">Clear</button>
921
- ${!hasCustom ? `<button class="btn btn-sm btn-primary" id="create-custom-btn" onclick="createCustomInstrumental()">Create Custom</button>` : ''}
922
- </div>
923
- ` : ''}
924
- </div>
925
-
926
- ${muteRegions.length > 0 ? `
927
- <div class="mute-regions-list">
928
- ${muteRegions.map((r, i) => `
929
- <div class="mute-region-tag">
930
- <span onclick="seekTo(${r.start_seconds}, true)" style="cursor: pointer">${formatTime(r.start_seconds)} – ${formatTime(r.end_seconds)}</span>
931
- <button onclick="removeRegion(${i})">×</button>
932
- </div>
933
- `).join('')}
934
- </div>
935
- ` : `
936
- <div style="color: var(--text-muted); font-size: 0.75rem;">
937
- ${hasSegments ? 'Click segments below or <kbd class="kbd">Shift</kbd> + drag on waveform' : 'No backing vocals detected – clean instrumental recommended'}
938
- </div>
939
- `}
940
-
941
- ${hasSegments ? `
942
- <div class="quick-segments">
943
- ${segments.slice(0, 8).map((seg, i) => `
944
- <button class="quick-segment-btn" onclick="addSegmentAsRegion(${i})" title="Add to mute regions">
945
- ${formatTime(seg.start_seconds)} – ${formatTime(seg.end_seconds)}
946
- </button>
947
- `).join('')}
948
- ${segments.length > 8 ? `<span style="font-size: 0.7rem; color: var(--text-muted); padding: 3px;">+${segments.length - 8} more</span>` : ''}
949
- </div>
950
- ` : ''}
951
- </div>
952
-
953
- <div class="selection-panel">
954
- <span class="selection-panel-title">Final Selection</span>
955
- <div class="selection-options">
956
- <label class="selection-option ${selectedOption === 'clean' ? 'selected' : ''}" onclick="setSelection('clean')">
957
- <input type="radio" name="selection" value="clean">
958
- <div class="selection-radio"></div>
959
- <div class="selection-label">
960
- <div class="selection-label-title">Clean Instrumental</div>
961
- <div class="selection-label-desc">No backing vocals</div>
962
- </div>
963
- </label>
964
- <label class="selection-option ${selectedOption === 'with_backing' ? 'selected' : ''}" onclick="setSelection('with_backing')">
965
- <input type="radio" name="selection" value="with_backing">
966
- <div class="selection-radio"></div>
967
- <div class="selection-label">
968
- <div class="selection-label-title">With Backing Vocals</div>
969
- <div class="selection-label-desc">All backing vocals included</div>
970
- </div>
971
- </label>
972
- ${hasOriginal ? `
973
- <label class="selection-option ${selectedOption === 'original' ? 'selected' : ''}" onclick="setSelection('original')">
974
- <input type="radio" name="selection" value="original">
975
- <div class="selection-radio"></div>
976
- <div class="selection-label">
977
- <div class="selection-label-title">Original Audio</div>
978
- <div class="selection-label-desc">Full original with lead vocals</div>
979
- </div>
980
- </label>
981
- ` : ''}
982
- ${hasCustom ? `
983
- <label class="selection-option ${selectedOption === 'custom' ? 'selected' : ''}" onclick="setSelection('custom')">
984
- <input type="radio" name="selection" value="custom">
985
- <div class="selection-radio"></div>
986
- <div class="selection-label">
987
- <div class="selection-label-title">Custom</div>
988
- <div class="selection-label-desc">${muteRegions.length} regions muted</div>
989
- </div>
990
- </label>
991
- ` : ''}
992
- ${hasUploaded ? `
993
- <label class="selection-option ${selectedOption === 'uploaded' ? 'selected' : ''}" onclick="setSelection('uploaded')">
994
- <input type="radio" name="selection" value="uploaded">
995
- <div class="selection-radio"></div>
996
- <div class="selection-label">
997
- <div class="selection-label-title">Uploaded</div>
998
- <div class="selection-label-desc">${escapeHtml(uploadedFilename)}</div>
999
- </div>
1000
- </label>
1001
- ` : ''}
1002
- </div>
1003
- <button class="btn btn-primary submit-btn" id="submit-btn" onclick="submitSelection()">
1004
- ✓ Confirm & Continue
1005
- </button>
1006
- </div>
1007
- </div>
1008
- `;
1009
-
1010
- // Setup after render
1011
- if (waveformData) {
1012
- resizeCanvas();
1013
- drawWaveform();
1014
- setupWaveformInteraction();
1015
- }
1016
-
1017
- // Setup audio state - add listeners when element changes
1018
- const audio = document.getElementById('audio-player');
1019
- if (audio) {
1020
- // Check if this is a new audio element (DOM was rebuilt)
1021
- if (audio !== currentAudioElement) {
1022
- audio.addEventListener('timeupdate', onTimeUpdate);
1023
- audio.addEventListener('play', onAudioPlay);
1024
- audio.addEventListener('pause', onAudioPause);
1025
- audio.addEventListener('ended', onAudioEnded);
1026
- currentAudioElement = audio;
1027
- }
1028
-
1029
- // Restore playback position and state after audio is ready
1030
- audio.addEventListener('loadeddata', function onLoaded() {
1031
- audio.currentTime = currentTime;
1032
- if (wasPlaying) {
1033
- audio.play().catch(() => {});
1034
- }
1035
- }, { once: true });
1036
-
1037
- // If already loaded (cached), set time directly
1038
- if (audio.readyState >= 2) {
1039
- audio.currentTime = currentTime;
1040
- if (wasPlaying) {
1041
- audio.play().catch(() => {});
1042
- }
1043
- }
1044
- }
1045
- }
1046
-
1047
- function resizeCanvas() {
1048
- const canvas = document.getElementById('waveform-canvas');
1049
- const container = document.getElementById('waveform-container');
1050
- const area = document.getElementById('waveform-area');
1051
- if (!canvas || !container || !area) return;
1052
-
1053
- // Set canvas width based on container width * zoom level
1054
- canvas.width = container.clientWidth * zoomLevel;
1055
- canvas.height = container.clientHeight;
1056
-
1057
- // Update area width to match
1058
- area.style.width = `${zoomLevel * 100}%`;
1059
- }
1060
-
1061
- function drawWaveform() {
1062
- const canvas = document.getElementById('waveform-canvas');
1063
- if (!canvas || !waveformData) return;
1064
-
1065
- const ctx = canvas.getContext('2d');
1066
- const { amplitudes } = waveformData;
1067
- const width = canvas.width;
1068
- const height = canvas.height;
1069
- const centerY = height / 2;
1070
- const bgColor = getComputedStyle(document.documentElement).getPropertyValue('--waveform-bg').trim();
1071
-
1072
- // Clear
1073
- ctx.fillStyle = bgColor;
1074
- ctx.fillRect(0, 0, width, height);
1075
-
1076
- // Draw center line
1077
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
1078
- ctx.setLineDash([4, 4]);
1079
- ctx.beginPath();
1080
- ctx.moveTo(0, centerY);
1081
- ctx.lineTo(width, centerY);
1082
- ctx.stroke();
1083
- ctx.setLineDash([]);
1084
-
1085
- // Draw waveform bars
1086
- const barWidth = width / amplitudes.length;
1087
-
1088
- amplitudes.forEach((amp, i) => {
1089
- const x = i * barWidth;
1090
- const barHeight = Math.max(2, amp * height * 0.9);
1091
- const y = centerY - barHeight / 2;
1092
- const time = (i / amplitudes.length) * duration;
1093
-
1094
- // Check regions
1095
- const inMuteRegion = muteRegions.some(r => time >= r.start_seconds && time <= r.end_seconds);
1096
- const inAudibleSegment = analysisData.analysis.audible_segments.some(
1097
- s => time >= s.start_seconds && time <= s.end_seconds
1098
- );
1099
-
1100
- if (inMuteRegion) {
1101
- // Muted regions blend into background - very subtle
1102
- ctx.fillStyle = 'rgba(13, 17, 23, 0.8)';
1103
- } else if (inAudibleSegment) {
1104
- ctx.fillStyle = '#ec4899'; // pink for detected backing vocals
1105
- } else {
1106
- ctx.fillStyle = '#60a5fa'; // blue for rest
1107
- }
1108
-
1109
- ctx.fillRect(x, y, Math.max(1, barWidth - 0.5), barHeight);
1110
- });
1111
- }
1112
-
1113
- function setupWaveformInteraction() {
1114
- const canvas = document.getElementById('waveform-canvas');
1115
- const area = document.getElementById('waveform-area');
1116
- if (!canvas || !area) return;
1117
-
1118
- canvas.onmousedown = (e) => {
1119
- const rect = canvas.getBoundingClientRect();
1120
- const x = e.clientX - rect.left;
1121
-
1122
- // Guard against invalid duration
1123
- if (!Number.isFinite(duration) || duration <= 0 || !Number.isFinite(rect.width) || rect.width <= 0) {
1124
- return;
1125
- }
1126
-
1127
- const time = (x / rect.width) * duration;
1128
-
1129
- // Shift+drag to select mute region
1130
- if (e.shiftKey) {
1131
- isDragging = true;
1132
- dragStartTime = time;
1133
- selectionStartX = x;
1134
- updateSelectionOverlay(x, x);
1135
- showSelectionOverlay(true);
1136
- } else {
1137
- // Regular click to seek and play
1138
- seekTo(time);
1139
- }
1140
- };
1141
-
1142
- canvas.onmousemove = (e) => {
1143
- if (!isDragging) return;
1144
- const rect = canvas.getBoundingClientRect();
1145
- const x = e.clientX - rect.left;
1146
- updateSelectionOverlay(selectionStartX, x);
1147
- };
1148
-
1149
- const endDrag = (e) => {
1150
- if (!isDragging) return;
1151
-
1152
- const rect = canvas.getBoundingClientRect();
1153
- const x = e.clientX - rect.left;
1154
-
1155
- // Guard against invalid duration
1156
- if (!Number.isFinite(duration) || duration <= 0 || !Number.isFinite(rect.width) || rect.width <= 0) {
1157
- isDragging = false;
1158
- showSelectionOverlay(false);
1159
- return;
1160
- }
1161
-
1162
- const time = (x / rect.width) * duration;
1163
-
1164
- const start = Math.min(dragStartTime, time);
1165
- const end = Math.max(dragStartTime, time);
1166
-
1167
- if (end - start > 0.5) {
1168
- addRegion(start, end);
1169
- }
1170
-
1171
- isDragging = false;
1172
- showSelectionOverlay(false);
1173
- };
1174
-
1175
- canvas.onmouseup = endDrag;
1176
- canvas.onmouseleave = (e) => {
1177
- if (isDragging) endDrag(e);
1178
- };
1179
- }
1180
-
1181
- function updateSelectionOverlay(startX, endX) {
1182
- const overlay = document.getElementById('selection-overlay');
1183
- if (!overlay) return;
1184
-
1185
- const left = Math.min(startX, endX);
1186
- const width = Math.abs(endX - startX);
1187
-
1188
- overlay.style.left = `${left}px`;
1189
- overlay.style.width = `${width}px`;
1190
- }
1191
-
1192
- function showSelectionOverlay(show) {
1193
- const overlay = document.getElementById('selection-overlay');
1194
- if (overlay) {
1195
- overlay.classList.toggle('hidden', !show);
1196
- }
1197
- }
1198
-
1199
- function startPlayheadAnimation() {
1200
- const animate = () => {
1201
- updatePlayhead();
1202
- animationFrameId = requestAnimationFrame(animate);
1203
- };
1204
- animate();
1205
- }
1206
-
1207
- function stopPlayheadAnimation() {
1208
- if (animationFrameId) {
1209
- cancelAnimationFrame(animationFrameId);
1210
- animationFrameId = null;
1211
- }
1212
- }
1213
-
1214
- function updatePlayhead() {
1215
- const playhead = document.getElementById('playhead');
1216
- const canvas = document.getElementById('waveform-canvas');
1217
- const audio = document.getElementById('audio-player');
1218
-
1219
- if (!playhead || !canvas || !audio) return;
1220
-
1221
- currentTime = audio.currentTime;
1222
-
1223
- // Update time display regardless of playhead position validity
1224
- const timeEl = document.getElementById('current-time');
1225
- if (timeEl) timeEl.textContent = formatTime(currentTime);
1226
-
1227
- // Guard against NaN/Infinity when calculating playhead position
1228
- if (!Number.isFinite(duration) || duration <= 0 || !Number.isFinite(canvas.width) || canvas.width <= 0) {
1229
- playhead.style.left = '0px';
1230
- } else {
1231
- const x = Math.max(0, Math.min((currentTime / duration) * canvas.width, canvas.width));
1232
- playhead.style.left = `${x}px`;
1233
- }
1234
-
1235
- playhead.classList.remove('hidden');
1236
- }
1237
-
1238
- function updatePlayButton() {
1239
- const btn = document.getElementById('play-btn');
1240
- if (btn) btn.innerHTML = isPlaying ? '⏸' : '▶';
1241
- }
1242
-
1243
- function togglePlayPause() {
1244
- const audio = document.getElementById('audio-player');
1245
- if (!audio) return;
1246
-
1247
- if (isPlaying) {
1248
- audio.pause();
1249
- } else {
1250
- audio.play();
1251
- }
1252
- }
1253
-
1254
- function seekTo(time, autoPlay = true) {
1255
- const audio = document.getElementById('audio-player');
1256
- // Guard against non-finite time values (NaN, Infinity)
1257
- if (!audio || !Number.isFinite(time)) return;
1258
-
1259
- audio.currentTime = time;
1260
- currentTime = time;
1261
- updatePlayhead();
1262
- // Auto-play when seeking via click (if not already playing)
1263
- if (autoPlay && !isPlaying) {
1264
- audio.play();
1265
- }
1266
- }
1267
-
1268
- function onTimeUpdate(e) {
1269
- currentTime = e.target.currentTime;
1270
- }
1271
-
1272
-
1273
- function setActiveAudio(type) {
1274
- const audio = document.getElementById('audio-player');
1275
- if (!audio) return;
1276
-
1277
- const wasPlaying = !audio.paused;
1278
- const time = audio.currentTime;
1279
-
1280
- // Pause before changing source
1281
- audio.pause();
1282
-
1283
- activeAudio = type;
1284
-
1285
- // Update toggle button states using data attributes (robust detection)
1286
- document.querySelectorAll('.audio-toggle').forEach(btn => {
1287
- const btnType = btn.dataset.audioType || 'custom';
1288
- btn.classList.toggle('active', btnType === type);
1289
- });
1290
-
1291
- // Change source and restore playback
1292
- audio.src = getAudioUrl();
1293
- audio.addEventListener('loadeddata', function onLoaded() {
1294
- audio.currentTime = time;
1295
- if (wasPlaying) {
1296
- audio.play().catch(() => {});
1297
- }
1298
- }, { once: true });
1299
- }
1300
-
1301
- function getAudioUrl() {
1302
- const stemTypes = {
1303
- original: 'original',
1304
- backing: 'backing_vocals',
1305
- clean: 'clean_instrumental',
1306
- with_backing: 'with_backing',
1307
- custom: 'custom_instrumental',
1308
- uploaded: 'uploaded_instrumental'
1309
- };
1310
- const stemType = stemTypes[activeAudio] || stemTypes.backing;
1311
-
1312
- // Cloud mode uses /audio-stream/{stem_type}, local mode uses /api/audio/{stem_type}
1313
- const isCloudMode = !!encodedBaseApiUrl;
1314
- const url = isCloudMode
1315
- ? `${API_BASE}/audio-stream/${stemType}`
1316
- : `/api/audio/${stemType}`;
1317
-
1318
- return addTokenToUrl(url);
1319
- }
1320
-
1321
- function formatTime(seconds) {
1322
- // Guard against NaN/Infinity
1323
- if (!Number.isFinite(seconds)) return '0:00';
1324
- const mins = Math.floor(seconds / 60);
1325
- const secs = Math.floor(seconds % 60);
1326
- return `${mins}:${secs.toString().padStart(2, '0')}`;
1327
- }
1328
-
1329
- function addRegion(start, end) {
1330
- muteRegions.push({ start_seconds: start, end_seconds: end });
1331
- muteRegions.sort((a, b) => a.start_seconds - b.start_seconds);
1332
- mergeOverlappingRegions();
1333
- hasCustom = false; // Invalidate custom when regions change
1334
-
1335
- // Just redraw waveform instead of full render to preserve scroll position
1336
- drawWaveform();
1337
- updateMuteRegionsPanel();
1338
- }
1339
-
1340
- function updateMuteRegionsPanel() {
1341
- // Update only the mute regions panel without full DOM rebuild
1342
- const panel = document.querySelector('.mute-panel');
1343
- if (!panel) return;
1344
-
1345
- const segments = analysisData.analysis.audible_segments;
1346
- const hasSegments = segments.length > 0;
1347
-
1348
- // Build mute regions list HTML
1349
- let regionsListHtml = '';
1350
- if (muteRegions.length > 0) {
1351
- regionsListHtml = '<div class="mute-regions-list">' +
1352
- muteRegions.map((r, i) =>
1353
- '<div class="mute-region-tag">' +
1354
- '<span onclick="seekTo(' + r.start_seconds + ', true)" style="cursor: pointer">' +
1355
- formatTime(r.start_seconds) + ' – ' + formatTime(r.end_seconds) + '</span>' +
1356
- '<button onclick="removeRegion(' + i + ')">×</button>' +
1357
- '</div>'
1358
- ).join('') +
1359
- '</div>';
1360
- } else {
1361
- regionsListHtml = '<div style="color: var(--text-muted); font-size: 0.75rem;">' +
1362
- (hasSegments ? 'Click segments below or <kbd class="kbd">Shift</kbd> + drag on waveform' : 'No backing vocals detected – clean instrumental recommended') +
1363
- '</div>';
1364
- }
1365
-
1366
- // Build quick segments HTML
1367
- let quickSegmentsHtml = '';
1368
- if (hasSegments) {
1369
- quickSegmentsHtml = '<div class="quick-segments">' +
1370
- segments.slice(0, 8).map((seg, i) =>
1371
- '<button class="quick-segment-btn" onclick="addSegmentAsRegion(' + i + ')" title="Add to mute regions">' +
1372
- formatTime(seg.start_seconds) + ' – ' + formatTime(seg.end_seconds) +
1373
- '</button>'
1374
- ).join('') +
1375
- (segments.length > 8 ? '<span style="font-size: 0.7rem; color: var(--text-muted); padding: 3px;">+' + (segments.length - 8) + ' more</span>' : '') +
1376
- '</div>';
1377
- }
1378
-
1379
- // Build header buttons
1380
- let headerButtons = '';
1381
- if (muteRegions.length > 0) {
1382
- headerButtons = '<div style="display: flex; gap: 6px;">' +
1383
- '<button class="btn btn-sm btn-secondary" onclick="clearAllRegions()">Clear</button>' +
1384
- (!hasCustom ? '<button class="btn btn-sm btn-primary" id="create-custom-btn" onclick="createCustomInstrumental()">Create Custom</button>' : '') +
1385
- '</div>';
1386
- }
1387
-
1388
- panel.innerHTML =
1389
- '<div class="mute-panel-header">' +
1390
- '<span class="mute-panel-title">Mute Regions ' + (muteRegions.length > 0 ? '(' + muteRegions.length + ')' : '') + '</span>' +
1391
- headerButtons +
1392
- '</div>' +
1393
- regionsListHtml +
1394
- quickSegmentsHtml;
1395
- }
1396
-
1397
- function addSegmentAsRegion(index) {
1398
- const seg = analysisData.analysis.audible_segments[index];
1399
- if (seg) {
1400
- addRegion(seg.start_seconds, seg.end_seconds);
1401
- }
1402
- }
1403
-
1404
- function removeRegion(index) {
1405
- muteRegions.splice(index, 1);
1406
- hasCustom = false;
1407
- drawWaveform();
1408
- updateMuteRegionsPanel();
1409
- }
1410
-
1411
- function clearAllRegions() {
1412
- muteRegions = [];
1413
- hasCustom = false;
1414
- drawWaveform();
1415
- updateMuteRegionsPanel();
1416
- }
1417
-
1418
- function mergeOverlappingRegions() {
1419
- if (muteRegions.length < 2) return;
1420
-
1421
- const merged = [muteRegions[0]];
1422
- for (let i = 1; i < muteRegions.length; i++) {
1423
- const last = merged[merged.length - 1];
1424
- const curr = muteRegions[i];
1425
-
1426
- if (curr.start_seconds <= last.end_seconds + 0.5) {
1427
- last.end_seconds = Math.max(last.end_seconds, curr.end_seconds);
1428
- } else {
1429
- merged.push(curr);
1430
- }
1431
- }
1432
- muteRegions = merged;
1433
- }
1434
-
1435
- function setSelection(value) {
1436
- selectedOption = value;
1437
- render();
1438
- }
1439
-
1440
- function setZoom(level) {
1441
- const container = document.getElementById('waveform-container');
1442
- const oldScrollRatio = container ? container.scrollLeft / (container.scrollWidth - container.clientWidth || 1) : 0;
1443
-
1444
- zoomLevel = level;
1445
-
1446
- // Update zoom button states directly (avoid full render)
1447
- document.querySelectorAll('.zoom-btn').forEach(btn => {
1448
- const btnLevel = parseInt(btn.textContent);
1449
- btn.classList.toggle('active', btnLevel === level);
1450
- });
1451
-
1452
- // Resize canvas and redraw
1453
- resizeCanvas();
1454
- drawWaveform();
1455
-
1456
- // Maintain scroll position proportionally
1457
- if (container && zoomLevel > 1) {
1458
- const newMaxScroll = container.scrollWidth - container.clientWidth;
1459
- container.scrollLeft = oldScrollRatio * newMaxScroll;
1460
- }
1461
- }
1462
-
1463
- async function handleUpload(event) {
1464
- const file = event.target.files[0];
1465
- if (!file) return;
1466
-
1467
- // Show upload progress
1468
- const overlay = document.createElement('div');
1469
- overlay.className = 'upload-overlay';
1470
- overlay.id = 'upload-overlay';
1471
- document.body.appendChild(overlay);
1472
-
1473
- const progress = document.createElement('div');
1474
- progress.className = 'upload-progress';
1475
- progress.id = 'upload-progress';
1476
- progress.innerHTML = `
1477
- <div class="spinner" style="margin: 0 auto 12px;"></div>
1478
- <div>Uploading ${file.name}...</div>
1479
- <div style="font-size: 0.8rem; color: var(--text-muted); margin-top: 8px;">Validating duration...</div>
1480
- `;
1481
- document.body.appendChild(progress);
1482
-
1483
- try {
1484
- const formData = new FormData();
1485
- formData.append('file', file);
1486
-
1487
- const response = await fetch(addTokenToUrl(`${API_BASE}/upload-instrumental`), {
1488
- method: 'POST',
1489
- body: formData
1490
- });
1491
-
1492
- if (!response.ok) {
1493
- const data = await response.json();
1494
- throw new Error(data.detail || 'Upload failed');
1495
- }
1496
-
1497
- const result = await response.json();
1498
- hasUploaded = true;
1499
- uploadedFilename = file.name;
1500
- activeAudio = 'uploaded';
1501
- selectedOption = 'uploaded';
1502
-
1503
- render();
1504
- showSuccess(`Uploaded ${file.name} (${result.duration_seconds.toFixed(1)}s)`);
1505
- } catch (error) {
1506
- showError(error.message);
1507
- } finally {
1508
- // Clean up progress UI
1509
- document.getElementById('upload-overlay')?.remove();
1510
- document.getElementById('upload-progress')?.remove();
1511
- // Reset file input so same file can be uploaded again
1512
- event.target.value = '';
1513
- }
1514
- }
1515
-
1516
- function showSuccess(message) {
1517
- const existing = document.querySelector('.alert-success-toast');
1518
- if (existing) existing.remove();
1519
-
1520
- const el = document.createElement('div');
1521
- el.className = 'alert-error'; // Reuse error styling but green
1522
- el.style.background = 'rgba(34, 197, 94, 0.95)';
1523
- el.textContent = message;
1524
- document.body.appendChild(el);
1525
-
1526
- setTimeout(() => el.remove(), 3000);
1527
- }
1528
-
1529
- async function createCustomInstrumental() {
1530
- const btn = document.getElementById('create-custom-btn');
1531
- const audio = document.getElementById('audio-player');
1532
- const wasPlaying = isPlaying;
1533
- const time = currentTime;
1534
-
1535
- // Pause audio while creating custom instrumental
1536
- if (audio && !audio.paused) {
1537
- audio.pause();
1538
- }
1539
-
1540
- if (btn) {
1541
- btn.disabled = true;
1542
- btn.textContent = 'Creating...';
1543
- }
1544
-
1545
- try {
1546
- const response = await fetch(addTokenToUrl(`${API_BASE}/create-custom-instrumental`), {
1547
- method: 'POST',
1548
- headers: { 'Content-Type': 'application/json' },
1549
- body: JSON.stringify({ mute_regions: muteRegions })
1550
- });
1551
-
1552
- if (!response.ok) {
1553
- const data = await response.json();
1554
- throw new Error(data.detail || 'Failed to create custom');
1555
- }
1556
-
1557
- hasCustom = true;
1558
- selectedOption = 'custom';
1559
- activeAudio = 'custom';
1560
-
1561
- // Render first, then restore playback
1562
- render();
1563
-
1564
- // After render, seek to previous position and optionally resume
1565
- const newAudio = document.getElementById('audio-player');
1566
- if (newAudio) {
1567
- newAudio.addEventListener('loadeddata', function onLoaded() {
1568
- newAudio.removeEventListener('loadeddata', onLoaded);
1569
- newAudio.currentTime = time;
1570
- if (wasPlaying) {
1571
- newAudio.play().catch(() => {});
1572
- }
1573
- }, { once: true });
1574
- }
1575
- } catch (error) {
1576
- showError(error.message);
1577
- if (btn) {
1578
- btn.disabled = false;
1579
- btn.textContent = 'Create Custom';
1580
- }
1581
- // Resume playback if there was an error
1582
- if (wasPlaying && audio) {
1583
- audio.play().catch(() => {});
1584
- }
1585
- }
1586
- }
1587
-
1588
- async function submitSelection() {
1589
- const btn = document.getElementById('submit-btn');
1590
- if (btn) {
1591
- btn.disabled = true;
1592
- btn.textContent = 'Submitting...';
1593
- }
1594
-
1595
- try {
1596
- const response = await fetch(addTokenToUrl(`${API_BASE}/select-instrumental`), {
1597
- method: 'POST',
1598
- headers: { 'Content-Type': 'application/json' },
1599
- body: JSON.stringify({ selection: selectedOption })
1600
- });
1601
-
1602
- if (!response.ok) {
1603
- const data = await response.json();
1604
- throw new Error(data.detail || 'Failed to submit');
1605
- }
1606
-
1607
- const selectionLabels = {
1608
- clean: 'Clean Instrumental',
1609
- with_backing: 'With Backing Vocals',
1610
- custom: 'Custom',
1611
- uploaded: 'Uploaded Instrumental',
1612
- original: 'Original Audio'
1613
- };
1614
- const selectionLabel = selectionLabels[selectedOption] || selectedOption;
1615
-
1616
- document.getElementById('app').innerHTML = `
1617
- <div class="success-screen">
1618
- <h2>✓ Selection Submitted</h2>
1619
- <p>You selected: <strong>${escapeHtml(selectionLabel)}</strong></p>
1620
- <p id="close-msg" style="color: var(--text-muted);">Closing in <span id="countdown">2</span>s...</p>
1621
- </div>
1622
- `;
1623
-
1624
- // Auto-close window after 2 seconds
1625
- let countdown = 2;
1626
- const countdownEl = document.getElementById('countdown');
1627
- const countdownInterval = setInterval(() => {
1628
- countdown--;
1629
- if (countdownEl) countdownEl.textContent = countdown;
1630
- if (countdown <= 0) {
1631
- clearInterval(countdownInterval);
1632
- // Try to close the window (works for windows opened by script)
1633
- window.close();
1634
- // If window.close() didn't work, update message
1635
- const msg = document.getElementById('close-msg');
1636
- if (msg) msg.textContent = 'You can close this window now.';
1637
- }
1638
- }, 1000);
1639
- } catch (error) {
1640
- showError(error.message);
1641
- if (btn) {
1642
- btn.disabled = false;
1643
- btn.textContent = '✓ Confirm & Continue';
1644
- }
1645
- }
1646
- }
1647
-
1648
- function setupKeyboardShortcuts() {
1649
- document.addEventListener('keydown', (e) => {
1650
- // Ignore if typing in input
1651
- if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
1652
-
1653
- switch (e.code) {
1654
- case 'Space':
1655
- e.preventDefault();
1656
- togglePlayPause();
1657
- break;
1658
- case 'Escape':
1659
- // Cancel any in-progress drag selection
1660
- if (isDragging) {
1661
- isDragging = false;
1662
- showSelectionOverlay(false);
1663
- }
1664
- break;
1665
- }
1666
- });
1667
- }
1668
-
1669
- function showError(message) {
1670
- // Remove any existing error
1671
- const existing = document.querySelector('.alert-error');
1672
- if (existing) existing.remove();
1673
-
1674
- const errorEl = document.createElement('div');
1675
- errorEl.className = 'alert-error';
1676
- errorEl.textContent = message;
1677
- document.body.appendChild(errorEl);
1678
-
1679
- setTimeout(() => errorEl.remove(), 5000);
1680
- }
1681
-
1682
- // Handle window resize
1683
- window.addEventListener('resize', () => {
1684
- if (waveformData) {
1685
- resizeCanvas();
1686
- drawWaveform();
1687
- }
1688
- });
1689
-
1690
- // Start
1691
- init();
1692
- </script>
1693
- </body>
1694
- </html>
1695
-