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,999 +0,0 @@
1
- import { useState, useCallback, useEffect, useRef, useMemo, memo } from 'react'
2
- import {
3
- Box,
4
- Typography,
5
- Slider,
6
- Dialog,
7
- DialogTitle,
8
- DialogContent,
9
- DialogActions,
10
- TextField,
11
- Button,
12
- Paper,
13
- Alert,
14
- useTheme,
15
- useMediaQuery
16
- } from '@mui/material'
17
- import ZoomInIcon from '@mui/icons-material/ZoomIn'
18
- import ZoomOutIcon from '@mui/icons-material/ZoomOut'
19
- import { Word, LyricsSegment } from '../../types'
20
- import TimelineCanvas from './TimelineCanvas'
21
- import UpcomingWordsBar from './UpcomingWordsBar'
22
- import SyncControls from './SyncControls'
23
-
24
- // Augment window type for audio functions
25
- declare global {
26
- interface Window {
27
- getAudioDuration?: () => number
28
- toggleAudioPlayback?: () => void
29
- isAudioPlaying?: boolean
30
- }
31
- }
32
-
33
- interface LyricsSynchronizerProps {
34
- segments: LyricsSegment[]
35
- currentTime: number
36
- onPlaySegment?: (startTime: number) => void
37
- onSave: (segments: LyricsSegment[]) => void
38
- onCancel: () => void
39
- setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
40
- }
41
-
42
- // Constants for zoom
43
- const MIN_ZOOM_SECONDS = 2 // Most zoomed in - 2 seconds visible
44
- const MAX_ZOOM_SECONDS = 24 // Most zoomed out - 24 seconds visible
45
- const ZOOM_STEPS = 50 // Number of zoom levels
46
-
47
- // Get all words from segments
48
- function getAllWords(segments: LyricsSegment[]): Word[] {
49
- return segments.flatMap(s => s.words)
50
- }
51
-
52
- // Deep clone segments
53
- function cloneSegments(segments: LyricsSegment[]): LyricsSegment[] {
54
- return JSON.parse(JSON.stringify(segments))
55
- }
56
-
57
- const LyricsSynchronizer = memo(function LyricsSynchronizer({
58
- segments: initialSegments,
59
- currentTime,
60
- onPlaySegment,
61
- onSave,
62
- onCancel,
63
- setModalSpacebarHandler
64
- }: LyricsSynchronizerProps) {
65
- const theme = useTheme()
66
- const isDarkMode = theme.palette.mode === 'dark'
67
- const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
68
-
69
- // Working copy of segments
70
- const [workingSegments, setWorkingSegments] = useState<LyricsSegment[]>(() =>
71
- cloneSegments(initialSegments)
72
- )
73
-
74
- // Get all words flattened
75
- const allWords = useMemo(() => getAllWords(workingSegments), [workingSegments])
76
-
77
- // Audio duration
78
- const audioDuration = useMemo(() => {
79
- if (typeof window.getAudioDuration === 'function') {
80
- const duration = window.getAudioDuration()
81
- return duration > 0 ? duration : 300
82
- }
83
- return 300 // 5 minute fallback
84
- }, [])
85
-
86
- // Zoom state (value is the visible time window in seconds)
87
- const [zoomSeconds, setZoomSeconds] = useState(12) // Default ~12 seconds visible
88
-
89
- // Visible time range
90
- const [visibleStartTime, setVisibleStartTime] = useState(0)
91
- const visibleEndTime = useMemo(() =>
92
- Math.min(visibleStartTime + zoomSeconds, audioDuration),
93
- [visibleStartTime, zoomSeconds, audioDuration]
94
- )
95
-
96
- // Manual sync state
97
- const [isManualSyncing, setIsManualSyncing] = useState(false)
98
- const [isPaused, setIsPaused] = useState(false)
99
- const [syncWordIndex, setSyncWordIndex] = useState(-1)
100
- const [isSpacebarPressed, setIsSpacebarPressed] = useState(false)
101
- const wordStartTimeRef = useRef<number | null>(null)
102
- const spacebarPressTimeRef = useRef<number | null>(null)
103
- const currentTimeRef = useRef(currentTime)
104
-
105
- // Selection state
106
- const [selectedWordIds, setSelectedWordIds] = useState<Set<string>>(new Set())
107
-
108
- // Edit lyrics modal state
109
- const [showEditLyricsModal, setShowEditLyricsModal] = useState(false)
110
- const [editLyricsText, setEditLyricsText] = useState('')
111
-
112
- // Edit word modal state
113
- const [showEditWordModal, setShowEditWordModal] = useState(false)
114
- const [editWordText, setEditWordText] = useState('')
115
- const [editWordId, setEditWordId] = useState<string | null>(null)
116
-
117
- // Keep currentTimeRef up to date
118
- useEffect(() => {
119
- currentTimeRef.current = currentTime
120
- }, [currentTime])
121
-
122
- // Auto-scroll to follow playhead during sync
123
- useEffect(() => {
124
- if (isManualSyncing && !isPaused && currentTime > 0) {
125
- // If playhead is near the end of visible area, scroll forward
126
- if (currentTime > visibleEndTime - (zoomSeconds * 0.1)) {
127
- const newStart = Math.max(0, currentTime - zoomSeconds * 0.1)
128
- setVisibleStartTime(newStart)
129
- }
130
- // If playhead is before visible area, scroll back
131
- else if (currentTime < visibleStartTime) {
132
- setVisibleStartTime(Math.max(0, currentTime - 1))
133
- }
134
- }
135
- }, [currentTime, isManualSyncing, isPaused, visibleStartTime, visibleEndTime, zoomSeconds])
136
-
137
- // Handle zoom slider change
138
- const handleZoomChange = useCallback((_: Event, value: number | number[]) => {
139
- const zoomValue = value as number
140
- // Map slider value (0-50) to zoom range (4.5-24 seconds)
141
- const newZoomSeconds = MIN_ZOOM_SECONDS + (zoomValue / ZOOM_STEPS) * (MAX_ZOOM_SECONDS - MIN_ZOOM_SECONDS)
142
- setZoomSeconds(newZoomSeconds)
143
- }, [])
144
-
145
- // Get slider value from zoom seconds
146
- const sliderValue = useMemo(() => {
147
- return ((zoomSeconds - MIN_ZOOM_SECONDS) / (MAX_ZOOM_SECONDS - MIN_ZOOM_SECONDS)) * ZOOM_STEPS
148
- }, [zoomSeconds])
149
-
150
- // Handle scroll change from timeline
151
- const handleScrollChange = useCallback((newStartTime: number) => {
152
- setVisibleStartTime(newStartTime)
153
- }, [])
154
-
155
- // Update words in segments
156
- const updateWords = useCallback((newWords: Word[]) => {
157
- setWorkingSegments(prevSegments => {
158
- const newSegments = cloneSegments(prevSegments)
159
-
160
- // Create a map of word id to word for quick lookup
161
- const wordMap = new Map(newWords.map(w => [w.id, w]))
162
-
163
- // Update each segment's words
164
- for (const segment of newSegments) {
165
- segment.words = segment.words.map(w => wordMap.get(w.id) || w)
166
-
167
- // Recalculate segment timing
168
- const timedWords = segment.words.filter(w =>
169
- w.start_time !== null && w.end_time !== null
170
- )
171
-
172
- if (timedWords.length > 0) {
173
- segment.start_time = Math.min(...timedWords.map(w => w.start_time!))
174
- segment.end_time = Math.max(...timedWords.map(w => w.end_time!))
175
- } else {
176
- segment.start_time = null
177
- segment.end_time = null
178
- }
179
- }
180
-
181
- return newSegments
182
- })
183
- }, [])
184
-
185
- // Check if audio is playing
186
- const [isPlaying, setIsPlaying] = useState(false)
187
-
188
- // Update isPlaying state periodically
189
- useEffect(() => {
190
- const checkPlaying = () => {
191
- setIsPlaying(typeof window.isAudioPlaying === 'boolean' ? window.isAudioPlaying : false)
192
- }
193
- checkPlaying()
194
- const interval = setInterval(checkPlaying, 100)
195
- return () => clearInterval(interval)
196
- }, [])
197
-
198
- // Play audio from current position
199
- const handlePlayAudio = useCallback(() => {
200
- if (onPlaySegment) {
201
- onPlaySegment(currentTimeRef.current)
202
- }
203
- }, [onPlaySegment])
204
-
205
- // Stop audio playback - also exits sync mode
206
- const handleStopAudio = useCallback(() => {
207
- if (typeof window.toggleAudioPlayback === 'function' && window.isAudioPlaying) {
208
- window.toggleAudioPlayback()
209
- }
210
- // Also exit sync mode when stopping
211
- if (isManualSyncing) {
212
- setIsManualSyncing(false)
213
- setIsPaused(false)
214
- setIsSpacebarPressed(false)
215
- }
216
- }, [isManualSyncing])
217
-
218
- // Start manual sync
219
- const handleStartSync = useCallback(() => {
220
- if (isManualSyncing) {
221
- // Stop sync
222
- setIsManualSyncing(false)
223
- setIsPaused(false)
224
- setSyncWordIndex(-1)
225
- setIsSpacebarPressed(false)
226
-
227
- // Stop audio
228
- handleStopAudio()
229
- return
230
- }
231
-
232
- // Find first unsynced word
233
- const firstUnsyncedIndex = allWords.findIndex(w =>
234
- w.start_time === null || w.end_time === null
235
- )
236
-
237
- const startIndex = firstUnsyncedIndex !== -1 ? firstUnsyncedIndex : 0
238
-
239
- setIsManualSyncing(true)
240
- setIsPaused(false)
241
- setSyncWordIndex(startIndex)
242
- setIsSpacebarPressed(false)
243
-
244
- // Start playback
245
- if (onPlaySegment) {
246
- onPlaySegment(Math.max(0, currentTimeRef.current - 1))
247
- }
248
- }, [isManualSyncing, allWords, onPlaySegment, handleStopAudio])
249
-
250
- // Pause sync
251
- const handlePauseSync = useCallback(() => {
252
- setIsPaused(true)
253
- handleStopAudio()
254
- }, [handleStopAudio])
255
-
256
- // Resume sync
257
- const handleResumeSync = useCallback(() => {
258
- setIsPaused(false)
259
-
260
- // Find first unsynced word from current position
261
- const firstUnsyncedIndex = allWords.findIndex(w =>
262
- w.start_time === null || w.end_time === null
263
- )
264
-
265
- if (firstUnsyncedIndex !== -1 && firstUnsyncedIndex !== syncWordIndex) {
266
- setSyncWordIndex(firstUnsyncedIndex)
267
- }
268
-
269
- // Resume playback
270
- if (onPlaySegment) {
271
- onPlaySegment(currentTimeRef.current)
272
- }
273
- }, [allWords, syncWordIndex, onPlaySegment])
274
-
275
- // Clear all sync data
276
- const handleClearSync = useCallback(() => {
277
- setWorkingSegments(prevSegments => {
278
- const newSegments = cloneSegments(prevSegments)
279
-
280
- for (const segment of newSegments) {
281
- for (const word of segment.words) {
282
- word.start_time = null
283
- word.end_time = null
284
- }
285
- segment.start_time = null
286
- segment.end_time = null
287
- }
288
-
289
- return newSegments
290
- })
291
-
292
- setSyncWordIndex(-1)
293
- }, [])
294
-
295
- // Unsync from cursor position
296
- const handleUnsyncFromCursor = useCallback(() => {
297
- const cursorTime = currentTimeRef.current
298
-
299
- setWorkingSegments(prevSegments => {
300
- const newSegments = cloneSegments(prevSegments)
301
-
302
- for (const segment of newSegments) {
303
- for (const word of segment.words) {
304
- // Reset words that start after cursor position
305
- if (word.start_time !== null && word.start_time > cursorTime) {
306
- word.start_time = null
307
- word.end_time = null
308
- }
309
- }
310
-
311
- // Recalculate segment timing
312
- const timedWords = segment.words.filter(w =>
313
- w.start_time !== null && w.end_time !== null
314
- )
315
-
316
- if (timedWords.length > 0) {
317
- segment.start_time = Math.min(...timedWords.map(w => w.start_time!))
318
- segment.end_time = Math.max(...timedWords.map(w => w.end_time!))
319
- } else {
320
- segment.start_time = null
321
- segment.end_time = null
322
- }
323
- }
324
-
325
- return newSegments
326
- })
327
- }, [])
328
-
329
- // Check if there are words after cursor that can be unsynced
330
- const canUnsyncFromCursor = useMemo(() => {
331
- const cursorTime = currentTimeRef.current
332
- return allWords.some(w =>
333
- w.start_time !== null && w.start_time > cursorTime
334
- )
335
- }, [allWords, currentTime])
336
-
337
- // Open edit lyrics modal
338
- const handleEditLyrics = useCallback(() => {
339
- const text = workingSegments.map(s => s.text).join('\n')
340
- setEditLyricsText(text)
341
- setShowEditLyricsModal(true)
342
- }, [workingSegments])
343
-
344
- // Save edited lyrics (warning: resets timing)
345
- const handleSaveEditedLyrics = useCallback(() => {
346
- const lines = editLyricsText.split('\n').filter(l => l.trim())
347
-
348
- const newSegments: LyricsSegment[] = lines.map((line, idx) => {
349
- const words = line.trim().split(/\s+/).map((text, wIdx) => ({
350
- id: `word-${idx}-${wIdx}-${Date.now()}`,
351
- text,
352
- start_time: null,
353
- end_time: null,
354
- confidence: 1.0
355
- }))
356
-
357
- return {
358
- id: `segment-${idx}-${Date.now()}`,
359
- text: line.trim(),
360
- words,
361
- start_time: null,
362
- end_time: null
363
- }
364
- })
365
-
366
- setWorkingSegments(newSegments)
367
- setShowEditLyricsModal(false)
368
- setSyncWordIndex(-1)
369
- }, [editLyricsText])
370
-
371
- // Open edit word modal
372
- const handleEditSelectedWord = useCallback(() => {
373
- if (selectedWordIds.size !== 1) return
374
-
375
- const wordId = Array.from(selectedWordIds)[0]
376
- const word = allWords.find(w => w.id === wordId)
377
-
378
- if (word) {
379
- setEditWordId(wordId)
380
- setEditWordText(word.text)
381
- setShowEditWordModal(true)
382
- }
383
- }, [selectedWordIds, allWords])
384
-
385
- // Save edited word
386
- const handleSaveEditedWord = useCallback(() => {
387
- if (!editWordId) return
388
-
389
- const newText = editWordText.trim()
390
- if (!newText) return
391
-
392
- // Check if we're splitting into multiple words
393
- const newWords = newText.split(/\s+/)
394
-
395
- if (newWords.length === 1) {
396
- // Simple rename
397
- const updatedWords = allWords.map(w =>
398
- w.id === editWordId ? { ...w, text: newWords[0] } : w
399
- )
400
- updateWords(updatedWords)
401
- } else {
402
- // Split word - preserve timing for first word, null for rest
403
- const originalWord = allWords.find(w => w.id === editWordId)
404
- if (!originalWord) return
405
-
406
- setWorkingSegments(prevSegments => {
407
- const newSegments = cloneSegments(prevSegments)
408
-
409
- for (const segment of newSegments) {
410
- const wordIndex = segment.words.findIndex(w => w.id === editWordId)
411
- if (wordIndex !== -1) {
412
- const newWordObjects = newWords.map((text, idx) => ({
413
- id: idx === 0 ? editWordId : `${editWordId}-split-${idx}`,
414
- text,
415
- start_time: idx === 0 ? originalWord.start_time : null,
416
- end_time: idx === 0 ? originalWord.end_time : null,
417
- confidence: 1.0
418
- }))
419
-
420
- segment.words.splice(wordIndex, 1, ...newWordObjects)
421
- segment.text = segment.words.map(w => w.text).join(' ')
422
- break
423
- }
424
- }
425
-
426
- return newSegments
427
- })
428
- }
429
-
430
- setShowEditWordModal(false)
431
- setEditWordId(null)
432
- setEditWordText('')
433
- setSelectedWordIds(new Set())
434
- }, [editWordId, editWordText, allWords, updateWords])
435
-
436
- // Delete selected words
437
- const handleDeleteSelected = useCallback(() => {
438
- if (selectedWordIds.size === 0) return
439
-
440
- setWorkingSegments(prevSegments => {
441
- const newSegments = cloneSegments(prevSegments)
442
-
443
- for (const segment of newSegments) {
444
- segment.words = segment.words.filter(w => !selectedWordIds.has(w.id))
445
- segment.text = segment.words.map(w => w.text).join(' ')
446
-
447
- // Recalculate segment timing
448
- const timedWords = segment.words.filter(w =>
449
- w.start_time !== null && w.end_time !== null
450
- )
451
-
452
- if (timedWords.length > 0) {
453
- segment.start_time = Math.min(...timedWords.map(w => w.start_time!))
454
- segment.end_time = Math.max(...timedWords.map(w => w.end_time!))
455
- } else {
456
- segment.start_time = null
457
- segment.end_time = null
458
- }
459
- }
460
-
461
- // Remove empty segments
462
- return newSegments.filter(s => s.words.length > 0)
463
- })
464
-
465
- setSelectedWordIds(new Set())
466
- }, [selectedWordIds])
467
-
468
- // Handle word click (selection)
469
- const handleWordClick = useCallback((wordId: string, event: React.MouseEvent) => {
470
- if (event.shiftKey || event.ctrlKey || event.metaKey) {
471
- // Add to selection
472
- setSelectedWordIds(prev => {
473
- const newSet = new Set(prev)
474
- if (newSet.has(wordId)) {
475
- newSet.delete(wordId)
476
- } else {
477
- newSet.add(wordId)
478
- }
479
- return newSet
480
- })
481
- } else {
482
- // Single selection
483
- setSelectedWordIds(new Set([wordId]))
484
- }
485
- }, [])
486
-
487
- // Handle background click (deselect)
488
- const handleBackgroundClick = useCallback(() => {
489
- setSelectedWordIds(new Set())
490
- }, [])
491
-
492
- // Handle single word timing change (from resize)
493
- const handleWordTimingChange = useCallback((wordId: string, newStartTime: number, newEndTime: number) => {
494
- setWorkingSegments(prevSegments => {
495
- const newSegments = cloneSegments(prevSegments)
496
-
497
- for (const segment of newSegments) {
498
- const word = segment.words.find(w => w.id === wordId)
499
- if (word) {
500
- word.start_time = Math.max(0, newStartTime)
501
- word.end_time = Math.max(word.start_time + 0.05, newEndTime)
502
-
503
- // Recalculate segment timing
504
- const timedWords = segment.words.filter(w =>
505
- w.start_time !== null && w.end_time !== null
506
- )
507
- if (timedWords.length > 0) {
508
- segment.start_time = Math.min(...timedWords.map(w => w.start_time!))
509
- segment.end_time = Math.max(...timedWords.map(w => w.end_time!))
510
- }
511
- break
512
- }
513
- }
514
-
515
- return newSegments
516
- })
517
- }, [])
518
-
519
- // Handle moving multiple words (from drag)
520
- const handleWordsMove = useCallback((updates: Array<{ wordId: string; newStartTime: number; newEndTime: number }>) => {
521
- setWorkingSegments(prevSegments => {
522
- const newSegments = cloneSegments(prevSegments)
523
-
524
- // Create a map for quick lookup
525
- const updateMap = new Map(updates.map(u => [u.wordId, u]))
526
-
527
- for (const segment of newSegments) {
528
- for (const word of segment.words) {
529
- const update = updateMap.get(word.id)
530
- if (update) {
531
- word.start_time = update.newStartTime
532
- word.end_time = update.newEndTime
533
- }
534
- }
535
-
536
- // Recalculate segment timing
537
- const timedWords = segment.words.filter(w =>
538
- w.start_time !== null && w.end_time !== null
539
- )
540
- if (timedWords.length > 0) {
541
- segment.start_time = Math.min(...timedWords.map(w => w.start_time!))
542
- segment.end_time = Math.max(...timedWords.map(w => w.end_time!))
543
- }
544
- }
545
-
546
- return newSegments
547
- })
548
- }, [])
549
-
550
- // Handle time bar click (seek to position without playing)
551
- const handleTimeBarClick = useCallback((time: number) => {
552
- // Scroll the timeline to show this time centered
553
- const newStart = Math.max(0, time - zoomSeconds / 2)
554
- setVisibleStartTime(Math.min(newStart, Math.max(0, audioDuration - zoomSeconds)))
555
-
556
- // Seek to the position (this will briefly start playback)
557
- if (onPlaySegment) {
558
- onPlaySegment(time)
559
- // Immediately stop playback after seeking
560
- setTimeout(() => {
561
- if (typeof window.toggleAudioPlayback === 'function' && window.isAudioPlaying) {
562
- window.toggleAudioPlayback()
563
- }
564
- }, 50)
565
- }
566
- }, [zoomSeconds, audioDuration, onPlaySegment])
567
-
568
- // Handle selection complete from drag
569
- const handleSelectionComplete = useCallback((wordIds: string[]) => {
570
- setSelectedWordIds(new Set(wordIds))
571
- }, [])
572
-
573
- // Handle spacebar for manual sync
574
- const handleKeyDown = useCallback((e: KeyboardEvent) => {
575
- if (e.code !== 'Space') return
576
- if (!isManualSyncing || isPaused) return
577
- if (syncWordIndex < 0 || syncWordIndex >= allWords.length) return
578
-
579
- e.preventDefault()
580
- e.stopPropagation()
581
-
582
- if (isSpacebarPressed) return
583
-
584
- setIsSpacebarPressed(true)
585
- wordStartTimeRef.current = currentTimeRef.current
586
- spacebarPressTimeRef.current = Date.now()
587
-
588
- // Set start time for current word
589
- const newWords = [...allWords]
590
- const currentWord = newWords[syncWordIndex]
591
- currentWord.start_time = currentTimeRef.current
592
-
593
- // Handle previous word's end time
594
- if (syncWordIndex > 0) {
595
- const prevWord = newWords[syncWordIndex - 1]
596
- if (prevWord.start_time !== null && prevWord.end_time === null) {
597
- const gap = currentTimeRef.current - prevWord.start_time
598
- if (gap > 1.0) {
599
- prevWord.end_time = prevWord.start_time + 0.5
600
- } else {
601
- prevWord.end_time = currentTimeRef.current - 0.005
602
- }
603
- }
604
- }
605
-
606
- updateWords(newWords)
607
- }, [isManualSyncing, isPaused, syncWordIndex, allWords, isSpacebarPressed, updateWords])
608
-
609
- const handleKeyUp = useCallback((e: KeyboardEvent) => {
610
- if (e.code !== 'Space') return
611
- if (!isManualSyncing || isPaused) return
612
- if (!isSpacebarPressed) return
613
-
614
- e.preventDefault()
615
- e.stopPropagation()
616
-
617
- setIsSpacebarPressed(false)
618
-
619
- const pressDuration = spacebarPressTimeRef.current
620
- ? Date.now() - spacebarPressTimeRef.current
621
- : 0
622
- const isTap = pressDuration < 200 // 200ms threshold
623
-
624
- const newWords = [...allWords]
625
- const currentWord = newWords[syncWordIndex]
626
-
627
- if (isTap) {
628
- // Short tap: default 500ms duration
629
- currentWord.end_time = (wordStartTimeRef.current || currentTimeRef.current) + 0.5
630
- } else {
631
- // Hold: use actual timing
632
- currentWord.end_time = currentTimeRef.current
633
- }
634
-
635
- updateWords(newWords)
636
-
637
- // Move to next word
638
- if (syncWordIndex < allWords.length - 1) {
639
- setSyncWordIndex(syncWordIndex + 1)
640
- } else {
641
- // All words synced
642
- setIsManualSyncing(false)
643
- setSyncWordIndex(-1)
644
- handleStopAudio()
645
- }
646
-
647
- wordStartTimeRef.current = null
648
- spacebarPressTimeRef.current = null
649
- }, [isManualSyncing, isPaused, isSpacebarPressed, syncWordIndex, allWords, updateWords, handleStopAudio])
650
-
651
- // Combined spacebar handler
652
- const handleSpacebar = useCallback((e: KeyboardEvent) => {
653
- if (e.type === 'keydown') {
654
- handleKeyDown(e)
655
- } else if (e.type === 'keyup') {
656
- handleKeyUp(e)
657
- }
658
- }, [handleKeyDown, handleKeyUp])
659
-
660
- // Keep ref for handler
661
- const spacebarHandlerRef = useRef(handleSpacebar)
662
- spacebarHandlerRef.current = handleSpacebar
663
-
664
- // Set up spacebar handler
665
- useEffect(() => {
666
- const handler = (e: KeyboardEvent) => {
667
- if (e.code === 'Space') {
668
- e.preventDefault()
669
- e.stopPropagation()
670
- spacebarHandlerRef.current(e)
671
- }
672
- }
673
-
674
- setModalSpacebarHandler(() => handler)
675
-
676
- return () => {
677
- setModalSpacebarHandler(undefined)
678
- }
679
- }, [setModalSpacebarHandler])
680
-
681
- // Tap handlers for mobile (simulate spacebar press/release)
682
- const handleTapStart = useCallback(() => {
683
- if (!isManualSyncing || isPaused) return
684
- if (syncWordIndex < 0 || syncWordIndex >= allWords.length) return
685
- if (isSpacebarPressed) return
686
-
687
- setIsSpacebarPressed(true)
688
- wordStartTimeRef.current = currentTimeRef.current
689
- spacebarPressTimeRef.current = Date.now()
690
-
691
- // Set start time for current word
692
- const newWords = [...allWords]
693
- const currentWord = newWords[syncWordIndex]
694
- currentWord.start_time = currentTimeRef.current
695
-
696
- // Handle previous word's end time
697
- if (syncWordIndex > 0) {
698
- const prevWord = newWords[syncWordIndex - 1]
699
- if (prevWord.start_time !== null && prevWord.end_time === null) {
700
- const gap = currentTimeRef.current - prevWord.start_time
701
- if (gap > 1.0) {
702
- prevWord.end_time = prevWord.start_time + 0.5
703
- } else {
704
- prevWord.end_time = currentTimeRef.current - 0.005
705
- }
706
- }
707
- }
708
-
709
- updateWords(newWords)
710
- }, [isManualSyncing, isPaused, syncWordIndex, allWords, isSpacebarPressed, updateWords])
711
-
712
- const handleTapEnd = useCallback(() => {
713
- if (!isManualSyncing || isPaused) return
714
- if (!isSpacebarPressed) return
715
-
716
- setIsSpacebarPressed(false)
717
-
718
- const pressDuration = spacebarPressTimeRef.current
719
- ? Date.now() - spacebarPressTimeRef.current
720
- : 0
721
- const isTap = pressDuration < 200
722
-
723
- const newWords = [...allWords]
724
- const currentWord = newWords[syncWordIndex]
725
-
726
- if (isTap) {
727
- currentWord.end_time = (wordStartTimeRef.current || currentTimeRef.current) + 0.5
728
- } else {
729
- currentWord.end_time = currentTimeRef.current
730
- }
731
-
732
- updateWords(newWords)
733
-
734
- // Move to next word
735
- if (syncWordIndex < allWords.length - 1) {
736
- setSyncWordIndex(syncWordIndex + 1)
737
- } else {
738
- setIsManualSyncing(false)
739
- setSyncWordIndex(-1)
740
- handleStopAudio()
741
- }
742
-
743
- wordStartTimeRef.current = null
744
- spacebarPressTimeRef.current = null
745
- }, [isManualSyncing, isPaused, isSpacebarPressed, syncWordIndex, allWords, updateWords, handleStopAudio])
746
-
747
- // Handle save
748
- const handleSave = useCallback(() => {
749
- onSave(workingSegments)
750
- }, [workingSegments, onSave])
751
-
752
- // Progress stats
753
- const stats = useMemo(() => {
754
- const total = allWords.length
755
- const synced = allWords.filter(w =>
756
- w.start_time !== null && w.end_time !== null
757
- ).length
758
- return { total, synced, remaining: total - synced }
759
- }, [allWords])
760
-
761
- // Get instruction text based on state
762
- const getInstructionText = useCallback(() => {
763
- if (isManualSyncing) {
764
- if (isSpacebarPressed) {
765
- // Always include secondary text to prevent layout shift
766
- return { primary: '⏱️ Holding... release when word ends', secondary: 'Release spacebar when the word finishes' }
767
- }
768
- if (stats.remaining === 0) {
769
- return { primary: '✅ All words synced!', secondary: 'Click "Stop Sync" then "Apply" to save' }
770
- }
771
- return { primary: '👆 Press SPACEBAR when you hear each word', secondary: 'Tap for short words, hold for longer words' }
772
- }
773
- if (stats.synced === 0) {
774
- return { primary: 'Click "Start Sync" to begin timing words', secondary: 'Audio will play and you\'ll tap spacebar for each word' }
775
- }
776
- if (stats.remaining > 0) {
777
- return { primary: `${stats.remaining} words remaining to sync`, secondary: 'Click "Start Sync" to continue, or "Unsync from Cursor" to re-sync from a point' }
778
- }
779
- return { primary: '✅ All words synced!', secondary: 'Click "Apply" to save changes, or make adjustments first' }
780
- }, [isManualSyncing, isSpacebarPressed, stats.synced, stats.remaining])
781
-
782
- const instruction = getInstructionText()
783
-
784
- return (
785
- <Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', gap: 1 }}>
786
- {/* Stats bar - fixed height */}
787
- <Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', height: 24 }}>
788
- <Typography variant="body2" color="text.secondary">
789
- {stats.synced} / {stats.total} words synced
790
- {stats.remaining > 0 && ` (${stats.remaining} remaining)`}
791
- </Typography>
792
- </Box>
793
-
794
- {/* Instruction banner - fixed height to prevent layout shifts */}
795
- <Box
796
- sx={{
797
- height: 56,
798
- flexShrink: 0
799
- }}
800
- >
801
- <Paper
802
- sx={{
803
- p: 1.5,
804
- height: '100%',
805
- bgcolor: isManualSyncing
806
- ? 'success.main'
807
- : (isDarkMode ? 'grey.800' : 'grey.100'),
808
- color: isManualSyncing ? 'common.white' : 'text.primary',
809
- display: 'flex',
810
- flexDirection: 'column',
811
- justifyContent: 'center',
812
- overflow: 'hidden',
813
- boxSizing: 'border-box'
814
- }}
815
- >
816
- <Typography
817
- variant="body2"
818
- sx={{
819
- fontWeight: 500,
820
- lineHeight: 1.3,
821
- color: isManualSyncing ? 'common.white' : 'text.primary'
822
- }}
823
- >
824
- {instruction.primary}
825
- </Typography>
826
- <Typography
827
- variant="caption"
828
- sx={{
829
- opacity: 0.9,
830
- display: 'block',
831
- lineHeight: 1.3,
832
- color: isManualSyncing ? 'common.white' : 'text.secondary'
833
- }}
834
- >
835
- {instruction.secondary}
836
- </Typography>
837
- </Paper>
838
- </Box>
839
-
840
- {/* Controls section */}
841
- <Box sx={{ minHeight: 88, flexShrink: 0 }}>
842
- <SyncControls
843
- isManualSyncing={isManualSyncing}
844
- isPaused={isPaused}
845
- onStartSync={handleStartSync}
846
- onPauseSync={handlePauseSync}
847
- onResumeSync={handleResumeSync}
848
- onClearSync={handleClearSync}
849
- onEditLyrics={handleEditLyrics}
850
- onPlay={handlePlayAudio}
851
- onStop={handleStopAudio}
852
- isPlaying={isPlaying}
853
- hasSelectedWords={selectedWordIds.size > 0}
854
- selectedWordCount={selectedWordIds.size}
855
- onUnsyncFromCursor={handleUnsyncFromCursor}
856
- onEditSelectedWord={handleEditSelectedWord}
857
- onDeleteSelected={handleDeleteSelected}
858
- canUnsyncFromCursor={canUnsyncFromCursor}
859
- isMobile={isMobile}
860
- onTapStart={handleTapStart}
861
- onTapEnd={handleTapEnd}
862
- isTapping={isSpacebarPressed}
863
- />
864
- </Box>
865
-
866
- {/* Upcoming words bar - fixed height, immediately above timeline */}
867
- <Box sx={{ height: 44, flexShrink: 0 }}>
868
- {isManualSyncing && (
869
- <UpcomingWordsBar
870
- words={allWords}
871
- syncWordIndex={syncWordIndex}
872
- isManualSyncing={isManualSyncing}
873
- />
874
- )}
875
- </Box>
876
-
877
- {/* Timeline canvas */}
878
- <Box sx={{ flexGrow: 1, minHeight: 200 }}>
879
- <TimelineCanvas
880
- words={allWords}
881
- segments={workingSegments}
882
- visibleStartTime={visibleStartTime}
883
- visibleEndTime={visibleEndTime}
884
- currentTime={currentTime}
885
- selectedWordIds={selectedWordIds}
886
- onWordClick={handleWordClick}
887
- onBackgroundClick={handleBackgroundClick}
888
- onTimeBarClick={handleTimeBarClick}
889
- onSelectionComplete={handleSelectionComplete}
890
- onWordTimingChange={handleWordTimingChange}
891
- onWordsMove={handleWordsMove}
892
- syncWordIndex={syncWordIndex}
893
- isManualSyncing={isManualSyncing}
894
- onScrollChange={handleScrollChange}
895
- audioDuration={audioDuration}
896
- zoomSeconds={zoomSeconds}
897
- height={200}
898
- isDarkMode={isDarkMode}
899
- />
900
- </Box>
901
-
902
- {/* Zoom slider */}
903
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, px: 2 }}>
904
- <ZoomInIcon color="action" fontSize="small" />
905
- <Slider
906
- value={sliderValue}
907
- onChange={handleZoomChange}
908
- min={0}
909
- max={ZOOM_STEPS}
910
- step={1}
911
- sx={{ flexGrow: 1 }}
912
- disabled={isManualSyncing && !isPaused}
913
- />
914
- <ZoomOutIcon color="action" fontSize="small" />
915
- <Typography variant="caption" color="text.secondary" sx={{ minWidth: 60 }}>
916
- {zoomSeconds.toFixed(1)}s view
917
- </Typography>
918
- </Box>
919
-
920
- {/* Action buttons */}
921
- <Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, pt: 2, borderTop: 1, borderColor: 'divider' }}>
922
- <Button onClick={onCancel} color="inherit">
923
- Cancel
924
- </Button>
925
- <Button
926
- onClick={handleSave}
927
- variant="contained"
928
- color="primary"
929
- disabled={isManualSyncing && !isPaused}
930
- >
931
- Apply
932
- </Button>
933
- </Box>
934
-
935
- {/* Edit Lyrics Modal */}
936
- <Dialog
937
- open={showEditLyricsModal}
938
- onClose={() => setShowEditLyricsModal(false)}
939
- maxWidth="md"
940
- fullWidth
941
- >
942
- <DialogTitle>Edit Lyrics</DialogTitle>
943
- <DialogContent>
944
- <Alert severity="warning" sx={{ mb: 2 }}>
945
- Editing lyrics will reset all timing data. You will need to re-sync the entire song.
946
- </Alert>
947
- <TextField
948
- multiline
949
- rows={15}
950
- fullWidth
951
- value={editLyricsText}
952
- onChange={(e) => setEditLyricsText(e.target.value)}
953
- placeholder="Enter lyrics, one line per segment..."
954
- />
955
- </DialogContent>
956
- <DialogActions>
957
- <Button onClick={() => setShowEditLyricsModal(false)}>Cancel</Button>
958
- <Button onClick={handleSaveEditedLyrics} variant="contained" color="warning">
959
- Save & Reset Timing
960
- </Button>
961
- </DialogActions>
962
- </Dialog>
963
-
964
- {/* Edit Word Modal */}
965
- <Dialog
966
- open={showEditWordModal}
967
- onClose={() => setShowEditWordModal(false)}
968
- maxWidth="xs"
969
- fullWidth
970
- >
971
- <DialogTitle>Edit Word</DialogTitle>
972
- <DialogContent>
973
- <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
974
- Edit the word text. Enter multiple words separated by spaces to split.
975
- </Typography>
976
- <TextField
977
- fullWidth
978
- value={editWordText}
979
- onChange={(e) => setEditWordText(e.target.value)}
980
- autoFocus
981
- onKeyDown={(e) => {
982
- if (e.key === 'Enter') {
983
- handleSaveEditedWord()
984
- }
985
- }}
986
- />
987
- </DialogContent>
988
- <DialogActions>
989
- <Button onClick={() => setShowEditWordModal(false)}>Cancel</Button>
990
- <Button onClick={handleSaveEditedWord} variant="contained">
991
- Save
992
- </Button>
993
- </DialogActions>
994
- </Dialog>
995
- </Box>
996
- )
997
- })
998
-
999
- export default LyricsSynchronizer