karaoke-gen 0.105.4__py3-none-any.whl → 0.107.0__py3-none-any.whl

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