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,721 +0,0 @@
1
- import { useRef, useEffect, useCallback, useState, memo } from 'react'
2
- import { Box, IconButton, Tooltip } from '@mui/material'
3
- import ArrowBackIcon from '@mui/icons-material/ArrowBack'
4
- import ArrowForwardIcon from '@mui/icons-material/ArrowForward'
5
- import { Word, LyricsSegment } from '../../types'
6
-
7
- interface TimelineCanvasProps {
8
- words: Word[]
9
- segments: LyricsSegment[]
10
- visibleStartTime: number
11
- visibleEndTime: number
12
- currentTime: number
13
- selectedWordIds: Set<string>
14
- onWordClick: (wordId: string, event: React.MouseEvent) => void
15
- onBackgroundClick: () => void
16
- onTimeBarClick: (time: number) => void
17
- onSelectionComplete: (wordIds: string[]) => void
18
- onWordTimingChange: (wordId: string, newStartTime: number, newEndTime: number) => void
19
- onWordsMove: (updates: Array<{ wordId: string; newStartTime: number; newEndTime: number }>) => void
20
- syncWordIndex: number
21
- isManualSyncing: boolean
22
- onScrollChange: (newStartTime: number) => void
23
- audioDuration: number
24
- zoomSeconds: number
25
- height?: number
26
- isDarkMode?: boolean
27
- }
28
-
29
- // Constants for rendering
30
- const TIME_BAR_HEIGHT = 28
31
- const WORD_BLOCK_HEIGHT = 24
32
- const WORD_LEVEL_SPACING = 50
33
- const CANVAS_PADDING = 8
34
- const TEXT_ABOVE_BLOCK = 14
35
- const RESIZE_HANDLE_SIZE = 8
36
- const RESIZE_HANDLE_HITAREA = 12
37
-
38
- // Theme-aware colors
39
- const getThemeColors = (isDarkMode: boolean) => ({
40
- playhead: '#f97316', // orange-500 - same for both themes
41
- wordBlock: isDarkMode ? '#dc2626' : '#ef4444', // red-600 dark, red-500 light
42
- wordBlockSelected: isDarkMode ? '#b91c1c' : '#dc2626', // red-700 dark, red-600 light
43
- wordBlockCurrent: isDarkMode ? '#ef4444' : '#f87171', // red-500 dark, red-400 light
44
- wordTextCurrent: isDarkMode ? '#fca5a5' : '#991b1b', // red-300 dark, red-800 light
45
- wordText: isDarkMode ? '#f8fafc' : '#1e293b', // slate-50 dark, slate-800 light
46
- upcomingWordBg: isDarkMode ? '#2a2a2a' : '#e5e7eb', // slate-700 dark, gray-200 light
47
- upcomingWordText: isDarkMode ? '#e5e5e5' : '#374151', // slate-50 dark, gray-700 light
48
- timeBarBg: isDarkMode ? '#1a1a1a' : '#f3f4f6', // slate-800 dark, gray-100 light
49
- timeBarText: isDarkMode ? '#888888' : '#6b7280', // slate-400 dark, gray-500 light
50
- timelineBg: isDarkMode ? '#0f0f0f' : '#ffffff', // slate-900 dark, white light
51
- gridLine: isDarkMode ? '#64748b' : '#94a3b8', // slate-500 dark, slate-400 light
52
- borderLine: isDarkMode ? '#2a2a2a' : '#cbd5e1', // slate-700 dark, slate-300 light
53
- handleStroke: isDarkMode ? '#0f0f0f' : '#ffffff', // stroke around handles
54
- playheadShadow: isDarkMode ? 'rgba(0,0,0,0.6)' : 'rgba(0,0,0,0.3)', // shadow behind playhead line
55
- })
56
-
57
- // Drag modes
58
- type DragMode = 'none' | 'selection' | 'resize' | 'move'
59
-
60
- // Build a map of word ID to segment index
61
- function buildWordToSegmentMap(segments: LyricsSegment[]): Map<string, number> {
62
- const map = new Map<string, number>()
63
- segments.forEach((segment, idx) => {
64
- segment.words.forEach(word => {
65
- map.set(word.id, idx)
66
- })
67
- })
68
- return map
69
- }
70
-
71
- // Calculate which vertical level a word should be on
72
- function calculateWordLevels(words: Word[], segments: LyricsSegment[]): Map<string, number> {
73
- const levels = new Map<string, number>()
74
- const wordToSegment = buildWordToSegmentMap(segments)
75
-
76
- const segmentsWithTiming = segments
77
- .map((segment, idx) => {
78
- const timedWords = segment.words.filter(w => w.start_time !== null)
79
- const minStart = timedWords.length > 0
80
- ? Math.min(...timedWords.map(w => w.start_time!))
81
- : Infinity
82
- return { idx, minStart }
83
- })
84
- .filter(s => s.minStart !== Infinity)
85
- .sort((a, b) => a.minStart - b.minStart)
86
-
87
- const segmentLevels = new Map<number, number>()
88
- segmentsWithTiming.forEach(({ idx }, orderIndex) => {
89
- segmentLevels.set(idx, orderIndex % 2)
90
- })
91
-
92
- for (const word of words) {
93
- const segmentIdx = wordToSegment.get(word.id)
94
- if (segmentIdx !== undefined && segmentLevels.has(segmentIdx)) {
95
- levels.set(word.id, segmentLevels.get(segmentIdx)!)
96
- } else {
97
- levels.set(word.id, 0)
98
- }
99
- }
100
-
101
- return levels
102
- }
103
-
104
- function formatTime(seconds: number): string {
105
- const mins = Math.floor(seconds / 60)
106
- const secs = Math.floor(seconds % 60)
107
- return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
108
- }
109
-
110
- const TimelineCanvas = memo(function TimelineCanvas({
111
- words,
112
- segments,
113
- visibleStartTime,
114
- visibleEndTime,
115
- currentTime,
116
- selectedWordIds,
117
- onWordClick,
118
- onBackgroundClick,
119
- onTimeBarClick,
120
- onSelectionComplete,
121
- onWordTimingChange,
122
- onWordsMove,
123
- syncWordIndex,
124
- isManualSyncing,
125
- onScrollChange,
126
- audioDuration,
127
- zoomSeconds,
128
- height = 200,
129
- isDarkMode = true
130
- }: TimelineCanvasProps) {
131
- // Get theme colors
132
- const colors = getThemeColors(isDarkMode)
133
- const canvasRef = useRef<HTMLCanvasElement>(null)
134
- const containerRef = useRef<HTMLDivElement>(null)
135
- const [canvasWidth, setCanvasWidth] = useState(800)
136
- const animationFrameRef = useRef<number>()
137
- const wordLevelsRef = useRef<Map<string, number>>(new Map())
138
-
139
- // Drag state
140
- const [dragMode, setDragMode] = useState<DragMode>('none')
141
- const dragStartRef = useRef<{ x: number; y: number; time: number } | null>(null)
142
- const dragWordIdRef = useRef<string | null>(null)
143
- const dragOriginalTimesRef = useRef<Map<string, { start: number; end: number }>>(new Map())
144
-
145
- // Selection rectangle
146
- const [selectionRect, setSelectionRect] = useState<{
147
- startX: number; startY: number; endX: number; endY: number
148
- } | null>(null)
149
-
150
- // Hover state for showing resize handle
151
- const [hoveredWordId, setHoveredWordId] = useState<string | null>(null)
152
- const [cursorStyle, setCursorStyle] = useState<string>('default')
153
-
154
- // Update canvas width on resize
155
- useEffect(() => {
156
- const updateWidth = () => {
157
- if (containerRef.current) {
158
- setCanvasWidth(containerRef.current.clientWidth)
159
- }
160
- }
161
-
162
- updateWidth()
163
- const resizeObserver = new ResizeObserver(updateWidth)
164
- if (containerRef.current) {
165
- resizeObserver.observe(containerRef.current)
166
- }
167
-
168
- return () => resizeObserver.disconnect()
169
- }, [])
170
-
171
- // Calculate word levels when words or segments change
172
- useEffect(() => {
173
- wordLevelsRef.current = calculateWordLevels(words, segments)
174
- }, [words, segments])
175
-
176
- // Convert time to x position
177
- const timeToX = useCallback((time: number): number => {
178
- const duration = visibleEndTime - visibleStartTime
179
- if (duration <= 0) return 0
180
- return CANVAS_PADDING + ((time - visibleStartTime) / duration) * (canvasWidth - CANVAS_PADDING * 2)
181
- }, [visibleStartTime, visibleEndTime, canvasWidth])
182
-
183
- // Convert x position to time
184
- const xToTime = useCallback((x: number): number => {
185
- const duration = visibleEndTime - visibleStartTime
186
- return visibleStartTime + ((x - CANVAS_PADDING) / (canvasWidth - CANVAS_PADDING * 2)) * duration
187
- }, [visibleStartTime, visibleEndTime, canvasWidth])
188
-
189
- // Get word bounds
190
- const getWordBounds = useCallback((word: Word) => {
191
- if (word.start_time === null || word.end_time === null) return null
192
-
193
- const level = wordLevelsRef.current.get(word.id) || 0
194
- const startX = timeToX(word.start_time)
195
- const endX = timeToX(word.end_time)
196
- const blockWidth = Math.max(endX - startX, 4)
197
- const y = TIME_BAR_HEIGHT + CANVAS_PADDING + TEXT_ABOVE_BLOCK + level * WORD_LEVEL_SPACING
198
-
199
- return { startX, endX, blockWidth, y, level }
200
- }, [timeToX])
201
-
202
- // Check if position is near resize handle
203
- const isNearResizeHandlePos = useCallback((word: Word, x: number, y: number): boolean => {
204
- const bounds = getWordBounds(word)
205
- if (!bounds) return false
206
-
207
- const handleX = bounds.startX + bounds.blockWidth - RESIZE_HANDLE_SIZE / 2
208
- const handleY = bounds.y + WORD_BLOCK_HEIGHT / 2
209
-
210
- return Math.abs(x - handleX) < RESIZE_HANDLE_HITAREA / 2 &&
211
- Math.abs(y - handleY) < RESIZE_HANDLE_HITAREA / 2
212
- }, [getWordBounds])
213
-
214
- // Find word at position
215
- const findWordAtPosition = useCallback((x: number, y: number): Word | null => {
216
- for (const word of words) {
217
- const bounds = getWordBounds(word)
218
- if (!bounds) continue
219
-
220
- if (x >= bounds.startX && x <= bounds.startX + bounds.blockWidth &&
221
- y >= bounds.y && y <= bounds.y + WORD_BLOCK_HEIGHT) {
222
- return word
223
- }
224
- }
225
- return null
226
- }, [words, getWordBounds])
227
-
228
- // Find words in selection rectangle
229
- const findWordsInRect = useCallback((rect: { startX: number; startY: number; endX: number; endY: number }): string[] => {
230
- const rectLeft = Math.min(rect.startX, rect.endX)
231
- const rectRight = Math.max(rect.startX, rect.endX)
232
- const rectTop = Math.min(rect.startY, rect.endY)
233
- const rectBottom = Math.max(rect.startY, rect.endY)
234
-
235
- const selectedIds: string[] = []
236
-
237
- for (const word of words) {
238
- const bounds = getWordBounds(word)
239
- if (!bounds) continue
240
-
241
- if (bounds.startX + bounds.blockWidth >= rectLeft && bounds.startX <= rectRight &&
242
- bounds.y + WORD_BLOCK_HEIGHT >= rectTop && bounds.y <= rectBottom) {
243
- selectedIds.push(word.id)
244
- }
245
- }
246
-
247
- return selectedIds
248
- }, [words, getWordBounds])
249
-
250
- // Draw the timeline
251
- const draw = useCallback(() => {
252
- const canvas = canvasRef.current
253
- if (!canvas) return
254
-
255
- const ctx = canvas.getContext('2d')
256
- if (!ctx) return
257
-
258
- const dpr = window.devicePixelRatio || 1
259
- canvas.width = canvasWidth * dpr
260
- canvas.height = height * dpr
261
- ctx.scale(dpr, dpr)
262
-
263
- // Clear canvas
264
- ctx.fillStyle = colors.timelineBg
265
- ctx.fillRect(0, 0, canvasWidth, height)
266
-
267
- // Draw time bar background
268
- ctx.fillStyle = colors.timeBarBg
269
- ctx.fillRect(0, 0, canvasWidth, TIME_BAR_HEIGHT)
270
-
271
- // Draw time markers
272
- const duration = visibleEndTime - visibleStartTime
273
- const secondsPerTick = duration > 15 ? 2 : duration > 8 ? 1 : 0.5
274
- const startSecond = Math.ceil(visibleStartTime / secondsPerTick) * secondsPerTick
275
-
276
- ctx.fillStyle = colors.timeBarText
277
- ctx.font = '11px system-ui, -apple-system, sans-serif'
278
- ctx.textAlign = 'center'
279
-
280
- for (let t = startSecond; t <= visibleEndTime; t += secondsPerTick) {
281
- const x = timeToX(t)
282
-
283
- ctx.beginPath()
284
- ctx.strokeStyle = colors.gridLine
285
- ctx.lineWidth = 1
286
- ctx.moveTo(x, TIME_BAR_HEIGHT - 6)
287
- ctx.lineTo(x, TIME_BAR_HEIGHT)
288
- ctx.stroke()
289
-
290
- if (t % 1 === 0) {
291
- ctx.fillText(formatTime(t), x, TIME_BAR_HEIGHT - 10)
292
- }
293
- }
294
-
295
- ctx.beginPath()
296
- ctx.strokeStyle = colors.borderLine
297
- ctx.lineWidth = 1
298
- ctx.moveTo(0, TIME_BAR_HEIGHT)
299
- ctx.lineTo(canvasWidth, TIME_BAR_HEIGHT)
300
- ctx.stroke()
301
-
302
- const wordToSegment = buildWordToSegmentMap(segments)
303
- const syncedWords = words.filter(w => w.start_time !== null && w.end_time !== null)
304
-
305
- const currentWordId = syncedWords.find(w =>
306
- currentTime >= w.start_time! && currentTime <= w.end_time!
307
- )?.id || null
308
-
309
- // First pass: draw all blocks
310
- for (const word of syncedWords) {
311
- const bounds = getWordBounds(word)
312
- if (!bounds) continue
313
-
314
- const isSelected = selectedWordIds.has(word.id)
315
- const isCurrent = word.id === currentWordId
316
- const isHovered = word.id === hoveredWordId
317
-
318
- // Draw word block background
319
- if (isSelected) {
320
- ctx.fillStyle = colors.wordBlockSelected
321
- } else if (isCurrent) {
322
- ctx.fillStyle = colors.wordBlockCurrent
323
- } else {
324
- ctx.fillStyle = colors.wordBlock
325
- }
326
- ctx.fillRect(bounds.startX, bounds.y, bounds.blockWidth, WORD_BLOCK_HEIGHT)
327
-
328
- // Draw selection border
329
- if (isSelected) {
330
- ctx.strokeStyle = colors.playhead
331
- ctx.lineWidth = 2
332
- ctx.strokeRect(bounds.startX, bounds.y, bounds.blockWidth, WORD_BLOCK_HEIGHT)
333
-
334
- // Draw resize handle (orange dot on right edge) for selected words when hovered
335
- if (isHovered || selectedWordIds.size === 1) {
336
- const handleX = bounds.startX + bounds.blockWidth - RESIZE_HANDLE_SIZE / 2
337
- const handleY = bounds.y + WORD_BLOCK_HEIGHT / 2
338
-
339
- ctx.beginPath()
340
- ctx.fillStyle = colors.playhead
341
- ctx.arc(handleX, handleY, RESIZE_HANDLE_SIZE / 2, 0, Math.PI * 2)
342
- ctx.fill()
343
- ctx.strokeStyle = colors.handleStroke
344
- ctx.lineWidth = 1
345
- ctx.stroke()
346
- }
347
- }
348
- }
349
-
350
- // Second pass: draw text
351
- const wordsBySegment = new Map<number, Word[]>()
352
- for (const word of syncedWords) {
353
- const segIdx = wordToSegment.get(word.id)
354
- if (segIdx !== undefined) {
355
- if (!wordsBySegment.has(segIdx)) {
356
- wordsBySegment.set(segIdx, [])
357
- }
358
- wordsBySegment.get(segIdx)!.push(word)
359
- }
360
- }
361
-
362
- ctx.font = '11px system-ui, -apple-system, sans-serif'
363
- ctx.textAlign = 'left'
364
-
365
- for (const [, segmentWords] of wordsBySegment) {
366
- const sortedWords = [...segmentWords].sort((a, b) =>
367
- (a.start_time || 0) - (b.start_time || 0)
368
- )
369
-
370
- if (sortedWords.length === 0) continue
371
-
372
- const level = wordLevelsRef.current.get(sortedWords[0].id) || 0
373
- const textY = TIME_BAR_HEIGHT + CANVAS_PADDING + TEXT_ABOVE_BLOCK + level * WORD_LEVEL_SPACING - 3
374
-
375
- let rightmostTextEnd = -Infinity
376
-
377
- for (const word of sortedWords) {
378
- const blockStartX = timeToX(word.start_time!)
379
- const textWidth = ctx.measureText(word.text).width
380
- const textStartX = Math.max(blockStartX, rightmostTextEnd + 3)
381
-
382
- if (textStartX < canvasWidth - 10) {
383
- const isCurrent = word.id === currentWordId
384
- ctx.fillStyle = isCurrent ? colors.wordTextCurrent : colors.wordText
385
- ctx.fillText(word.text, textStartX, textY)
386
- rightmostTextEnd = textStartX + textWidth
387
- }
388
- }
389
- }
390
-
391
- // Draw upcoming words during sync
392
- if (isManualSyncing && syncWordIndex >= 0) {
393
- const upcomingWords = words.slice(syncWordIndex).filter(w => w.start_time === null)
394
- const playheadX = timeToX(currentTime)
395
- let offsetX = playheadX + 10
396
-
397
- ctx.font = '11px system-ui, -apple-system, sans-serif'
398
-
399
- for (let i = 0; i < Math.min(upcomingWords.length, 12); i++) {
400
- const word = upcomingWords[i]
401
- const textWidth = ctx.measureText(word.text).width + 10
402
-
403
- ctx.fillStyle = colors.upcomingWordBg
404
- ctx.fillRect(offsetX, TIME_BAR_HEIGHT + CANVAS_PADDING + WORD_LEVEL_SPACING + 60, textWidth, 20)
405
-
406
- ctx.fillStyle = colors.upcomingWordText
407
- ctx.textAlign = 'left'
408
- ctx.fillText(word.text, offsetX + 5, TIME_BAR_HEIGHT + CANVAS_PADDING + WORD_LEVEL_SPACING + 74)
409
-
410
- offsetX += textWidth + 3
411
- if (offsetX > canvasWidth - 20) break
412
- }
413
- }
414
-
415
- // Draw playhead
416
- if (currentTime >= visibleStartTime && currentTime <= visibleEndTime) {
417
- const playheadX = timeToX(currentTime)
418
-
419
- ctx.beginPath()
420
- ctx.fillStyle = colors.playhead
421
- ctx.strokeStyle = colors.handleStroke
422
- ctx.lineWidth = 1
423
- ctx.moveTo(playheadX - 6, 2)
424
- ctx.lineTo(playheadX + 6, 2)
425
- ctx.lineTo(playheadX, TIME_BAR_HEIGHT - 4)
426
- ctx.closePath()
427
- ctx.fill()
428
- ctx.stroke()
429
-
430
- ctx.beginPath()
431
- ctx.strokeStyle = colors.playhead
432
- ctx.lineWidth = 2
433
- ctx.moveTo(playheadX, TIME_BAR_HEIGHT)
434
- ctx.lineTo(playheadX, height)
435
- ctx.stroke()
436
-
437
- ctx.beginPath()
438
- ctx.strokeStyle = colors.playheadShadow
439
- ctx.lineWidth = 1
440
- ctx.moveTo(playheadX + 1, TIME_BAR_HEIGHT)
441
- ctx.lineTo(playheadX + 1, height)
442
- ctx.stroke()
443
- }
444
-
445
- // Draw selection rectangle
446
- if (selectionRect) {
447
- ctx.fillStyle = 'rgba(25, 118, 210, 0.2)'
448
- ctx.strokeStyle = 'rgba(25, 118, 210, 0.8)'
449
- ctx.lineWidth = 1
450
-
451
- const rectX = Math.min(selectionRect.startX, selectionRect.endX)
452
- const rectY = Math.min(selectionRect.startY, selectionRect.endY)
453
- const rectW = Math.abs(selectionRect.endX - selectionRect.startX)
454
- const rectH = Math.abs(selectionRect.endY - selectionRect.startY)
455
-
456
- ctx.fillRect(rectX, rectY, rectW, rectH)
457
- ctx.strokeRect(rectX, rectY, rectW, rectH)
458
- }
459
- }, [
460
- canvasWidth, height, visibleStartTime, visibleEndTime, currentTime,
461
- words, segments, selectedWordIds, selectionRect, hoveredWordId,
462
- syncWordIndex, isManualSyncing, timeToX, getWordBounds, colors
463
- ])
464
-
465
- // Animation frame
466
- useEffect(() => {
467
- const animate = () => {
468
- draw()
469
- animationFrameRef.current = requestAnimationFrame(animate)
470
- }
471
- animate()
472
-
473
- return () => {
474
- if (animationFrameRef.current) {
475
- cancelAnimationFrame(animationFrameRef.current)
476
- }
477
- }
478
- }, [draw])
479
-
480
- // Mouse handlers
481
- const handleMouseDown = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
482
- const rect = canvasRef.current?.getBoundingClientRect()
483
- if (!rect) return
484
-
485
- const x = e.clientX - rect.left
486
- const y = e.clientY - rect.top
487
- const time = xToTime(x)
488
-
489
- // Time bar click
490
- if (y < TIME_BAR_HEIGHT) {
491
- onTimeBarClick(Math.max(0, time))
492
- return
493
- }
494
-
495
- const clickedWord = findWordAtPosition(x, y)
496
-
497
- if (clickedWord && selectedWordIds.has(clickedWord.id)) {
498
- // Check if clicking on resize handle
499
- if (isNearResizeHandlePos(clickedWord, x, y)) {
500
- // Start resize
501
- setDragMode('resize')
502
- dragStartRef.current = { x, y, time }
503
- dragWordIdRef.current = clickedWord.id
504
- dragOriginalTimesRef.current = new Map([[clickedWord.id, {
505
- start: clickedWord.start_time!,
506
- end: clickedWord.end_time!
507
- }]])
508
- return
509
- }
510
-
511
- // Start move (for all selected words)
512
- setDragMode('move')
513
- dragStartRef.current = { x, y, time }
514
- dragWordIdRef.current = clickedWord.id
515
-
516
- // Store original times for all selected words
517
- const originalTimes = new Map<string, { start: number; end: number }>()
518
- for (const wordId of selectedWordIds) {
519
- const word = words.find(w => w.id === wordId)
520
- if (word && word.start_time !== null && word.end_time !== null) {
521
- originalTimes.set(wordId, { start: word.start_time, end: word.end_time })
522
- }
523
- }
524
- dragOriginalTimesRef.current = originalTimes
525
- return
526
- }
527
-
528
- if (clickedWord) {
529
- // Click on unselected word - select it
530
- onWordClick(clickedWord.id, e)
531
- return
532
- }
533
-
534
- // Background - start selection
535
- setDragMode('selection')
536
- dragStartRef.current = { x, y, time }
537
- setSelectionRect({ startX: x, startY: y, endX: x, endY: y })
538
- }, [xToTime, onTimeBarClick, findWordAtPosition, selectedWordIds, isNearResizeHandlePos, onWordClick, words])
539
-
540
- const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
541
- const rect = canvasRef.current?.getBoundingClientRect()
542
- if (!rect) return
543
-
544
- const x = e.clientX - rect.left
545
- const y = e.clientY - rect.top
546
- const time = xToTime(x)
547
-
548
- // Update hover state and cursor
549
- if (dragMode === 'none') {
550
- const hoveredWord = findWordAtPosition(x, y)
551
- setHoveredWordId(hoveredWord?.id || null)
552
-
553
- if (hoveredWord && selectedWordIds.has(hoveredWord.id)) {
554
- const nearHandle = isNearResizeHandlePos(hoveredWord, x, y)
555
- setCursorStyle(nearHandle ? 'ew-resize' : 'grab')
556
- } else if (hoveredWord) {
557
- setCursorStyle('pointer')
558
- } else if (y < TIME_BAR_HEIGHT) {
559
- setCursorStyle('pointer')
560
- } else {
561
- setCursorStyle('default')
562
- }
563
- }
564
-
565
- if (!dragStartRef.current) return
566
-
567
- if (dragMode === 'selection') {
568
- setSelectionRect({
569
- startX: dragStartRef.current.x,
570
- startY: dragStartRef.current.y,
571
- endX: x,
572
- endY: y
573
- })
574
- } else if (dragMode === 'resize' && dragWordIdRef.current) {
575
- // Resize the word
576
- const originalTimes = dragOriginalTimesRef.current.get(dragWordIdRef.current)
577
- if (originalTimes) {
578
- const deltaTime = time - dragStartRef.current.time
579
- const newEndTime = Math.max(originalTimes.start + 0.05, originalTimes.end + deltaTime)
580
- onWordTimingChange(dragWordIdRef.current, originalTimes.start, newEndTime)
581
- }
582
- setCursorStyle('ew-resize')
583
- } else if (dragMode === 'move') {
584
- // Move all selected words
585
- const deltaTime = time - dragStartRef.current.time
586
- const updates: Array<{ wordId: string; newStartTime: number; newEndTime: number }> = []
587
-
588
- for (const [wordId, originalTimes] of dragOriginalTimesRef.current) {
589
- // Ensure end time is always after start time (at least 0.05s duration)
590
- const newStartTime = Math.max(0, originalTimes.start + deltaTime)
591
- const newEndTime = Math.max(newStartTime + 0.05, originalTimes.end + deltaTime)
592
- updates.push({
593
- wordId,
594
- newStartTime,
595
- newEndTime
596
- })
597
- }
598
-
599
- if (updates.length > 0) {
600
- onWordsMove(updates)
601
- }
602
- setCursorStyle('grabbing')
603
- }
604
- }, [dragMode, xToTime, findWordAtPosition, selectedWordIds, isNearResizeHandlePos, onWordTimingChange, onWordsMove])
605
-
606
- const handleMouseUp = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
607
- const rect = canvasRef.current?.getBoundingClientRect()
608
-
609
- if (dragMode === 'selection' && dragStartRef.current && rect) {
610
- const endX = e.clientX - rect.left
611
- const endY = e.clientY - rect.top
612
-
613
- const dragDistance = Math.sqrt(
614
- Math.pow(endX - dragStartRef.current.x, 2) +
615
- Math.pow(endY - dragStartRef.current.y, 2)
616
- )
617
-
618
- if (dragDistance < 5) {
619
- onBackgroundClick()
620
- } else {
621
- const finalRect = {
622
- startX: dragStartRef.current.x,
623
- startY: dragStartRef.current.y,
624
- endX,
625
- endY
626
- }
627
- const selectedIds = findWordsInRect(finalRect)
628
- if (selectedIds.length > 0) {
629
- onSelectionComplete(selectedIds)
630
- }
631
- }
632
- }
633
-
634
- // Reset drag state
635
- setDragMode('none')
636
- dragStartRef.current = null
637
- dragWordIdRef.current = null
638
- dragOriginalTimesRef.current = new Map()
639
- setSelectionRect(null)
640
- setCursorStyle('default')
641
- }, [dragMode, onBackgroundClick, findWordsInRect, onSelectionComplete])
642
-
643
- // Wheel handler
644
- const handleWheel = useCallback((e: React.WheelEvent<HTMLCanvasElement>) => {
645
- const delta = e.deltaX !== 0 ? e.deltaX : e.deltaY
646
- const scrollAmount = (delta / 100) * (zoomSeconds / 4)
647
- let newStart = Math.max(0, Math.min(audioDuration - zoomSeconds, visibleStartTime + scrollAmount))
648
-
649
- if (newStart !== visibleStartTime) {
650
- onScrollChange(newStart)
651
- }
652
- }, [visibleStartTime, zoomSeconds, audioDuration, onScrollChange])
653
-
654
- const handleScrollLeft = useCallback(() => {
655
- const newStart = Math.max(0, visibleStartTime - zoomSeconds * 0.25)
656
- onScrollChange(newStart)
657
- }, [visibleStartTime, zoomSeconds, onScrollChange])
658
-
659
- const handleScrollRight = useCallback(() => {
660
- const newStart = Math.min(audioDuration - zoomSeconds, visibleStartTime + zoomSeconds * 0.25)
661
- onScrollChange(Math.max(0, newStart))
662
- }, [visibleStartTime, zoomSeconds, audioDuration, onScrollChange])
663
-
664
- return (
665
- <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
666
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
667
- <Tooltip title="Scroll Left">
668
- <span>
669
- <IconButton
670
- size="small"
671
- onClick={handleScrollLeft}
672
- disabled={visibleStartTime <= 0}
673
- >
674
- <ArrowBackIcon fontSize="small" />
675
- </IconButton>
676
- </span>
677
- </Tooltip>
678
-
679
- <Box
680
- ref={containerRef}
681
- sx={{
682
- flexGrow: 1,
683
- height,
684
- cursor: cursorStyle,
685
- borderRadius: 1,
686
- overflow: 'hidden'
687
- }}
688
- >
689
- <canvas
690
- ref={canvasRef}
691
- style={{
692
- width: '100%',
693
- height: '100%',
694
- display: 'block',
695
- cursor: cursorStyle
696
- }}
697
- onMouseDown={handleMouseDown}
698
- onMouseMove={handleMouseMove}
699
- onMouseUp={handleMouseUp}
700
- onMouseLeave={handleMouseUp}
701
- onWheel={handleWheel}
702
- />
703
- </Box>
704
-
705
- <Tooltip title="Scroll Right">
706
- <span>
707
- <IconButton
708
- size="small"
709
- onClick={handleScrollRight}
710
- disabled={visibleStartTime >= audioDuration - zoomSeconds}
711
- >
712
- <ArrowForwardIcon fontSize="small" />
713
- </IconButton>
714
- </span>
715
- </Tooltip>
716
- </Box>
717
- </Box>
718
- )
719
- })
720
-
721
- export default TimelineCanvas