karaoke-gen 0.105.4__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 (322) hide show
  1. backend/api/routes/users.py +14 -3
  2. backend/config.py +3 -0
  3. backend/services/encoding_interface.py +4 -0
  4. backend/services/job_notification_service.py +4 -21
  5. backend/tests/test_job_notification_service.py +24 -58
  6. backend/tests/test_video_worker_orchestrator.py +189 -0
  7. backend/workers/video_worker_orchestrator.py +7 -0
  8. karaoke_gen/instrumental_review/server.py +145 -35
  9. karaoke_gen/nextjs_frontend/__init__.py +98 -0
  10. karaoke_gen/nextjs_frontend/out/404/index.html +1 -0
  11. karaoke_gen/nextjs_frontend/out/404.html +1 -0
  12. karaoke_gen/nextjs_frontend/out/__next.__PAGE__.txt +9 -0
  13. karaoke_gen/nextjs_frontend/out/__next._full.txt +22 -0
  14. karaoke_gen/nextjs_frontend/out/__next._head.txt +8 -0
  15. karaoke_gen/nextjs_frontend/out/__next._index.txt +9 -0
  16. karaoke_gen/nextjs_frontend/out/__next._tree.txt +2 -0
  17. karaoke_gen/nextjs_frontend/out/_next/static/chunks/01a7f8fe40f1ff47.js +1 -0
  18. karaoke_gen/nextjs_frontend/out/_next/static/chunks/112f346e31f991df.js +4 -0
  19. karaoke_gen/nextjs_frontend/out/_next/static/chunks/16d1a4dd9d8a873a.js +3 -0
  20. karaoke_gen/nextjs_frontend/out/_next/static/chunks/1ab85c362b8b0e86.js +9 -0
  21. karaoke_gen/nextjs_frontend/out/_next/static/chunks/247eb132b7f7b574.js +1 -0
  22. karaoke_gen/nextjs_frontend/out/_next/static/chunks/2b80d15cc95e4818.js +1 -0
  23. karaoke_gen/nextjs_frontend/out/_next/static/chunks/32c7eba5cd46c1bc.js +7 -0
  24. karaoke_gen/nextjs_frontend/out/_next/static/chunks/483f26794eae53d0.js +1 -0
  25. karaoke_gen/nextjs_frontend/out/_next/static/chunks/550c3b02e85f196a.js +1 -0
  26. karaoke_gen/nextjs_frontend/out/_next/static/chunks/55c5ade44387bef8.js +1 -0
  27. karaoke_gen/nextjs_frontend/out/_next/static/chunks/5628d92b5893add2.css +1 -0
  28. karaoke_gen/nextjs_frontend/out/_next/static/chunks/56ebf7665e4341c8.js +7 -0
  29. karaoke_gen/nextjs_frontend/out/_next/static/chunks/5997132b61dec430.js +1 -0
  30. karaoke_gen/nextjs_frontend/out/_next/static/chunks/5ea55255bce3eb9e.js +5 -0
  31. karaoke_gen/nextjs_frontend/out/_next/static/chunks/5eda89a57490b3cd.js +1 -0
  32. karaoke_gen/nextjs_frontend/out/_next/static/chunks/692f5d9e0d700c76.js +3 -0
  33. karaoke_gen/nextjs_frontend/out/_next/static/chunks/71d7a05b14f9f0f4.js +1 -0
  34. karaoke_gen/nextjs_frontend/out/_next/static/chunks/81ac355749ef3302.js +1 -0
  35. karaoke_gen/nextjs_frontend/out/_next/static/chunks/95f7e5934dbb0e5d.js +1 -0
  36. karaoke_gen/nextjs_frontend/out/_next/static/chunks/9bce8f19eaa46940.js +1 -0
  37. karaoke_gen/nextjs_frontend/out/_next/static/chunks/a6dad97d9634a72d.js +1 -0
  38. karaoke_gen/nextjs_frontend/out/_next/static/chunks/a9ed54eed3e14c92.js +2 -0
  39. karaoke_gen/nextjs_frontend/out/_next/static/chunks/b35cd41238ecfb17.js +1 -0
  40. karaoke_gen/nextjs_frontend/out/_next/static/chunks/b5bc3c3d5ebd49eb.js +1 -0
  41. karaoke_gen/nextjs_frontend/out/_next/static/chunks/b5c078c08db5ae32.js +5 -0
  42. karaoke_gen/nextjs_frontend/out/_next/static/chunks/be9c44a178104187.js +1 -0
  43. karaoke_gen/nextjs_frontend/out/_next/static/chunks/c4c840e18cb4861c.js +1 -0
  44. karaoke_gen/nextjs_frontend/out/_next/static/chunks/c645af7d6b65f73e.js +1 -0
  45. karaoke_gen/nextjs_frontend/out/_next/static/chunks/d2c5e2575df784d4.js +1 -0
  46. karaoke_gen/nextjs_frontend/out/_next/static/chunks/d30af02b96d81462.js +1 -0
  47. karaoke_gen/nextjs_frontend/out/_next/static/chunks/d9bdf64f4ec1e9b7.js +7 -0
  48. karaoke_gen/nextjs_frontend/out/_next/static/chunks/dcde6ed684dacd0e.js +5 -0
  49. karaoke_gen/nextjs_frontend/out/_next/static/chunks/e422cbe931246000.js +1 -0
  50. karaoke_gen/nextjs_frontend/out/_next/static/chunks/e483af34fc792d38.js +1 -0
  51. karaoke_gen/nextjs_frontend/out/_next/static/chunks/e57422aad6b897da.js +1 -0
  52. karaoke_gen/nextjs_frontend/out/_next/static/chunks/ef02697fb404726a.js +1 -0
  53. karaoke_gen/nextjs_frontend/out/_next/static/chunks/ff1a16fafef87110.js +1 -0
  54. karaoke_gen/nextjs_frontend/out/_next/static/chunks/turbopack-2d9ca3017a9deedf.js +3 -0
  55. karaoke_gen/nextjs_frontend/out/_next/static/zpw_-rjFIDV5tlPPtnvRI/_buildManifest.js +11 -0
  56. karaoke_gen/nextjs_frontend/out/_next/static/zpw_-rjFIDV5tlPPtnvRI/_clientMiddlewareManifest.json +1 -0
  57. karaoke_gen/nextjs_frontend/out/_next/static/zpw_-rjFIDV5tlPPtnvRI/_ssgManifest.js +1 -0
  58. karaoke_gen/nextjs_frontend/out/_not-found/__next._full.txt +18 -0
  59. karaoke_gen/nextjs_frontend/out/_not-found/__next._head.txt +8 -0
  60. karaoke_gen/nextjs_frontend/out/_not-found/__next._index.txt +9 -0
  61. karaoke_gen/nextjs_frontend/out/_not-found/__next._not-found.__PAGE__.txt +5 -0
  62. karaoke_gen/nextjs_frontend/out/_not-found/__next._not-found.txt +4 -0
  63. karaoke_gen/nextjs_frontend/out/_not-found/__next._tree.txt +2 -0
  64. karaoke_gen/nextjs_frontend/out/_not-found/index.html +1 -0
  65. karaoke_gen/nextjs_frontend/out/_not-found/index.txt +18 -0
  66. karaoke_gen/nextjs_frontend/out/admin/__next._full.txt +25 -0
  67. karaoke_gen/nextjs_frontend/out/admin/__next._head.txt +8 -0
  68. karaoke_gen/nextjs_frontend/out/admin/__next._index.txt +9 -0
  69. karaoke_gen/nextjs_frontend/out/admin/__next._tree.txt +2 -0
  70. karaoke_gen/nextjs_frontend/out/admin/__next.admin.__PAGE__.txt +9 -0
  71. karaoke_gen/nextjs_frontend/out/admin/__next.admin.txt +7 -0
  72. karaoke_gen/nextjs_frontend/out/admin/beta/__next._full.txt +25 -0
  73. karaoke_gen/nextjs_frontend/out/admin/beta/__next._head.txt +8 -0
  74. karaoke_gen/nextjs_frontend/out/admin/beta/__next._index.txt +9 -0
  75. karaoke_gen/nextjs_frontend/out/admin/beta/__next._tree.txt +2 -0
  76. karaoke_gen/nextjs_frontend/out/admin/beta/__next.admin.beta.__PAGE__.txt +9 -0
  77. karaoke_gen/nextjs_frontend/out/admin/beta/__next.admin.beta.txt +4 -0
  78. karaoke_gen/nextjs_frontend/out/admin/beta/__next.admin.txt +7 -0
  79. karaoke_gen/nextjs_frontend/out/admin/beta/index.html +1 -0
  80. karaoke_gen/nextjs_frontend/out/admin/beta/index.txt +25 -0
  81. karaoke_gen/nextjs_frontend/out/admin/index.html +1 -0
  82. karaoke_gen/nextjs_frontend/out/admin/index.txt +25 -0
  83. karaoke_gen/nextjs_frontend/out/admin/jobs/__next._full.txt +25 -0
  84. karaoke_gen/nextjs_frontend/out/admin/jobs/__next._head.txt +8 -0
  85. karaoke_gen/nextjs_frontend/out/admin/jobs/__next._index.txt +9 -0
  86. karaoke_gen/nextjs_frontend/out/admin/jobs/__next._tree.txt +2 -0
  87. karaoke_gen/nextjs_frontend/out/admin/jobs/__next.admin.jobs.__PAGE__.txt +9 -0
  88. karaoke_gen/nextjs_frontend/out/admin/jobs/__next.admin.jobs.txt +4 -0
  89. karaoke_gen/nextjs_frontend/out/admin/jobs/__next.admin.txt +7 -0
  90. karaoke_gen/nextjs_frontend/out/admin/jobs/index.html +1 -0
  91. karaoke_gen/nextjs_frontend/out/admin/jobs/index.txt +25 -0
  92. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._full.txt +25 -0
  93. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._head.txt +8 -0
  94. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._index.txt +9 -0
  95. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._tree.txt +2 -0
  96. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next.admin.rate-limits.__PAGE__.txt +9 -0
  97. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next.admin.rate-limits.txt +4 -0
  98. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next.admin.txt +7 -0
  99. karaoke_gen/nextjs_frontend/out/admin/rate-limits/index.html +1 -0
  100. karaoke_gen/nextjs_frontend/out/admin/rate-limits/index.txt +25 -0
  101. karaoke_gen/nextjs_frontend/out/admin/searches/__next._full.txt +25 -0
  102. karaoke_gen/nextjs_frontend/out/admin/searches/__next._head.txt +8 -0
  103. karaoke_gen/nextjs_frontend/out/admin/searches/__next._index.txt +9 -0
  104. karaoke_gen/nextjs_frontend/out/admin/searches/__next._tree.txt +2 -0
  105. karaoke_gen/nextjs_frontend/out/admin/searches/__next.admin.searches.__PAGE__.txt +9 -0
  106. karaoke_gen/nextjs_frontend/out/admin/searches/__next.admin.searches.txt +4 -0
  107. karaoke_gen/nextjs_frontend/out/admin/searches/__next.admin.txt +7 -0
  108. karaoke_gen/nextjs_frontend/out/admin/searches/index.html +1 -0
  109. karaoke_gen/nextjs_frontend/out/admin/searches/index.txt +25 -0
  110. karaoke_gen/nextjs_frontend/out/admin/users/__next._full.txt +25 -0
  111. karaoke_gen/nextjs_frontend/out/admin/users/__next._head.txt +8 -0
  112. karaoke_gen/nextjs_frontend/out/admin/users/__next._index.txt +9 -0
  113. karaoke_gen/nextjs_frontend/out/admin/users/__next._tree.txt +2 -0
  114. karaoke_gen/nextjs_frontend/out/admin/users/__next.admin.txt +7 -0
  115. karaoke_gen/nextjs_frontend/out/admin/users/__next.admin.users.__PAGE__.txt +9 -0
  116. karaoke_gen/nextjs_frontend/out/admin/users/__next.admin.users.txt +4 -0
  117. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._full.txt +25 -0
  118. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._head.txt +8 -0
  119. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._index.txt +9 -0
  120. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._tree.txt +2 -0
  121. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.txt +7 -0
  122. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.users.detail.__PAGE__.txt +9 -0
  123. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.users.detail.txt +4 -0
  124. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.users.txt +4 -0
  125. karaoke_gen/nextjs_frontend/out/admin/users/detail/index.html +1 -0
  126. karaoke_gen/nextjs_frontend/out/admin/users/detail/index.txt +25 -0
  127. karaoke_gen/nextjs_frontend/out/admin/users/index.html +1 -0
  128. karaoke_gen/nextjs_frontend/out/admin/users/index.txt +25 -0
  129. karaoke_gen/nextjs_frontend/out/app/__next._full.txt +22 -0
  130. karaoke_gen/nextjs_frontend/out/app/__next._head.txt +8 -0
  131. karaoke_gen/nextjs_frontend/out/app/__next._index.txt +9 -0
  132. karaoke_gen/nextjs_frontend/out/app/__next._tree.txt +2 -0
  133. karaoke_gen/nextjs_frontend/out/app/__next.app.__PAGE__.txt +9 -0
  134. karaoke_gen/nextjs_frontend/out/app/__next.app.txt +4 -0
  135. karaoke_gen/nextjs_frontend/out/app/index.html +1 -0
  136. karaoke_gen/nextjs_frontend/out/app/index.txt +22 -0
  137. karaoke_gen/nextjs_frontend/out/app/jobs/__next._full.txt +19 -0
  138. karaoke_gen/nextjs_frontend/out/app/jobs/__next._head.txt +8 -0
  139. karaoke_gen/nextjs_frontend/out/app/jobs/__next._index.txt +9 -0
  140. karaoke_gen/nextjs_frontend/out/app/jobs/__next._tree.txt +2 -0
  141. karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.jobs.$oc$slug.__PAGE__.txt +6 -0
  142. karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.jobs.$oc$slug.txt +4 -0
  143. karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.jobs.txt +4 -0
  144. karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.txt +4 -0
  145. karaoke_gen/nextjs_frontend/out/app/jobs/index.html +1 -0
  146. karaoke_gen/nextjs_frontend/out/app/jobs/index.txt +19 -0
  147. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._full.txt +19 -0
  148. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._head.txt +8 -0
  149. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._index.txt +9 -0
  150. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._tree.txt +2 -0
  151. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.jobs.$oc$slug.__PAGE__.txt +6 -0
  152. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.jobs.$oc$slug.txt +4 -0
  153. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.jobs.txt +4 -0
  154. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.txt +4 -0
  155. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/index.html +1 -0
  156. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/index.txt +19 -0
  157. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._full.txt +19 -0
  158. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._head.txt +8 -0
  159. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._index.txt +9 -0
  160. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._tree.txt +2 -0
  161. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.jobs.$oc$slug.__PAGE__.txt +6 -0
  162. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.jobs.$oc$slug.txt +4 -0
  163. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.jobs.txt +4 -0
  164. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.txt +4 -0
  165. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/index.html +1 -0
  166. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/index.txt +19 -0
  167. karaoke_gen/nextjs_frontend/out/auth/verify/__next._full.txt +22 -0
  168. karaoke_gen/nextjs_frontend/out/auth/verify/__next._head.txt +8 -0
  169. karaoke_gen/nextjs_frontend/out/auth/verify/__next._index.txt +9 -0
  170. karaoke_gen/nextjs_frontend/out/auth/verify/__next._tree.txt +2 -0
  171. karaoke_gen/nextjs_frontend/out/auth/verify/__next.auth.txt +4 -0
  172. karaoke_gen/nextjs_frontend/out/auth/verify/__next.auth.verify.__PAGE__.txt +9 -0
  173. karaoke_gen/nextjs_frontend/out/auth/verify/__next.auth.verify.txt +4 -0
  174. karaoke_gen/nextjs_frontend/out/auth/verify/index.html +1 -0
  175. karaoke_gen/nextjs_frontend/out/auth/verify/index.txt +22 -0
  176. karaoke_gen/nextjs_frontend/out/index.html +1 -0
  177. karaoke_gen/nextjs_frontend/out/index.txt +22 -0
  178. karaoke_gen/nextjs_frontend/out/manifest.webmanifest +31 -0
  179. karaoke_gen/nextjs_frontend/out/order/success/__next._full.txt +22 -0
  180. karaoke_gen/nextjs_frontend/out/order/success/__next._head.txt +8 -0
  181. karaoke_gen/nextjs_frontend/out/order/success/__next._index.txt +9 -0
  182. karaoke_gen/nextjs_frontend/out/order/success/__next._tree.txt +2 -0
  183. karaoke_gen/nextjs_frontend/out/order/success/__next.order.success.__PAGE__.txt +9 -0
  184. karaoke_gen/nextjs_frontend/out/order/success/__next.order.success.txt +4 -0
  185. karaoke_gen/nextjs_frontend/out/order/success/__next.order.txt +4 -0
  186. karaoke_gen/nextjs_frontend/out/order/success/index.html +1 -0
  187. karaoke_gen/nextjs_frontend/out/order/success/index.txt +22 -0
  188. karaoke_gen/nextjs_frontend/out/payment/success/__next._full.txt +22 -0
  189. karaoke_gen/nextjs_frontend/out/payment/success/__next._head.txt +8 -0
  190. karaoke_gen/nextjs_frontend/out/payment/success/__next._index.txt +9 -0
  191. karaoke_gen/nextjs_frontend/out/payment/success/__next._tree.txt +2 -0
  192. karaoke_gen/nextjs_frontend/out/payment/success/__next.payment.success.__PAGE__.txt +9 -0
  193. karaoke_gen/nextjs_frontend/out/payment/success/__next.payment.success.txt +4 -0
  194. karaoke_gen/nextjs_frontend/out/payment/success/__next.payment.txt +4 -0
  195. karaoke_gen/nextjs_frontend/out/payment/success/index.html +1 -0
  196. karaoke_gen/nextjs_frontend/out/payment/success/index.txt +22 -0
  197. karaoke_gen/nextjs_frontend/out/screenshots/email-action_reminder.png +0 -0
  198. karaoke_gen/nextjs_frontend/out/screenshots/email-beta_welcome.png +0 -0
  199. karaoke_gen/nextjs_frontend/out/screenshots/email-job_completion.png +0 -0
  200. karaoke_gen/nextjs_frontend/out/screenshots/example-output.avif +0 -0
  201. karaoke_gen/nextjs_frontend/out/screenshots/homepage-full.png +0 -0
  202. karaoke_gen/nextjs_frontend/out/screenshots/homepage-hero.png +0 -0
  203. karaoke_gen/nextjs_frontend/out/screenshots/instrumental-review.avif +0 -0
  204. karaoke_gen/nextjs_frontend/out/screenshots/instrumental-review.png +0 -0
  205. karaoke_gen/nextjs_frontend/out/screenshots/job-dashboard.avif +0 -0
  206. karaoke_gen/nextjs_frontend/out/screenshots/lyrics-review.avif +0 -0
  207. karaoke_gen/nextjs_frontend/out/screenshots/lyrics-review.png +0 -0
  208. karaoke_gen/nextjs_frontend/out/sw.js +183 -0
  209. karaoke_gen/utils/cli_args.py +3 -3
  210. karaoke_gen/utils/gen_cli.py +4 -0
  211. karaoke_gen/utils/remote_cli.py +8 -40
  212. {karaoke_gen-0.105.4.dist-info → karaoke_gen-0.107.0.dist-info}/METADATA +1 -1
  213. {karaoke_gen-0.105.4.dist-info → karaoke_gen-0.107.0.dist-info}/RECORD +227 -121
  214. {karaoke_gen-0.105.4.dist-info → karaoke_gen-0.107.0.dist-info}/WHEEL +1 -1
  215. lyrics_transcriber/correction/agentic/agent.py +83 -60
  216. lyrics_transcriber/correction/anchor_sequence.py +48 -3
  217. lyrics_transcriber/correction/corrector.py +92 -58
  218. lyrics_transcriber/review/server.py +165 -33
  219. lyrics_transcriber/utils/tracing.py +214 -0
  220. karaoke_gen/instrumental_review/static/index.html +0 -1721
  221. lyrics_transcriber/frontend/.gitignore +0 -24
  222. lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +0 -935
  223. lyrics_transcriber/frontend/.yarnrc.yml +0 -3
  224. lyrics_transcriber/frontend/README.md +0 -50
  225. lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +0 -210
  226. lyrics_transcriber/frontend/__init__.py +0 -25
  227. lyrics_transcriber/frontend/e2e/agentic-corrections.spec.ts +0 -207
  228. lyrics_transcriber/frontend/e2e/fixtures/agentic-correction-data.json +0 -226
  229. lyrics_transcriber/frontend/eslint.config.js +0 -28
  230. lyrics_transcriber/frontend/index.html +0 -22
  231. lyrics_transcriber/frontend/package-lock.json +0 -4553
  232. lyrics_transcriber/frontend/package.json +0 -48
  233. lyrics_transcriber/frontend/playwright.config.ts +0 -69
  234. lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
  235. lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
  236. lyrics_transcriber/frontend/src/App.tsx +0 -243
  237. lyrics_transcriber/frontend/src/api.ts +0 -262
  238. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +0 -111
  239. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +0 -114
  240. lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +0 -204
  241. lyrics_transcriber/frontend/src/components/AppHeader.tsx +0 -65
  242. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +0 -180
  243. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +0 -175
  244. lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +0 -359
  245. lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +0 -281
  246. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +0 -162
  247. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +0 -257
  248. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +0 -94
  249. lyrics_transcriber/frontend/src/components/EditModal.tsx +0 -720
  250. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +0 -592
  251. lyrics_transcriber/frontend/src/components/EditWordList.tsx +0 -431
  252. lyrics_transcriber/frontend/src/components/FileUpload.tsx +0 -77
  253. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +0 -467
  254. lyrics_transcriber/frontend/src/components/Header.tsx +0 -520
  255. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +0 -1526
  256. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +0 -216
  257. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +0 -721
  258. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +0 -80
  259. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +0 -999
  260. lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +0 -51
  261. lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +0 -127
  262. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +0 -67
  263. lyrics_transcriber/frontend/src/components/ModelSelector.tsx +0 -23
  264. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +0 -177
  265. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +0 -268
  266. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +0 -336
  267. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +0 -354
  268. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +0 -64
  269. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +0 -383
  270. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +0 -131
  271. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +0 -266
  272. lyrics_transcriber/frontend/src/components/WordDivider.tsx +0 -191
  273. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +0 -466
  274. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +0 -56
  275. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +0 -89
  276. lyrics_transcriber/frontend/src/components/shared/constants.ts +0 -30
  277. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +0 -180
  278. lyrics_transcriber/frontend/src/components/shared/styles.ts +0 -13
  279. lyrics_transcriber/frontend/src/components/shared/types.js +0 -2
  280. lyrics_transcriber/frontend/src/components/shared/types.ts +0 -135
  281. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +0 -177
  282. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +0 -78
  283. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +0 -75
  284. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +0 -360
  285. lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +0 -110
  286. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +0 -22
  287. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +0 -537
  288. lyrics_transcriber/frontend/src/main.tsx +0 -11
  289. lyrics_transcriber/frontend/src/theme.ts +0 -406
  290. lyrics_transcriber/frontend/src/types/global.d.ts +0 -9
  291. lyrics_transcriber/frontend/src/types.js +0 -2
  292. lyrics_transcriber/frontend/src/types.ts +0 -199
  293. lyrics_transcriber/frontend/src/validation.ts +0 -132
  294. lyrics_transcriber/frontend/src/vite-env.d.ts +0 -1
  295. lyrics_transcriber/frontend/tsconfig.app.json +0 -26
  296. lyrics_transcriber/frontend/tsconfig.json +0 -25
  297. lyrics_transcriber/frontend/tsconfig.node.json +0 -23
  298. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +0 -1
  299. lyrics_transcriber/frontend/update_version.js +0 -11
  300. lyrics_transcriber/frontend/vite.config.d.ts +0 -2
  301. lyrics_transcriber/frontend/vite.config.js +0 -15
  302. lyrics_transcriber/frontend/vite.config.ts +0 -16
  303. lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
  304. lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
  305. lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
  306. lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js +0 -44465
  307. lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +0 -1
  308. lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
  309. lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
  310. lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
  311. lyrics_transcriber/frontend/web_assets/index.html +0 -22
  312. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
  313. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +0 -5
  314. lyrics_transcriber/frontend/yarn.lock +0 -3711
  315. {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/apple-touch-icon.png +0 -0
  316. {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/favicon-16x16.png +0 -0
  317. {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/favicon-32x32.png +0 -0
  318. {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/favicon.ico +0 -0
  319. {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/nomad-karaoke-logo.svg +0 -0
  320. /lyrics_transcriber/frontend/public/nomad-karaoke-logo.png → /karaoke_gen/nextjs_frontend/out/nomad-logo.png +0 -0
  321. {karaoke_gen-0.105.4.dist-info → karaoke_gen-0.107.0.dist-info}/entry_points.txt +0 -0
  322. {karaoke_gen-0.105.4.dist-info → karaoke_gen-0.107.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,1721 +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
- // Check localStorage for full auth token (logged-in users)
755
- // This takes priority over instrumentalToken which can expire
756
- const fullAuthToken = localStorage.getItem('karaoke_access_token');
757
-
758
- // Determine API base URL - cloud mode uses provided URL, local mode uses default
759
- const API_BASE = encodedBaseApiUrl
760
- ? decodeURIComponent(encodedBaseApiUrl)
761
- : '/api/jobs/local';
762
-
763
- // Helper to add auth token to URL
764
- // Priority: fullAuthToken (doesn't expire) > instrumentalToken (expires after 48h)
765
- // This is essential for audio elements which can't use Bearer headers
766
- function addTokenToUrl(url) {
767
- // Prefer full auth token - it doesn't have the short expiry that magic link tokens have
768
- if (fullAuthToken) {
769
- const separator = url.includes('?') ? '&' : '?';
770
- return `${url}${separator}token=${encodeURIComponent(fullAuthToken)}`;
771
- }
772
- // Fall back to instrumental token for non-logged-in users
773
- if (!instrumentalToken) return url;
774
- const separator = url.includes('?') ? '&' : '?';
775
- return `${url}${separator}instrumental_token=${encodeURIComponent(instrumentalToken)}`;
776
- }
777
-
778
- // Auth-aware fetch helper
779
- // Priority: fullAuthToken (Bearer header) > instrumentalToken (query param)
780
- function authFetch(url, options = {}) {
781
- if (fullAuthToken) {
782
- // Use Bearer auth for logged-in users
783
- const headers = new Headers(options.headers || {});
784
- headers.set('Authorization', `Bearer ${fullAuthToken}`);
785
- return fetch(url, { ...options, headers });
786
- } else {
787
- // Fall back to instrumental token in URL
788
- return fetch(addTokenToUrl(url), options);
789
- }
790
- }
791
-
792
- // HTML escape helper to prevent XSS
793
- function escapeHtml(str) {
794
- if (!str) return '';
795
- const div = document.createElement('div');
796
- div.textContent = str;
797
- return div.innerHTML;
798
- }
799
-
800
- // Named event handlers for audio (so they can be added once)
801
- function onAudioPlay() { isPlaying = true; updatePlayButton(); startPlayheadAnimation(); }
802
- function onAudioPause() { isPlaying = false; updatePlayButton(); stopPlayheadAnimation(); }
803
- function onAudioEnded() { isPlaying = false; updatePlayButton(); stopPlayheadAnimation(); }
804
-
805
- // Initialize
806
- async function init() {
807
- try {
808
- const [analysisRes, waveformRes] = await Promise.all([
809
- authFetch(`${API_BASE}/instrumental-analysis`),
810
- authFetch(`${API_BASE}/waveform-data?num_points=1000`)
811
- ]);
812
-
813
- if (!analysisRes.ok) throw new Error('Failed to load analysis');
814
- analysisData = await analysisRes.json();
815
-
816
- if (waveformRes.ok) {
817
- waveformData = await waveformRes.json();
818
- // API may return duration_seconds (cloud) or duration (local)
819
- duration = waveformData.duration_seconds || waveformData.duration || 0;
820
- }
821
-
822
- // Set initial selection based on recommendation
823
- selectedOption = analysisData.analysis.recommended_selection === 'clean' ? 'clean' : 'with_backing';
824
-
825
- // Check if there's already an uploaded instrumental
826
- if (analysisData.has_uploaded_instrumental) {
827
- hasUploaded = true;
828
- }
829
-
830
- // Check if original audio is available
831
- if (analysisData.has_original) {
832
- hasOriginal = true;
833
- }
834
-
835
- render();
836
- setupKeyboardShortcuts();
837
- } catch (error) {
838
- showError(error.message);
839
- }
840
- }
841
-
842
- function render() {
843
- // Pause any existing audio before rebuilding DOM to avoid AbortError
844
- const existingAudio = document.getElementById('audio-player');
845
- const wasPlaying = isPlaying;
846
- if (existingAudio && !existingAudio.paused) {
847
- existingAudio.pause();
848
- }
849
-
850
- const app = document.getElementById('app');
851
- const segments = analysisData.analysis.audible_segments;
852
- const hasSegments = segments.length > 0;
853
-
854
- app.innerHTML = `
855
- <div class="header">
856
- <div class="header-left">
857
- <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>
858
- <span class="track-info">${escapeHtml(analysisData.artist) || ''} ${analysisData.artist && analysisData.title ? '–' : ''} ${escapeHtml(analysisData.title) || ''}</span>
859
- </div>
860
- <div class="header-right">
861
- ${hasSegments ? `
862
- <span class="badge">${segments.length} segments</span>
863
- <span class="badge">${analysisData.analysis.audible_percentage.toFixed(0)}% backing vocals</span>
864
- ` : ''}
865
- <span class="badge ${analysisData.analysis.recommended_selection === 'clean' ? 'badge-success' : 'badge-warning'}">
866
- ${analysisData.analysis.recommended_selection === 'clean' ? '✓ Clean recommended' : '⚠ Review needed'}
867
- </span>
868
- </div>
869
- </div>
870
-
871
- <div id="error-container"></div>
872
-
873
- <div class="waveform-player">
874
- <div class="waveform-toolbar">
875
- <div class="toolbar-left">
876
- <button class="btn btn-icon btn-primary" id="play-btn" onclick="togglePlayPause()">
877
- ${isPlaying ? '⏸' : '▶'}
878
- </button>
879
- <span class="time-display">
880
- <span id="current-time">${formatTime(currentTime)}</span>
881
- <span style="color: var(--text-muted)"> / ${formatTime(duration)}</span>
882
- </span>
883
- </div>
884
-
885
- <div class="toolbar-center">
886
- <div class="audio-toggle-group">
887
- ${hasOriginal ? `
888
- <button class="audio-toggle ${activeAudio === 'original' ? 'active' : ''}" data-audio-type="original" onclick="setActiveAudio('original')">Original</button>
889
- ` : ''}
890
- <button class="audio-toggle ${activeAudio === 'backing' ? 'active' : ''}" data-audio-type="backing" onclick="setActiveAudio('backing')">Backing Vocals Only</button>
891
- <button class="audio-toggle ${activeAudio === 'clean' ? 'active' : ''}" data-audio-type="clean" onclick="setActiveAudio('clean')">Pure Instrumental</button>
892
- ${analysisData.audio_urls.with_backing ? `
893
- <button class="audio-toggle ${activeAudio === 'with_backing' ? 'active' : ''}" data-audio-type="with_backing" onclick="setActiveAudio('with_backing')">Instrumental + Backing</button>
894
- ` : ''}
895
- ${hasCustom ? `
896
- <button class="audio-toggle ${activeAudio === 'custom' ? 'active' : ''}" data-audio-type="custom" onclick="setActiveAudio('custom')">Custom</button>
897
- ` : ''}
898
- ${hasUploaded ? `
899
- <button class="audio-toggle ${activeAudio === 'uploaded' ? 'active' : ''}" data-audio-type="uploaded" onclick="setActiveAudio('uploaded')" title="${escapeHtml(uploadedFilename)}">Uploaded</button>
900
- ` : ''}
901
- </div>
902
- <label class="btn btn-sm btn-secondary upload-btn" title="Upload custom instrumental">
903
- 📁 Upload
904
- <input type="file" accept=".flac,.mp3,.wav,.m4a,.ogg" onchange="handleUpload(event)">
905
- </label>
906
- </div>
907
-
908
- <div class="toolbar-right">
909
- <div class="zoom-controls">
910
- <button class="zoom-btn ${zoomLevel === 1 ? 'active' : ''}" onclick="setZoom(1)" title="1x zoom">1x</button>
911
- <button class="zoom-btn ${zoomLevel === 2 ? 'active' : ''}" onclick="setZoom(2)" title="2x zoom">2x</button>
912
- <button class="zoom-btn ${zoomLevel === 4 ? 'active' : ''}" onclick="setZoom(4)" title="4x zoom">4x</button>
913
- </div>
914
- <span style="font-size: 0.7rem; color: var(--text-muted);">
915
- <span class="kbd">Shift</span>+drag
916
- </span>
917
- <span class="kbd">Space</span>
918
- </div>
919
- </div>
920
-
921
- <div class="waveform-container" id="waveform-container">
922
- <div class="waveform-area" id="waveform-area" style="width: ${zoomLevel * 100}%;">
923
- <canvas id="waveform-canvas"></canvas>
924
- <div class="playhead hidden" id="playhead"></div>
925
- <div class="selection-overlay hidden" id="selection-overlay"></div>
926
- </div>
927
- </div>
928
-
929
- <div class="time-axis">
930
- <span>0:00</span>
931
- <span>${formatTime(duration * 0.25)}</span>
932
- <span>${formatTime(duration * 0.5)}</span>
933
- <span>${formatTime(duration * 0.75)}</span>
934
- <span>${formatTime(duration)}</span>
935
- </div>
936
- </div>
937
-
938
- <audio id="audio-player" src="${getAudioUrl()}"></audio>
939
-
940
- <div class="bottom-section">
941
- <div class="mute-panel">
942
- <div class="mute-panel-header">
943
- <span class="mute-panel-title">Mute Regions ${muteRegions.length > 0 ? `(${muteRegions.length})` : ''}</span>
944
- ${muteRegions.length > 0 ? `
945
- <div style="display: flex; gap: 6px;">
946
- <button class="btn btn-sm btn-secondary" onclick="clearAllRegions()">Clear</button>
947
- ${!hasCustom ? `<button class="btn btn-sm btn-primary" id="create-custom-btn" onclick="createCustomInstrumental()">Create Custom</button>` : ''}
948
- </div>
949
- ` : ''}
950
- </div>
951
-
952
- ${muteRegions.length > 0 ? `
953
- <div class="mute-regions-list">
954
- ${muteRegions.map((r, i) => `
955
- <div class="mute-region-tag">
956
- <span onclick="seekTo(${r.start_seconds}, true)" style="cursor: pointer">${formatTime(r.start_seconds)} – ${formatTime(r.end_seconds)}</span>
957
- <button onclick="removeRegion(${i})">×</button>
958
- </div>
959
- `).join('')}
960
- </div>
961
- ` : `
962
- <div style="color: var(--text-muted); font-size: 0.75rem;">
963
- ${hasSegments ? 'Click segments below or <kbd class="kbd">Shift</kbd> + drag on waveform' : 'No backing vocals detected – clean instrumental recommended'}
964
- </div>
965
- `}
966
-
967
- ${hasSegments ? `
968
- <div class="quick-segments">
969
- ${segments.slice(0, 8).map((seg, i) => `
970
- <button class="quick-segment-btn" onclick="addSegmentAsRegion(${i})" title="Add to mute regions">
971
- ${formatTime(seg.start_seconds)} – ${formatTime(seg.end_seconds)}
972
- </button>
973
- `).join('')}
974
- ${segments.length > 8 ? `<span style="font-size: 0.7rem; color: var(--text-muted); padding: 3px;">+${segments.length - 8} more</span>` : ''}
975
- </div>
976
- ` : ''}
977
- </div>
978
-
979
- <div class="selection-panel">
980
- <span class="selection-panel-title">Final Selection</span>
981
- <div class="selection-options">
982
- <label class="selection-option ${selectedOption === 'clean' ? 'selected' : ''}" onclick="setSelection('clean')">
983
- <input type="radio" name="selection" value="clean">
984
- <div class="selection-radio"></div>
985
- <div class="selection-label">
986
- <div class="selection-label-title">Clean Instrumental</div>
987
- <div class="selection-label-desc">No backing vocals</div>
988
- </div>
989
- </label>
990
- <label class="selection-option ${selectedOption === 'with_backing' ? 'selected' : ''}" onclick="setSelection('with_backing')">
991
- <input type="radio" name="selection" value="with_backing">
992
- <div class="selection-radio"></div>
993
- <div class="selection-label">
994
- <div class="selection-label-title">With Backing Vocals</div>
995
- <div class="selection-label-desc">All backing vocals included</div>
996
- </div>
997
- </label>
998
- ${hasOriginal ? `
999
- <label class="selection-option ${selectedOption === 'original' ? 'selected' : ''}" onclick="setSelection('original')">
1000
- <input type="radio" name="selection" value="original">
1001
- <div class="selection-radio"></div>
1002
- <div class="selection-label">
1003
- <div class="selection-label-title">Original Audio</div>
1004
- <div class="selection-label-desc">Full original with lead vocals</div>
1005
- </div>
1006
- </label>
1007
- ` : ''}
1008
- ${hasCustom ? `
1009
- <label class="selection-option ${selectedOption === 'custom' ? 'selected' : ''}" onclick="setSelection('custom')">
1010
- <input type="radio" name="selection" value="custom">
1011
- <div class="selection-radio"></div>
1012
- <div class="selection-label">
1013
- <div class="selection-label-title">Custom</div>
1014
- <div class="selection-label-desc">${muteRegions.length} regions muted</div>
1015
- </div>
1016
- </label>
1017
- ` : ''}
1018
- ${hasUploaded ? `
1019
- <label class="selection-option ${selectedOption === 'uploaded' ? 'selected' : ''}" onclick="setSelection('uploaded')">
1020
- <input type="radio" name="selection" value="uploaded">
1021
- <div class="selection-radio"></div>
1022
- <div class="selection-label">
1023
- <div class="selection-label-title">Uploaded</div>
1024
- <div class="selection-label-desc">${escapeHtml(uploadedFilename)}</div>
1025
- </div>
1026
- </label>
1027
- ` : ''}
1028
- </div>
1029
- <button class="btn btn-primary submit-btn" id="submit-btn" onclick="submitSelection()">
1030
- ✓ Confirm & Continue
1031
- </button>
1032
- </div>
1033
- </div>
1034
- `;
1035
-
1036
- // Setup after render
1037
- if (waveformData) {
1038
- resizeCanvas();
1039
- drawWaveform();
1040
- setupWaveformInteraction();
1041
- }
1042
-
1043
- // Setup audio state - add listeners when element changes
1044
- const audio = document.getElementById('audio-player');
1045
- if (audio) {
1046
- // Check if this is a new audio element (DOM was rebuilt)
1047
- if (audio !== currentAudioElement) {
1048
- audio.addEventListener('timeupdate', onTimeUpdate);
1049
- audio.addEventListener('play', onAudioPlay);
1050
- audio.addEventListener('pause', onAudioPause);
1051
- audio.addEventListener('ended', onAudioEnded);
1052
- currentAudioElement = audio;
1053
- }
1054
-
1055
- // Restore playback position and state after audio is ready
1056
- audio.addEventListener('loadeddata', function onLoaded() {
1057
- audio.currentTime = currentTime;
1058
- if (wasPlaying) {
1059
- audio.play().catch(() => {});
1060
- }
1061
- }, { once: true });
1062
-
1063
- // If already loaded (cached), set time directly
1064
- if (audio.readyState >= 2) {
1065
- audio.currentTime = currentTime;
1066
- if (wasPlaying) {
1067
- audio.play().catch(() => {});
1068
- }
1069
- }
1070
- }
1071
- }
1072
-
1073
- function resizeCanvas() {
1074
- const canvas = document.getElementById('waveform-canvas');
1075
- const container = document.getElementById('waveform-container');
1076
- const area = document.getElementById('waveform-area');
1077
- if (!canvas || !container || !area) return;
1078
-
1079
- // Set canvas width based on container width * zoom level
1080
- canvas.width = container.clientWidth * zoomLevel;
1081
- canvas.height = container.clientHeight;
1082
-
1083
- // Update area width to match
1084
- area.style.width = `${zoomLevel * 100}%`;
1085
- }
1086
-
1087
- function drawWaveform() {
1088
- const canvas = document.getElementById('waveform-canvas');
1089
- if (!canvas || !waveformData) return;
1090
-
1091
- const ctx = canvas.getContext('2d');
1092
- const { amplitudes } = waveformData;
1093
- const width = canvas.width;
1094
- const height = canvas.height;
1095
- const centerY = height / 2;
1096
- const bgColor = getComputedStyle(document.documentElement).getPropertyValue('--waveform-bg').trim();
1097
-
1098
- // Clear
1099
- ctx.fillStyle = bgColor;
1100
- ctx.fillRect(0, 0, width, height);
1101
-
1102
- // Draw center line
1103
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
1104
- ctx.setLineDash([4, 4]);
1105
- ctx.beginPath();
1106
- ctx.moveTo(0, centerY);
1107
- ctx.lineTo(width, centerY);
1108
- ctx.stroke();
1109
- ctx.setLineDash([]);
1110
-
1111
- // Draw waveform bars
1112
- const barWidth = width / amplitudes.length;
1113
-
1114
- amplitudes.forEach((amp, i) => {
1115
- const x = i * barWidth;
1116
- const barHeight = Math.max(2, amp * height * 0.9);
1117
- const y = centerY - barHeight / 2;
1118
- const time = (i / amplitudes.length) * duration;
1119
-
1120
- // Check regions
1121
- const inMuteRegion = muteRegions.some(r => time >= r.start_seconds && time <= r.end_seconds);
1122
- const inAudibleSegment = analysisData.analysis.audible_segments.some(
1123
- s => time >= s.start_seconds && time <= s.end_seconds
1124
- );
1125
-
1126
- if (inMuteRegion) {
1127
- // Muted regions blend into background - very subtle
1128
- ctx.fillStyle = 'rgba(13, 17, 23, 0.8)';
1129
- } else if (inAudibleSegment) {
1130
- ctx.fillStyle = '#ec4899'; // pink for detected backing vocals
1131
- } else {
1132
- ctx.fillStyle = '#60a5fa'; // blue for rest
1133
- }
1134
-
1135
- ctx.fillRect(x, y, Math.max(1, barWidth - 0.5), barHeight);
1136
- });
1137
- }
1138
-
1139
- function setupWaveformInteraction() {
1140
- const canvas = document.getElementById('waveform-canvas');
1141
- const area = document.getElementById('waveform-area');
1142
- if (!canvas || !area) return;
1143
-
1144
- canvas.onmousedown = (e) => {
1145
- const rect = canvas.getBoundingClientRect();
1146
- const x = e.clientX - rect.left;
1147
-
1148
- // Guard against invalid duration
1149
- if (!Number.isFinite(duration) || duration <= 0 || !Number.isFinite(rect.width) || rect.width <= 0) {
1150
- return;
1151
- }
1152
-
1153
- const time = (x / rect.width) * duration;
1154
-
1155
- // Shift+drag to select mute region
1156
- if (e.shiftKey) {
1157
- isDragging = true;
1158
- dragStartTime = time;
1159
- selectionStartX = x;
1160
- updateSelectionOverlay(x, x);
1161
- showSelectionOverlay(true);
1162
- } else {
1163
- // Regular click to seek and play
1164
- seekTo(time);
1165
- }
1166
- };
1167
-
1168
- canvas.onmousemove = (e) => {
1169
- if (!isDragging) return;
1170
- const rect = canvas.getBoundingClientRect();
1171
- const x = e.clientX - rect.left;
1172
- updateSelectionOverlay(selectionStartX, x);
1173
- };
1174
-
1175
- const endDrag = (e) => {
1176
- if (!isDragging) return;
1177
-
1178
- const rect = canvas.getBoundingClientRect();
1179
- const x = e.clientX - rect.left;
1180
-
1181
- // Guard against invalid duration
1182
- if (!Number.isFinite(duration) || duration <= 0 || !Number.isFinite(rect.width) || rect.width <= 0) {
1183
- isDragging = false;
1184
- showSelectionOverlay(false);
1185
- return;
1186
- }
1187
-
1188
- const time = (x / rect.width) * duration;
1189
-
1190
- const start = Math.min(dragStartTime, time);
1191
- const end = Math.max(dragStartTime, time);
1192
-
1193
- if (end - start > 0.5) {
1194
- addRegion(start, end);
1195
- }
1196
-
1197
- isDragging = false;
1198
- showSelectionOverlay(false);
1199
- };
1200
-
1201
- canvas.onmouseup = endDrag;
1202
- canvas.onmouseleave = (e) => {
1203
- if (isDragging) endDrag(e);
1204
- };
1205
- }
1206
-
1207
- function updateSelectionOverlay(startX, endX) {
1208
- const overlay = document.getElementById('selection-overlay');
1209
- if (!overlay) return;
1210
-
1211
- const left = Math.min(startX, endX);
1212
- const width = Math.abs(endX - startX);
1213
-
1214
- overlay.style.left = `${left}px`;
1215
- overlay.style.width = `${width}px`;
1216
- }
1217
-
1218
- function showSelectionOverlay(show) {
1219
- const overlay = document.getElementById('selection-overlay');
1220
- if (overlay) {
1221
- overlay.classList.toggle('hidden', !show);
1222
- }
1223
- }
1224
-
1225
- function startPlayheadAnimation() {
1226
- const animate = () => {
1227
- updatePlayhead();
1228
- animationFrameId = requestAnimationFrame(animate);
1229
- };
1230
- animate();
1231
- }
1232
-
1233
- function stopPlayheadAnimation() {
1234
- if (animationFrameId) {
1235
- cancelAnimationFrame(animationFrameId);
1236
- animationFrameId = null;
1237
- }
1238
- }
1239
-
1240
- function updatePlayhead() {
1241
- const playhead = document.getElementById('playhead');
1242
- const canvas = document.getElementById('waveform-canvas');
1243
- const audio = document.getElementById('audio-player');
1244
-
1245
- if (!playhead || !canvas || !audio) return;
1246
-
1247
- currentTime = audio.currentTime;
1248
-
1249
- // Update time display regardless of playhead position validity
1250
- const timeEl = document.getElementById('current-time');
1251
- if (timeEl) timeEl.textContent = formatTime(currentTime);
1252
-
1253
- // Guard against NaN/Infinity when calculating playhead position
1254
- if (!Number.isFinite(duration) || duration <= 0 || !Number.isFinite(canvas.width) || canvas.width <= 0) {
1255
- playhead.style.left = '0px';
1256
- } else {
1257
- const x = Math.max(0, Math.min((currentTime / duration) * canvas.width, canvas.width));
1258
- playhead.style.left = `${x}px`;
1259
- }
1260
-
1261
- playhead.classList.remove('hidden');
1262
- }
1263
-
1264
- function updatePlayButton() {
1265
- const btn = document.getElementById('play-btn');
1266
- if (btn) btn.innerHTML = isPlaying ? '⏸' : '▶';
1267
- }
1268
-
1269
- function togglePlayPause() {
1270
- const audio = document.getElementById('audio-player');
1271
- if (!audio) return;
1272
-
1273
- if (isPlaying) {
1274
- audio.pause();
1275
- } else {
1276
- audio.play();
1277
- }
1278
- }
1279
-
1280
- function seekTo(time, autoPlay = true) {
1281
- const audio = document.getElementById('audio-player');
1282
- // Guard against non-finite time values (NaN, Infinity)
1283
- if (!audio || !Number.isFinite(time)) return;
1284
-
1285
- audio.currentTime = time;
1286
- currentTime = time;
1287
- updatePlayhead();
1288
- // Auto-play when seeking via click (if not already playing)
1289
- if (autoPlay && !isPlaying) {
1290
- audio.play();
1291
- }
1292
- }
1293
-
1294
- function onTimeUpdate(e) {
1295
- currentTime = e.target.currentTime;
1296
- }
1297
-
1298
-
1299
- function setActiveAudio(type) {
1300
- const audio = document.getElementById('audio-player');
1301
- if (!audio) return;
1302
-
1303
- const wasPlaying = !audio.paused;
1304
- const time = audio.currentTime;
1305
-
1306
- // Pause before changing source
1307
- audio.pause();
1308
-
1309
- activeAudio = type;
1310
-
1311
- // Update toggle button states using data attributes (robust detection)
1312
- document.querySelectorAll('.audio-toggle').forEach(btn => {
1313
- const btnType = btn.dataset.audioType || 'custom';
1314
- btn.classList.toggle('active', btnType === type);
1315
- });
1316
-
1317
- // Change source and restore playback
1318
- audio.src = getAudioUrl();
1319
- audio.addEventListener('loadeddata', function onLoaded() {
1320
- audio.currentTime = time;
1321
- if (wasPlaying) {
1322
- audio.play().catch(() => {});
1323
- }
1324
- }, { once: true });
1325
- }
1326
-
1327
- function getAudioUrl() {
1328
- const stemTypes = {
1329
- original: 'original',
1330
- backing: 'backing_vocals',
1331
- clean: 'clean_instrumental',
1332
- with_backing: 'with_backing',
1333
- custom: 'custom_instrumental',
1334
- uploaded: 'uploaded_instrumental'
1335
- };
1336
- const stemType = stemTypes[activeAudio] || stemTypes.backing;
1337
-
1338
- // Cloud mode uses /audio-stream/{stem_type}, local mode uses /api/audio/{stem_type}
1339
- const isCloudMode = !!encodedBaseApiUrl;
1340
- const url = isCloudMode
1341
- ? `${API_BASE}/audio-stream/${stemType}`
1342
- : `/api/audio/${stemType}`;
1343
-
1344
- return addTokenToUrl(url);
1345
- }
1346
-
1347
- function formatTime(seconds) {
1348
- // Guard against NaN/Infinity
1349
- if (!Number.isFinite(seconds)) return '0:00';
1350
- const mins = Math.floor(seconds / 60);
1351
- const secs = Math.floor(seconds % 60);
1352
- return `${mins}:${secs.toString().padStart(2, '0')}`;
1353
- }
1354
-
1355
- function addRegion(start, end) {
1356
- muteRegions.push({ start_seconds: start, end_seconds: end });
1357
- muteRegions.sort((a, b) => a.start_seconds - b.start_seconds);
1358
- mergeOverlappingRegions();
1359
- hasCustom = false; // Invalidate custom when regions change
1360
-
1361
- // Just redraw waveform instead of full render to preserve scroll position
1362
- drawWaveform();
1363
- updateMuteRegionsPanel();
1364
- }
1365
-
1366
- function updateMuteRegionsPanel() {
1367
- // Update only the mute regions panel without full DOM rebuild
1368
- const panel = document.querySelector('.mute-panel');
1369
- if (!panel) return;
1370
-
1371
- const segments = analysisData.analysis.audible_segments;
1372
- const hasSegments = segments.length > 0;
1373
-
1374
- // Build mute regions list HTML
1375
- let regionsListHtml = '';
1376
- if (muteRegions.length > 0) {
1377
- regionsListHtml = '<div class="mute-regions-list">' +
1378
- muteRegions.map((r, i) =>
1379
- '<div class="mute-region-tag">' +
1380
- '<span onclick="seekTo(' + r.start_seconds + ', true)" style="cursor: pointer">' +
1381
- formatTime(r.start_seconds) + ' – ' + formatTime(r.end_seconds) + '</span>' +
1382
- '<button onclick="removeRegion(' + i + ')">×</button>' +
1383
- '</div>'
1384
- ).join('') +
1385
- '</div>';
1386
- } else {
1387
- regionsListHtml = '<div style="color: var(--text-muted); font-size: 0.75rem;">' +
1388
- (hasSegments ? 'Click segments below or <kbd class="kbd">Shift</kbd> + drag on waveform' : 'No backing vocals detected – clean instrumental recommended') +
1389
- '</div>';
1390
- }
1391
-
1392
- // Build quick segments HTML
1393
- let quickSegmentsHtml = '';
1394
- if (hasSegments) {
1395
- quickSegmentsHtml = '<div class="quick-segments">' +
1396
- segments.slice(0, 8).map((seg, i) =>
1397
- '<button class="quick-segment-btn" onclick="addSegmentAsRegion(' + i + ')" title="Add to mute regions">' +
1398
- formatTime(seg.start_seconds) + ' – ' + formatTime(seg.end_seconds) +
1399
- '</button>'
1400
- ).join('') +
1401
- (segments.length > 8 ? '<span style="font-size: 0.7rem; color: var(--text-muted); padding: 3px;">+' + (segments.length - 8) + ' more</span>' : '') +
1402
- '</div>';
1403
- }
1404
-
1405
- // Build header buttons
1406
- let headerButtons = '';
1407
- if (muteRegions.length > 0) {
1408
- headerButtons = '<div style="display: flex; gap: 6px;">' +
1409
- '<button class="btn btn-sm btn-secondary" onclick="clearAllRegions()">Clear</button>' +
1410
- (!hasCustom ? '<button class="btn btn-sm btn-primary" id="create-custom-btn" onclick="createCustomInstrumental()">Create Custom</button>' : '') +
1411
- '</div>';
1412
- }
1413
-
1414
- panel.innerHTML =
1415
- '<div class="mute-panel-header">' +
1416
- '<span class="mute-panel-title">Mute Regions ' + (muteRegions.length > 0 ? '(' + muteRegions.length + ')' : '') + '</span>' +
1417
- headerButtons +
1418
- '</div>' +
1419
- regionsListHtml +
1420
- quickSegmentsHtml;
1421
- }
1422
-
1423
- function addSegmentAsRegion(index) {
1424
- const seg = analysisData.analysis.audible_segments[index];
1425
- if (seg) {
1426
- addRegion(seg.start_seconds, seg.end_seconds);
1427
- }
1428
- }
1429
-
1430
- function removeRegion(index) {
1431
- muteRegions.splice(index, 1);
1432
- hasCustom = false;
1433
- drawWaveform();
1434
- updateMuteRegionsPanel();
1435
- }
1436
-
1437
- function clearAllRegions() {
1438
- muteRegions = [];
1439
- hasCustom = false;
1440
- drawWaveform();
1441
- updateMuteRegionsPanel();
1442
- }
1443
-
1444
- function mergeOverlappingRegions() {
1445
- if (muteRegions.length < 2) return;
1446
-
1447
- const merged = [muteRegions[0]];
1448
- for (let i = 1; i < muteRegions.length; i++) {
1449
- const last = merged[merged.length - 1];
1450
- const curr = muteRegions[i];
1451
-
1452
- if (curr.start_seconds <= last.end_seconds + 0.5) {
1453
- last.end_seconds = Math.max(last.end_seconds, curr.end_seconds);
1454
- } else {
1455
- merged.push(curr);
1456
- }
1457
- }
1458
- muteRegions = merged;
1459
- }
1460
-
1461
- function setSelection(value) {
1462
- selectedOption = value;
1463
- render();
1464
- }
1465
-
1466
- function setZoom(level) {
1467
- const container = document.getElementById('waveform-container');
1468
- const oldScrollRatio = container ? container.scrollLeft / (container.scrollWidth - container.clientWidth || 1) : 0;
1469
-
1470
- zoomLevel = level;
1471
-
1472
- // Update zoom button states directly (avoid full render)
1473
- document.querySelectorAll('.zoom-btn').forEach(btn => {
1474
- const btnLevel = parseInt(btn.textContent);
1475
- btn.classList.toggle('active', btnLevel === level);
1476
- });
1477
-
1478
- // Resize canvas and redraw
1479
- resizeCanvas();
1480
- drawWaveform();
1481
-
1482
- // Maintain scroll position proportionally
1483
- if (container && zoomLevel > 1) {
1484
- const newMaxScroll = container.scrollWidth - container.clientWidth;
1485
- container.scrollLeft = oldScrollRatio * newMaxScroll;
1486
- }
1487
- }
1488
-
1489
- async function handleUpload(event) {
1490
- const file = event.target.files[0];
1491
- if (!file) return;
1492
-
1493
- // Show upload progress
1494
- const overlay = document.createElement('div');
1495
- overlay.className = 'upload-overlay';
1496
- overlay.id = 'upload-overlay';
1497
- document.body.appendChild(overlay);
1498
-
1499
- const progress = document.createElement('div');
1500
- progress.className = 'upload-progress';
1501
- progress.id = 'upload-progress';
1502
- progress.innerHTML = `
1503
- <div class="spinner" style="margin: 0 auto 12px;"></div>
1504
- <div>Uploading ${file.name}...</div>
1505
- <div style="font-size: 0.8rem; color: var(--text-muted); margin-top: 8px;">Validating duration...</div>
1506
- `;
1507
- document.body.appendChild(progress);
1508
-
1509
- try {
1510
- const formData = new FormData();
1511
- formData.append('file', file);
1512
-
1513
- const response = await authFetch(`${API_BASE}/upload-instrumental`, {
1514
- method: 'POST',
1515
- body: formData
1516
- });
1517
-
1518
- if (!response.ok) {
1519
- const data = await response.json();
1520
- throw new Error(data.detail || 'Upload failed');
1521
- }
1522
-
1523
- const result = await response.json();
1524
- hasUploaded = true;
1525
- uploadedFilename = file.name;
1526
- activeAudio = 'uploaded';
1527
- selectedOption = 'uploaded';
1528
-
1529
- render();
1530
- showSuccess(`Uploaded ${file.name} (${result.duration_seconds.toFixed(1)}s)`);
1531
- } catch (error) {
1532
- showError(error.message);
1533
- } finally {
1534
- // Clean up progress UI
1535
- document.getElementById('upload-overlay')?.remove();
1536
- document.getElementById('upload-progress')?.remove();
1537
- // Reset file input so same file can be uploaded again
1538
- event.target.value = '';
1539
- }
1540
- }
1541
-
1542
- function showSuccess(message) {
1543
- const existing = document.querySelector('.alert-success-toast');
1544
- if (existing) existing.remove();
1545
-
1546
- const el = document.createElement('div');
1547
- el.className = 'alert-error'; // Reuse error styling but green
1548
- el.style.background = 'rgba(34, 197, 94, 0.95)';
1549
- el.textContent = message;
1550
- document.body.appendChild(el);
1551
-
1552
- setTimeout(() => el.remove(), 3000);
1553
- }
1554
-
1555
- async function createCustomInstrumental() {
1556
- const btn = document.getElementById('create-custom-btn');
1557
- const audio = document.getElementById('audio-player');
1558
- const wasPlaying = isPlaying;
1559
- const time = currentTime;
1560
-
1561
- // Pause audio while creating custom instrumental
1562
- if (audio && !audio.paused) {
1563
- audio.pause();
1564
- }
1565
-
1566
- if (btn) {
1567
- btn.disabled = true;
1568
- btn.textContent = 'Creating...';
1569
- }
1570
-
1571
- try {
1572
- const response = await authFetch(`${API_BASE}/create-custom-instrumental`, {
1573
- method: 'POST',
1574
- headers: { 'Content-Type': 'application/json' },
1575
- body: JSON.stringify({ mute_regions: muteRegions })
1576
- });
1577
-
1578
- if (!response.ok) {
1579
- const data = await response.json();
1580
- throw new Error(data.detail || 'Failed to create custom');
1581
- }
1582
-
1583
- hasCustom = true;
1584
- selectedOption = 'custom';
1585
- activeAudio = 'custom';
1586
-
1587
- // Render first, then restore playback
1588
- render();
1589
-
1590
- // After render, seek to previous position and optionally resume
1591
- const newAudio = document.getElementById('audio-player');
1592
- if (newAudio) {
1593
- newAudio.addEventListener('loadeddata', function onLoaded() {
1594
- newAudio.removeEventListener('loadeddata', onLoaded);
1595
- newAudio.currentTime = time;
1596
- if (wasPlaying) {
1597
- newAudio.play().catch(() => {});
1598
- }
1599
- }, { once: true });
1600
- }
1601
- } catch (error) {
1602
- showError(error.message);
1603
- if (btn) {
1604
- btn.disabled = false;
1605
- btn.textContent = 'Create Custom';
1606
- }
1607
- // Resume playback if there was an error
1608
- if (wasPlaying && audio) {
1609
- audio.play().catch(() => {});
1610
- }
1611
- }
1612
- }
1613
-
1614
- async function submitSelection() {
1615
- const btn = document.getElementById('submit-btn');
1616
- if (btn) {
1617
- btn.disabled = true;
1618
- btn.textContent = 'Submitting...';
1619
- }
1620
-
1621
- try {
1622
- const response = await authFetch(`${API_BASE}/select-instrumental`, {
1623
- method: 'POST',
1624
- headers: { 'Content-Type': 'application/json' },
1625
- body: JSON.stringify({ selection: selectedOption })
1626
- });
1627
-
1628
- if (!response.ok) {
1629
- const data = await response.json();
1630
- throw new Error(data.detail || 'Failed to submit');
1631
- }
1632
-
1633
- const selectionLabels = {
1634
- clean: 'Clean Instrumental',
1635
- with_backing: 'With Backing Vocals',
1636
- custom: 'Custom',
1637
- uploaded: 'Uploaded Instrumental',
1638
- original: 'Original Audio'
1639
- };
1640
- const selectionLabel = selectionLabels[selectedOption] || selectedOption;
1641
-
1642
- document.getElementById('app').innerHTML = `
1643
- <div class="success-screen">
1644
- <h2>✓ Selection Submitted</h2>
1645
- <p>You selected: <strong>${escapeHtml(selectionLabel)}</strong></p>
1646
- <p id="close-msg" style="color: var(--text-muted);">Closing in <span id="countdown">2</span>s...</p>
1647
- </div>
1648
- `;
1649
-
1650
- // Auto-close window after 2 seconds
1651
- let countdown = 2;
1652
- const countdownEl = document.getElementById('countdown');
1653
- const countdownInterval = setInterval(() => {
1654
- countdown--;
1655
- if (countdownEl) countdownEl.textContent = countdown;
1656
- if (countdown <= 0) {
1657
- clearInterval(countdownInterval);
1658
- // Try to close the window (works for windows opened by script)
1659
- window.close();
1660
- // If window.close() didn't work, update message
1661
- const msg = document.getElementById('close-msg');
1662
- if (msg) msg.textContent = 'You can close this window now.';
1663
- }
1664
- }, 1000);
1665
- } catch (error) {
1666
- showError(error.message);
1667
- if (btn) {
1668
- btn.disabled = false;
1669
- btn.textContent = '✓ Confirm & Continue';
1670
- }
1671
- }
1672
- }
1673
-
1674
- function setupKeyboardShortcuts() {
1675
- document.addEventListener('keydown', (e) => {
1676
- // Ignore if typing in input
1677
- if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
1678
-
1679
- switch (e.code) {
1680
- case 'Space':
1681
- e.preventDefault();
1682
- togglePlayPause();
1683
- break;
1684
- case 'Escape':
1685
- // Cancel any in-progress drag selection
1686
- if (isDragging) {
1687
- isDragging = false;
1688
- showSelectionOverlay(false);
1689
- }
1690
- break;
1691
- }
1692
- });
1693
- }
1694
-
1695
- function showError(message) {
1696
- // Remove any existing error
1697
- const existing = document.querySelector('.alert-error');
1698
- if (existing) existing.remove();
1699
-
1700
- const errorEl = document.createElement('div');
1701
- errorEl.className = 'alert-error';
1702
- errorEl.textContent = message;
1703
- document.body.appendChild(errorEl);
1704
-
1705
- setTimeout(() => errorEl.remove(), 5000);
1706
- }
1707
-
1708
- // Handle window resize
1709
- window.addEventListener('resize', () => {
1710
- if (waveformData) {
1711
- resizeCanvas();
1712
- drawWaveform();
1713
- }
1714
- });
1715
-
1716
- // Start
1717
- init();
1718
- </script>
1719
- </body>
1720
- </html>
1721
-