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,1526 +0,0 @@
1
- import { useState, useEffect, useCallback, useMemo, memo } from 'react'
2
- import {
3
- AnchorSequence,
4
- CorrectionData,
5
- GapSequence,
6
- HighlightInfo,
7
- InteractionMode,
8
- LyricsSegment,
9
- ReferenceSource,
10
- WordCorrection,
11
- CorrectionAnnotation
12
- } from '../types'
13
- import { Box, Button, Grid, Typography, useMediaQuery, useTheme } from '@mui/material'
14
- import { ApiClient } from '../api'
15
- import ReferenceView from './ReferenceView'
16
- import TranscriptionView from './TranscriptionView'
17
- import { WordClickInfo, FlashType } from './shared/types'
18
- import EditModal from './EditModal'
19
- import ReviewChangesModal from './ReviewChangesModal'
20
- import ReplaceAllLyricsModal from './ReplaceAllLyricsModal'
21
- import CorrectionAnnotationModal from './CorrectionAnnotationModal'
22
- import CorrectionDetailCard from './CorrectionDetailCard'
23
- import {
24
- addSegmentBefore,
25
- splitSegment,
26
- deleteSegment,
27
- mergeSegment,
28
- findAndReplace,
29
- deleteWord
30
- } from './shared/utils/segmentOperations'
31
- import { loadSavedData, saveData, clearSavedData } from './shared/utils/localStorage'
32
- import { setupKeyboardHandlers, setModalHandler, getModalState } from './shared/utils/keyboardHandlers'
33
- import Header from './Header'
34
- import { getWordsFromIds } from './shared/utils/wordUtils'
35
- import AddLyricsModal from './AddLyricsModal'
36
- import { OndemandVideo } from '@mui/icons-material'
37
- import FindReplaceModal from './FindReplaceModal'
38
- import TimingOffsetModal from './TimingOffsetModal'
39
- import { applyOffsetToCorrectionData, applyOffsetToSegment } from './shared/utils/timingUtils'
40
-
41
- // Add type for window augmentation at the top of the file
42
- declare global {
43
- interface Window {
44
- toggleAudioPlayback?: () => void;
45
- seekAndPlayAudio?: (startTime: number) => void;
46
- getAudioDuration?: () => number;
47
- isAudioPlaying?: boolean;
48
- }
49
- }
50
-
51
- const debugLog = false;
52
- export interface LyricsAnalyzerProps {
53
- data: CorrectionData
54
- onFileLoad: () => void
55
- onShowMetadata: () => void
56
- apiClient: ApiClient | null
57
- isReadOnly: boolean
58
- audioHash: string
59
- }
60
-
61
- export type ModalContent = {
62
- type: 'anchor'
63
- data: AnchorSequence & {
64
- wordId: string
65
- word?: string
66
- anchor_sequences: AnchorSequence[]
67
- }
68
- } | {
69
- type: 'gap'
70
- data: GapSequence & {
71
- wordId: string
72
- word: string
73
- anchor_sequences: AnchorSequence[]
74
- }
75
- }
76
-
77
- // Define types for the memoized components
78
- interface MemoizedTranscriptionViewProps {
79
- data: CorrectionData
80
- mode: InteractionMode
81
- onElementClick: (content: ModalContent) => void
82
- onWordClick: (info: WordClickInfo) => void
83
- flashingType: FlashType
84
- flashingHandler: string | null
85
- highlightInfo: HighlightInfo | null
86
- onPlaySegment?: (time: number) => void
87
- currentTime: number
88
- anchors: AnchorSequence[]
89
- disableHighlighting: boolean
90
- onDataChange?: (updatedData: CorrectionData) => void
91
- // Review mode props
92
- reviewMode: boolean
93
- onRevertCorrection: (wordId: string) => void
94
- onEditCorrection: (wordId: string) => void
95
- onAcceptCorrection: (wordId: string) => void
96
- onShowCorrectionDetail: (wordId: string) => void
97
- }
98
-
99
- // Create a memoized TranscriptionView component
100
- const MemoizedTranscriptionView = memo(function MemoizedTranscriptionView({
101
- data,
102
- mode,
103
- onElementClick,
104
- onWordClick,
105
- flashingType,
106
- flashingHandler,
107
- highlightInfo,
108
- onPlaySegment,
109
- currentTime,
110
- anchors,
111
- disableHighlighting,
112
- onDataChange,
113
- reviewMode,
114
- onRevertCorrection,
115
- onEditCorrection,
116
- onAcceptCorrection,
117
- onShowCorrectionDetail
118
- }: MemoizedTranscriptionViewProps) {
119
- return (
120
- <TranscriptionView
121
- data={data}
122
- mode={mode}
123
- onElementClick={onElementClick}
124
- onWordClick={onWordClick}
125
- flashingType={flashingType}
126
- flashingHandler={flashingHandler}
127
- highlightInfo={highlightInfo}
128
- onPlaySegment={onPlaySegment}
129
- currentTime={disableHighlighting ? undefined : currentTime}
130
- anchors={anchors}
131
- onDataChange={onDataChange}
132
- reviewMode={reviewMode}
133
- onRevertCorrection={onRevertCorrection}
134
- onEditCorrection={onEditCorrection}
135
- onAcceptCorrection={onAcceptCorrection}
136
- onShowCorrectionDetail={onShowCorrectionDetail}
137
- />
138
- );
139
- });
140
-
141
- interface MemoizedReferenceViewProps {
142
- referenceSources: Record<string, ReferenceSource>
143
- anchors: AnchorSequence[]
144
- gaps: GapSequence[]
145
- mode: InteractionMode
146
- onElementClick: (content: ModalContent) => void
147
- onWordClick: (info: WordClickInfo) => void
148
- flashingType: FlashType
149
- highlightInfo: HighlightInfo | null
150
- currentSource: string
151
- onSourceChange: (source: string) => void
152
- corrected_segments: LyricsSegment[]
153
- corrections: WordCorrection[]
154
- onAddLyrics?: () => void
155
- }
156
-
157
- // Create a memoized ReferenceView component
158
- const MemoizedReferenceView = memo(function MemoizedReferenceView({
159
- referenceSources,
160
- anchors,
161
- gaps,
162
- mode,
163
- onElementClick,
164
- onWordClick,
165
- flashingType,
166
- highlightInfo,
167
- currentSource,
168
- onSourceChange,
169
- corrected_segments,
170
- corrections,
171
- onAddLyrics
172
- }: MemoizedReferenceViewProps) {
173
- return (
174
- <ReferenceView
175
- referenceSources={referenceSources}
176
- anchors={anchors}
177
- gaps={gaps}
178
- mode={mode}
179
- onElementClick={onElementClick}
180
- onWordClick={onWordClick}
181
- flashingType={flashingType}
182
- highlightInfo={highlightInfo}
183
- currentSource={currentSource}
184
- onSourceChange={onSourceChange}
185
- corrected_segments={corrected_segments}
186
- corrections={corrections}
187
- onAddLyrics={onAddLyrics}
188
- />
189
- );
190
- });
191
-
192
- interface MemoizedHeaderProps {
193
- isReadOnly: boolean
194
- onFileLoad: () => void
195
- data: CorrectionData
196
- onMetricClick: {
197
- anchor: () => void
198
- corrected: () => void
199
- uncorrected: () => void
200
- }
201
- effectiveMode: InteractionMode
202
- onModeChange: (mode: InteractionMode) => void
203
- apiClient: ApiClient | null
204
- audioHash: string
205
- onTimeUpdate: (time: number) => void
206
- onHandlerToggle: (handler: string, enabled: boolean) => void
207
- isUpdatingHandlers: boolean
208
- onHandlerClick?: (handler: string) => void
209
- onAddLyrics?: () => void
210
- onFindReplace?: () => void
211
- onEditAll?: () => void
212
- onTimingOffset: () => void
213
- timingOffsetMs: number
214
- onUndo: () => void
215
- onRedo: () => void
216
- canUndo: boolean
217
- canRedo: boolean
218
- onUnCorrectAll: () => void
219
- onResetCorrections: () => void
220
- annotationsEnabled: boolean
221
- onAnnotationsToggle: (enabled: boolean) => void
222
- // Review mode props
223
- reviewMode: boolean
224
- onReviewModeToggle: (enabled: boolean) => void
225
- // Batch action props
226
- onAcceptAllCorrections: () => void
227
- onAcceptHighConfidenceCorrections: () => void
228
- onRevertAllCorrections: () => void
229
- }
230
-
231
- // Create a memoized Header component
232
- const MemoizedHeader = memo(function MemoizedHeader({
233
- isReadOnly,
234
- onFileLoad,
235
- data,
236
- onMetricClick,
237
- effectiveMode,
238
- onModeChange,
239
- apiClient,
240
- audioHash,
241
- onTimeUpdate,
242
- onHandlerToggle,
243
- isUpdatingHandlers,
244
- onHandlerClick,
245
- onFindReplace,
246
- onEditAll,
247
- onTimingOffset,
248
- timingOffsetMs,
249
- onUndo,
250
- onRedo,
251
- canUndo,
252
- canRedo,
253
- onUnCorrectAll,
254
- onResetCorrections,
255
- annotationsEnabled,
256
- onAnnotationsToggle,
257
- reviewMode,
258
- onReviewModeToggle,
259
- onAcceptAllCorrections,
260
- onAcceptHighConfidenceCorrections,
261
- onRevertAllCorrections
262
- }: MemoizedHeaderProps) {
263
- return (
264
- <Header
265
- isReadOnly={isReadOnly}
266
- onFileLoad={onFileLoad}
267
- data={data}
268
- onMetricClick={onMetricClick}
269
- effectiveMode={effectiveMode}
270
- onModeChange={onModeChange}
271
- apiClient={apiClient}
272
- audioHash={audioHash}
273
- onTimeUpdate={onTimeUpdate}
274
- onHandlerToggle={onHandlerToggle}
275
- isUpdatingHandlers={isUpdatingHandlers}
276
- onHandlerClick={onHandlerClick}
277
- onFindReplace={onFindReplace}
278
- onEditAll={onEditAll}
279
- onTimingOffset={onTimingOffset}
280
- timingOffsetMs={timingOffsetMs}
281
- onUndo={onUndo}
282
- onRedo={onRedo}
283
- canUndo={canUndo}
284
- canRedo={canRedo}
285
- onUnCorrectAll={onUnCorrectAll}
286
- onResetCorrections={onResetCorrections}
287
- annotationsEnabled={annotationsEnabled}
288
- onAnnotationsToggle={onAnnotationsToggle}
289
- reviewMode={reviewMode}
290
- onReviewModeToggle={onReviewModeToggle}
291
- onAcceptAllCorrections={onAcceptAllCorrections}
292
- onAcceptHighConfidenceCorrections={onAcceptHighConfidenceCorrections}
293
- onRevertAllCorrections={onRevertAllCorrections}
294
- />
295
- );
296
- });
297
-
298
- export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly, audioHash }: LyricsAnalyzerProps) {
299
- const [modalContent, setModalContent] = useState<ModalContent | null>(null)
300
- const [flashingType, setFlashingType] = useState<FlashType>(null)
301
- const [highlightInfo, setHighlightInfo] = useState<HighlightInfo | null>(null)
302
- const [currentSource, setCurrentSource] = useState<string>(() => {
303
- if (!initialData?.reference_lyrics) {
304
- return ''
305
- }
306
- const availableSources = Object.keys(initialData.reference_lyrics)
307
- return availableSources.length > 0 ? availableSources[0] : ''
308
- })
309
- const [isReviewComplete, setIsReviewComplete] = useState(false)
310
- const [originalData] = useState(() => JSON.parse(JSON.stringify(initialData)))
311
- const [interactionMode, setInteractionMode] = useState<InteractionMode>('edit')
312
- const [isShiftPressed, setIsShiftPressed] = useState(false)
313
- const [isCtrlPressed, setIsCtrlPressed] = useState(false)
314
- const [editModalSegment, setEditModalSegment] = useState<{
315
- segment: LyricsSegment
316
- index: number
317
- originalSegment: LyricsSegment
318
- } | null>(null)
319
- const [isReplaceAllLyricsModalOpen, setIsReplaceAllLyricsModalOpen] = useState(false)
320
- const [isReviewModalOpen, setIsReviewModalOpen] = useState(false)
321
- const [currentAudioTime, setCurrentAudioTime] = useState(0)
322
- const [isUpdatingHandlers, setIsUpdatingHandlers] = useState(false)
323
- const [flashingHandler, setFlashingHandler] = useState<string | null>(null)
324
- const [isAddingLyrics, setIsAddingLyrics] = useState(false)
325
- const [isAddLyricsModalOpen, setIsAddLyricsModalOpen] = useState(false)
326
- const [isAnyModalOpen, setIsAnyModalOpen] = useState(false)
327
- const [isFindReplaceModalOpen, setIsFindReplaceModalOpen] = useState(false)
328
- const [isTimingOffsetModalOpen, setIsTimingOffsetModalOpen] = useState(false)
329
- const [timingOffsetMs, setTimingOffsetMs] = useState(0)
330
-
331
- // Review mode state for agentic corrections
332
- const [reviewMode, setReviewMode] = useState(false)
333
-
334
- // Annotation collection state
335
- const [annotations, setAnnotations] = useState<Omit<CorrectionAnnotation, 'annotation_id' | 'timestamp'>[]>([])
336
- const [isAnnotationModalOpen, setIsAnnotationModalOpen] = useState(false)
337
- const [pendingAnnotation, setPendingAnnotation] = useState<{
338
- originalText: string
339
- correctedText: string
340
- wordIdsAffected: string[]
341
- gapId?: string
342
- } | null>(null)
343
- const [annotationsEnabled, setAnnotationsEnabled] = useState(() => {
344
- // Check localStorage for user preference
345
- const saved = localStorage.getItem('annotationsEnabled')
346
- return saved !== null ? saved === 'true' : true // Default: enabled
347
- })
348
-
349
- // Persist annotation preference to localStorage
350
- const handleAnnotationsToggle = useCallback((enabled: boolean) => {
351
- setAnnotationsEnabled(enabled)
352
- localStorage.setItem('annotationsEnabled', String(enabled))
353
- }, [])
354
-
355
- // Correction detail card state
356
- const [correctionDetailOpen, setCorrectionDetailOpen] = useState(false)
357
- const [selectedCorrection, setSelectedCorrection] = useState<{
358
- wordId: string
359
- originalWord: string
360
- correctedWord: string
361
- category: string | null
362
- confidence: number
363
- reason: string
364
- handler: string
365
- source: string
366
- } | null>(null)
367
-
368
- const theme = useTheme()
369
- const isMobile = useMediaQuery(theme.breakpoints.down('md'))
370
-
371
- // State history for Undo/Redo
372
- const [history, setHistory] = useState<CorrectionData[]>([initialData])
373
- const [historyIndex, setHistoryIndex] = useState(0)
374
-
375
- // Derived state: the current data based on history index
376
- const data = history[historyIndex];
377
-
378
- // Function to update data and manage history
379
- const updateDataWithHistory = useCallback((newData: CorrectionData, actionDescription?: string) => {
380
- if (debugLog) {
381
- console.log(`[DEBUG] updateDataWithHistory: Action - ${actionDescription || 'Unknown'}. Current index: ${historyIndex}, History length: ${history.length}`);
382
- }
383
- const newHistory = history.slice(0, historyIndex + 1)
384
- const deepCopiedNewData = JSON.parse(JSON.stringify(newData));
385
-
386
- newHistory.push(deepCopiedNewData)
387
- setHistory(newHistory)
388
- setHistoryIndex(newHistory.length - 1)
389
- if (debugLog) {
390
- console.log(`[DEBUG] updateDataWithHistory: History updated. New index: ${newHistory.length - 1}, New length: ${newHistory.length}`);
391
- }
392
- }, [history, historyIndex])
393
-
394
- // Reset history when initial data changes (e.g., new file loaded)
395
- useEffect(() => {
396
- setHistory([initialData])
397
- setHistoryIndex(0)
398
- }, [initialData])
399
-
400
- // Update debug logging to use new ID-based structure
401
- useEffect(() => {
402
- if (debugLog) {
403
- console.log('LyricsAnalyzer Initial Data:', {
404
- hasData: !!initialData,
405
- segmentsCount: initialData?.corrected_segments?.length ?? 0,
406
- anchorsCount: initialData?.anchor_sequences?.length ?? 0,
407
- gapsCount: initialData?.gap_sequences?.length ?? 0,
408
- firstAnchor: initialData?.anchor_sequences?.[0] && {
409
- transcribedWordIds: initialData.anchor_sequences[0].transcribed_word_ids,
410
- referenceWordIds: initialData.anchor_sequences[0].reference_word_ids
411
- },
412
- firstSegment: initialData?.corrected_segments?.[0],
413
- referenceSources: Object.keys(initialData?.reference_lyrics ?? {})
414
- });
415
- }
416
- }, [initialData]);
417
-
418
- // Load saved data
419
- useEffect(() => {
420
- const savedData = loadSavedData(initialData)
421
- if (savedData && window.confirm('Found saved progress for this song. Would you like to restore it?')) {
422
- // Replace history with saved data as the initial state
423
- setHistory([savedData])
424
- setHistoryIndex(0)
425
- }
426
- }, [initialData]) // Keep dependency only on initialData
427
-
428
- // Save data - This should save the *current* state, not affect history
429
- useEffect(() => {
430
- if (!isReadOnly) {
431
- saveData(data, initialData) // Use 'data' derived from history and the initialData prop
432
- }
433
- }, [data, isReadOnly, initialData]) // Correct dependencies
434
-
435
- // Keyboard handlers
436
- useEffect(() => {
437
- const { currentModalHandler } = getModalState()
438
-
439
- if (debugLog) {
440
- console.log('LyricsAnalyzer - Setting up keyboard effect', {
441
- isAnyModalOpen,
442
- hasSpacebarHandler: !!currentModalHandler
443
- })
444
- }
445
-
446
- const { handleKeyDown, handleKeyUp, cleanup } = setupKeyboardHandlers({
447
- setIsShiftPressed,
448
- setIsCtrlPressed
449
- })
450
-
451
- // Always add keyboard listeners
452
- if (debugLog) {
453
- console.log('LyricsAnalyzer - Adding keyboard event listeners')
454
- }
455
- window.addEventListener('keydown', handleKeyDown)
456
- window.addEventListener('keyup', handleKeyUp)
457
-
458
- // Reset modifier states when a modal opens
459
- if (isAnyModalOpen) {
460
- setIsShiftPressed(false)
461
- setIsCtrlPressed(false)
462
- }
463
-
464
- // Cleanup function
465
- return () => {
466
- if (debugLog) {
467
- console.log('LyricsAnalyzer - Cleanup effect running')
468
- }
469
- window.removeEventListener('keydown', handleKeyDown)
470
- window.removeEventListener('keyup', handleKeyUp)
471
- document.body.style.userSelect = ''
472
- // Call the cleanup function to remove window blur/focus listeners
473
- cleanup()
474
- }
475
- }, [setIsShiftPressed, setIsCtrlPressed, isAnyModalOpen])
476
-
477
- // Update modal state tracking
478
- useEffect(() => {
479
- const modalOpen = Boolean(
480
- modalContent ||
481
- editModalSegment ||
482
- isReviewModalOpen ||
483
- isAddLyricsModalOpen ||
484
- isFindReplaceModalOpen ||
485
- isReplaceAllLyricsModalOpen ||
486
- isTimingOffsetModalOpen
487
- )
488
- setIsAnyModalOpen(modalOpen)
489
- }, [modalContent, editModalSegment, isReviewModalOpen, isAddLyricsModalOpen, isFindReplaceModalOpen, isReplaceAllLyricsModalOpen, isTimingOffsetModalOpen])
490
-
491
- // Calculate effective mode based on modifier key states
492
- const effectiveMode = isCtrlPressed ? 'delete_word' : (isShiftPressed ? 'highlight' : interactionMode)
493
-
494
- const handleFlash = useCallback((type: FlashType, info?: HighlightInfo) => {
495
- setFlashingType(null)
496
- setHighlightInfo(null)
497
-
498
- requestAnimationFrame(() => {
499
- requestAnimationFrame(() => {
500
- setFlashingType(type)
501
- if (info) {
502
- setHighlightInfo(info)
503
- }
504
- setTimeout(() => {
505
- setFlashingType(null)
506
- setHighlightInfo(null)
507
- }, 1200)
508
- })
509
- })
510
- }, [])
511
-
512
- const handleWordClick = useCallback((info: WordClickInfo) => {
513
- if (debugLog) {
514
- console.log('LyricsAnalyzer handleWordClick:', { info });
515
- }
516
-
517
- if (effectiveMode === 'delete_word') {
518
- // Use the shared deleteWord utility function
519
- const newData = deleteWord(data, info.word_id);
520
- updateDataWithHistory(newData, 'delete word'); // Update history
521
-
522
- // Flash to indicate the word was deleted
523
- handleFlash('word');
524
- return;
525
- }
526
-
527
- if (effectiveMode === 'highlight') {
528
- // Find if this word is part of a correction
529
- const correction = data.corrections?.find(c =>
530
- c.corrected_word_id === info.word_id ||
531
- c.word_id === info.word_id
532
- );
533
-
534
- if (correction) {
535
- // For agentic corrections, show the detail card instead of just highlighting
536
- if (correction.handler === 'AgenticCorrector') {
537
- handleShowCorrectionDetail(info.word_id)
538
- return
539
- }
540
-
541
- setHighlightInfo({
542
- type: 'correction',
543
- transcribed_words: [], // Required by type but not used for corrections
544
- correction: correction
545
- });
546
- setFlashingType('word');
547
- return;
548
- }
549
-
550
- // Find if this word is part of an anchor sequence
551
- const anchor = data.anchor_sequences?.find(a =>
552
- a.transcribed_word_ids.includes(info.word_id) ||
553
- Object.values(a.reference_word_ids).some(ids =>
554
- ids.includes(info.word_id)
555
- )
556
- );
557
-
558
- if (anchor) {
559
- // Create a temporary segment containing all words
560
- const allWords = data.corrected_segments.flatMap(s => s.words)
561
- const tempSegment: LyricsSegment = {
562
- id: 'temp',
563
- words: allWords,
564
- text: allWords.map(w => w.text).join(' '),
565
- start_time: allWords[0]?.start_time ?? null,
566
- end_time: allWords[allWords.length - 1]?.end_time ?? null
567
- }
568
-
569
- const transcribedWords = getWordsFromIds(
570
- [tempSegment],
571
- anchor.transcribed_word_ids
572
- );
573
-
574
- const referenceWords = Object.fromEntries(
575
- Object.entries(anchor.reference_word_ids).map(([source, ids]) => {
576
- const sourceWords = data.reference_lyrics[source].segments.flatMap(s => s.words)
577
- const tempSourceSegment: LyricsSegment = {
578
- id: `temp-${source}`,
579
- words: sourceWords,
580
- text: sourceWords.map(w => w.text).join(' '),
581
- start_time: sourceWords[0]?.start_time ?? null,
582
- end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
583
- }
584
- return [source, getWordsFromIds([tempSourceSegment], ids)]
585
- })
586
- );
587
-
588
- setHighlightInfo({
589
- type: 'anchor',
590
- sequence: anchor,
591
- transcribed_words: transcribedWords,
592
- reference_words: referenceWords
593
- });
594
- setFlashingType('word');
595
- return;
596
- }
597
-
598
- // Find if this word is part of a gap sequence
599
- const gap = data.gap_sequences?.find(g =>
600
- g.transcribed_word_ids.includes(info.word_id)
601
- );
602
-
603
- if (gap) {
604
- // Create a temporary segment containing all words
605
- const allWords = data.corrected_segments.flatMap(s => s.words)
606
- const tempSegment: LyricsSegment = {
607
- id: 'temp',
608
- words: allWords,
609
- text: allWords.map(w => w.text).join(' '),
610
- start_time: allWords[0]?.start_time ?? null,
611
- end_time: allWords[allWords.length - 1]?.end_time ?? null
612
- }
613
-
614
- const transcribedWords = getWordsFromIds(
615
- [tempSegment],
616
- gap.transcribed_word_ids
617
- );
618
-
619
- const referenceWords = Object.fromEntries(
620
- Object.entries(gap.reference_word_ids).map(([source, ids]) => {
621
- const sourceWords = data.reference_lyrics[source].segments.flatMap(s => s.words)
622
- const tempSourceSegment: LyricsSegment = {
623
- id: `temp-${source}`,
624
- words: sourceWords,
625
- text: sourceWords.map(w => w.text).join(' '),
626
- start_time: sourceWords[0]?.start_time ?? null,
627
- end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
628
- }
629
- return [source, getWordsFromIds([tempSourceSegment], ids)]
630
- })
631
- );
632
-
633
- setHighlightInfo({
634
- type: 'gap',
635
- sequence: gap,
636
- transcribed_words: transcribedWords,
637
- reference_words: referenceWords
638
- });
639
- setFlashingType('word');
640
- return;
641
- }
642
- } else if (effectiveMode === 'edit') {
643
- // Find the segment containing this word
644
- const segmentIndex = data.corrected_segments.findIndex(segment =>
645
- segment.words.some(word => word.id === info.word_id)
646
- );
647
-
648
- if (segmentIndex !== -1) {
649
- const segment = data.corrected_segments[segmentIndex];
650
- setEditModalSegment({
651
- segment,
652
- index: segmentIndex,
653
- originalSegment: JSON.parse(JSON.stringify(segment))
654
- });
655
- }
656
- }
657
- }, [data, effectiveMode, setModalContent, handleFlash, deleteWord, updateDataWithHistory]);
658
-
659
- // Annotation handlers (declared early for use in other callbacks)
660
- const handleSaveAnnotation = useCallback((annotation: Omit<CorrectionAnnotation, 'annotation_id' | 'timestamp'>) => {
661
- setAnnotations(prev => [...prev, annotation])
662
- console.log('Annotation saved:', annotation)
663
- }, [])
664
-
665
- const handleSkipAnnotation = useCallback(() => {
666
- console.log('Annotation skipped')
667
- }, [])
668
-
669
- const triggerAnnotationModal = useCallback((
670
- originalText: string,
671
- correctedText: string,
672
- wordIdsAffected: string[],
673
- gapId?: string
674
- ) => {
675
- if (!annotationsEnabled || isReadOnly) {
676
- return
677
- }
678
-
679
- setPendingAnnotation({
680
- originalText,
681
- correctedText,
682
- wordIdsAffected,
683
- gapId
684
- })
685
- setIsAnnotationModalOpen(true)
686
- }, [annotationsEnabled, isReadOnly])
687
-
688
- const handleUpdateSegment = useCallback((updatedSegment: LyricsSegment) => {
689
- if (!editModalSegment) return
690
-
691
- if (debugLog) {
692
- console.log('[DEBUG] handleUpdateSegment: Updating history from modal save', {
693
- segmentIndex: editModalSegment.index,
694
- currentHistoryIndex: historyIndex,
695
- currentHistoryLength: history.length,
696
- currentSegmentText: history[historyIndex]?.corrected_segments[editModalSegment.index]?.text,
697
- updatedSegmentText: updatedSegment.text
698
- });
699
- }
700
-
701
- // --- Ensure Immutability Here ---
702
- const currentData = history[historyIndex];
703
- const newSegments = currentData.corrected_segments.map((segment, i) =>
704
- i === editModalSegment.index ? updatedSegment : segment
705
- );
706
- const newDataImmutable: CorrectionData = {
707
- ...currentData,
708
- corrected_segments: newSegments,
709
- };
710
- // --- End Immutability Ensure ---
711
-
712
- updateDataWithHistory(newDataImmutable, 'update segment');
713
-
714
- if (debugLog) {
715
- console.log('[DEBUG] handleUpdateSegment: History updated (async)', {
716
- newHistoryIndex: historyIndex + 1,
717
- newHistoryLength: history.length - historyIndex === 1 ? history.length + 1 : historyIndex + 2
718
- });
719
- }
720
-
721
- // Trigger annotation modal if enabled and text changed
722
- const originalSegment = editModalSegment.originalSegment || editModalSegment.segment
723
- if (originalSegment && originalSegment.text !== updatedSegment.text) {
724
- // Get word IDs that were affected
725
- const wordIds = updatedSegment.words.map(w => w.id)
726
- triggerAnnotationModal(
727
- originalSegment.text,
728
- updatedSegment.text,
729
- wordIds
730
- )
731
- }
732
-
733
- setEditModalSegment(null)
734
- }, [history, historyIndex, editModalSegment, updateDataWithHistory, triggerAnnotationModal])
735
-
736
- const handleDeleteSegment = useCallback((segmentIndex: number) => {
737
- const newData = deleteSegment(data, segmentIndex)
738
- updateDataWithHistory(newData, 'delete segment')
739
- }, [data, updateDataWithHistory])
740
-
741
- // Correction action handlers
742
- const handleRevertCorrection = useCallback((wordId: string) => {
743
- // Find the correction for this word
744
- const correction = data.corrections?.find(c =>
745
- c.corrected_word_id === wordId || c.word_id === wordId
746
- )
747
-
748
- if (!correction) {
749
- console.error('Correction not found for word:', wordId)
750
- return
751
- }
752
-
753
- // Find the segment containing the corrected word
754
- const segmentIndex = data.corrected_segments.findIndex(segment =>
755
- segment.words.some(w => w.id === wordId)
756
- )
757
-
758
- if (segmentIndex === -1) {
759
- console.error('Segment not found for word:', wordId)
760
- return
761
- }
762
-
763
- const segment = data.corrected_segments[segmentIndex]
764
-
765
- // Replace the corrected word with the original
766
- const newWords = segment.words.map(word => {
767
- if (word.id === wordId) {
768
- return {
769
- ...word,
770
- text: correction.original_word,
771
- id: correction.word_id // Restore original word ID
772
- }
773
- }
774
- return word
775
- })
776
-
777
- // Rebuild segment text
778
- const newText = newWords.map(w => w.text).join(' ')
779
-
780
- const newSegment = {
781
- ...segment,
782
- words: newWords,
783
- text: newText
784
- }
785
-
786
- // Update data
787
- const newSegments = data.corrected_segments.map((seg, idx) =>
788
- idx === segmentIndex ? newSegment : seg
789
- )
790
-
791
- // Remove the correction from the corrections list
792
- const newCorrections = data.corrections?.filter(c =>
793
- c.corrected_word_id !== wordId && c.word_id !== wordId
794
- )
795
-
796
- const newData: CorrectionData = {
797
- ...data,
798
- corrected_segments: newSegments,
799
- corrections: newCorrections || []
800
- }
801
-
802
- updateDataWithHistory(newData, 'revert correction')
803
-
804
- console.log('Reverted correction:', {
805
- originalWord: correction.original_word,
806
- correctedWord: correction.corrected_word,
807
- wordId
808
- })
809
- }, [data, updateDataWithHistory])
810
-
811
- const handleEditCorrection = useCallback((wordId: string) => {
812
- // Find the segment containing this word
813
- const segmentIndex = data.corrected_segments.findIndex(segment =>
814
- segment.words.some(w => w.id === wordId)
815
- )
816
-
817
- if (segmentIndex === -1) {
818
- console.error('Segment not found for word:', wordId)
819
- return
820
- }
821
-
822
- // Open edit modal for this segment
823
- const segment = data.corrected_segments[segmentIndex]
824
- setEditModalSegment({
825
- segment: segment,
826
- index: segmentIndex,
827
- originalSegment: segment
828
- })
829
- }, [data])
830
-
831
- const handleAcceptCorrection = useCallback((wordId: string) => {
832
- // For now, just log acceptance
833
- // In the future, this could be tracked in the annotation system
834
- console.log('Accepted correction for word:', wordId)
835
-
836
- // TODO: Track acceptance in annotation system
837
- // This could be used to build confidence in the AI's corrections over time
838
- }, [])
839
-
840
- // Batch action handlers for review mode
841
- const handleAcceptAllCorrections = useCallback(() => {
842
- // Accept all corrections - for now just log, could track in annotation system
843
- data.corrections?.forEach(c => {
844
- console.log('Batch accepted correction:', c.corrected_word_id || c.word_id)
845
- })
846
- console.log(`Accepted all ${data.corrections?.length || 0} corrections`)
847
- }, [data.corrections])
848
-
849
- const handleAcceptHighConfidenceCorrections = useCallback(() => {
850
- // Accept corrections with confidence >= 80%
851
- const highConfidence = data.corrections?.filter(c => c.confidence >= 0.8) || []
852
- highConfidence.forEach(c => {
853
- console.log('Batch accepted high-confidence correction:', c.corrected_word_id || c.word_id)
854
- })
855
- console.log(`Accepted ${highConfidence.length} high-confidence corrections`)
856
- }, [data.corrections])
857
-
858
- const handleRevertAllCorrections = useCallback(() => {
859
- if (!window.confirm(`Are you sure you want to revert all ${data.corrections?.length || 0} corrections? This cannot be undone.`)) {
860
- return
861
- }
862
-
863
- // Revert all corrections by reverting each one
864
- // Process in reverse order to avoid ID conflicts
865
- const corrections = [...(data.corrections || [])].reverse()
866
-
867
- let newData = data
868
- for (const correction of corrections) {
869
- const wordId = correction.corrected_word_id || correction.word_id
870
-
871
- // Find the segment containing the corrected word
872
- const segmentIndex = newData.corrected_segments.findIndex(segment =>
873
- segment.words.some(w => w.id === wordId)
874
- )
875
-
876
- if (segmentIndex === -1) continue
877
-
878
- const segment = newData.corrected_segments[segmentIndex]
879
-
880
- // Replace the corrected word with the original
881
- const newWords = segment.words.map(word => {
882
- if (word.id === wordId) {
883
- return {
884
- ...word,
885
- text: correction.original_word,
886
- id: correction.word_id
887
- }
888
- }
889
- return word
890
- })
891
-
892
- const newText = newWords.map(w => w.text).join(' ')
893
- const newSegment = { ...segment, words: newWords, text: newText }
894
- const newSegments = newData.corrected_segments.map((seg, idx) =>
895
- idx === segmentIndex ? newSegment : seg
896
- )
897
-
898
- newData = {
899
- ...newData,
900
- corrected_segments: newSegments,
901
- corrections: newData.corrections?.filter(c =>
902
- c.corrected_word_id !== wordId && c.word_id !== wordId
903
- ) || []
904
- }
905
- }
906
-
907
- updateDataWithHistory(newData, 'revert all corrections')
908
- console.log(`Reverted all ${corrections.length} corrections`)
909
- }, [data, updateDataWithHistory])
910
-
911
- const handleShowCorrectionDetail = useCallback((wordId: string) => {
912
- // Find the correction for this word
913
- const correction = data.corrections?.find(c =>
914
- c.corrected_word_id === wordId || c.word_id === wordId
915
- )
916
-
917
- if (!correction) {
918
- console.error('Correction not found for word:', wordId)
919
- return
920
- }
921
-
922
- // Extract category from reason (format: "reason [CATEGORY] (confidence: XX%)")
923
- const categoryMatch = correction.reason?.match(/\[([A-Z_]+)\]/)
924
- const category = categoryMatch ? categoryMatch[1] : null
925
-
926
- // Find the corrected word text
927
- const correctedWord = data.corrected_segments
928
- .flatMap(s => s.words)
929
- .find(w => w.id === wordId)?.text || correction.corrected_word
930
-
931
- setSelectedCorrection({
932
- wordId,
933
- originalWord: correction.original_word,
934
- correctedWord: correctedWord,
935
- category,
936
- confidence: correction.confidence,
937
- reason: correction.reason,
938
- handler: correction.handler,
939
- source: correction.source
940
- })
941
- setCorrectionDetailOpen(true)
942
- }, [data])
943
-
944
- const handleFinishReview = useCallback(() => {
945
- console.log(`[TIMING] handleFinishReview - Current timing offset: ${timingOffsetMs}ms`);
946
- setIsReviewModalOpen(true)
947
- }, [timingOffsetMs])
948
-
949
- const handleSubmitToServer = useCallback(async () => {
950
- if (!apiClient) return
951
-
952
- try {
953
- if (debugLog) {
954
- console.log('Submitting changes to server')
955
- }
956
-
957
- // Debug logging for timing offset
958
- console.log(`[TIMING] handleSubmitToServer - Current timing offset: ${timingOffsetMs}ms`);
959
-
960
- // Apply timing offset to the data before submission if needed
961
- const dataToSubmit = timingOffsetMs !== 0
962
- ? applyOffsetToCorrectionData(data, timingOffsetMs)
963
- : data
964
-
965
- // Log some example timestamps after potential offset application
966
- if (dataToSubmit.corrected_segments.length > 0) {
967
- const firstSegment = dataToSubmit.corrected_segments[0];
968
- console.log(`[TIMING] Submitting data - First segment id: ${firstSegment.id}`);
969
- console.log(`[TIMING] - start_time: ${firstSegment.start_time}, end_time: ${firstSegment.end_time}`);
970
-
971
- if (firstSegment.words.length > 0) {
972
- const firstWord = firstSegment.words[0];
973
- console.log(`[TIMING] - first word "${firstWord.text}" time: ${firstWord.start_time} -> ${firstWord.end_time}`);
974
- }
975
- }
976
-
977
- await apiClient.submitCorrections(dataToSubmit)
978
-
979
- // Submit annotations if any were collected
980
- if (annotations.length > 0) {
981
- console.log(`Submitting ${annotations.length} annotations...`)
982
- try {
983
- await apiClient.submitAnnotations(annotations)
984
- console.log('Annotations submitted successfully')
985
- } catch (error) {
986
- console.error('Failed to submit annotations:', error)
987
- // Don't block the main submission if annotations fail
988
- }
989
- }
990
-
991
- setIsReviewComplete(true)
992
- setIsReviewModalOpen(false)
993
-
994
- // Close the browser tab
995
- window.close()
996
- } catch (error) {
997
- console.error('Failed to submit corrections:', error)
998
- alert('Failed to submit corrections. Please try again.')
999
- }
1000
- }, [apiClient, data, timingOffsetMs, annotations])
1001
-
1002
- // Update play segment handler
1003
- const handlePlaySegment = useCallback((startTime: number) => {
1004
- if (window.seekAndPlayAudio) {
1005
- // Apply the timing offset to the start time
1006
- const adjustedStartTime = timingOffsetMs !== 0
1007
- ? startTime + (timingOffsetMs / 1000)
1008
- : startTime;
1009
-
1010
- window.seekAndPlayAudio(adjustedStartTime)
1011
- }
1012
- }, [timingOffsetMs])
1013
-
1014
- const handleResetCorrections = useCallback(() => {
1015
- if (window.confirm('Are you sure you want to reset all corrections? This cannot be undone.')) {
1016
- clearSavedData(initialData)
1017
- // Reset history to the original initial data
1018
- setHistory([JSON.parse(JSON.stringify(initialData))])
1019
- setHistoryIndex(0)
1020
- setModalContent(null)
1021
- setFlashingType(null)
1022
- setHighlightInfo(null)
1023
- setInteractionMode('edit')
1024
- }
1025
- }, [initialData])
1026
-
1027
- const handleAddSegment = useCallback((beforeIndex: number) => {
1028
- const newData = addSegmentBefore(data, beforeIndex)
1029
- updateDataWithHistory(newData, 'add segment')
1030
- }, [data, updateDataWithHistory])
1031
-
1032
- const handleSplitSegment = useCallback((segmentIndex: number, afterWordIndex: number) => {
1033
- const newData = splitSegment(data, segmentIndex, afterWordIndex)
1034
- if (newData) {
1035
- updateDataWithHistory(newData, 'split segment')
1036
- setEditModalSegment(null)
1037
- }
1038
- }, [data, updateDataWithHistory])
1039
-
1040
- const handleMergeSegment = useCallback((segmentIndex: number, mergeWithNext: boolean) => {
1041
- const newData = mergeSegment(data, segmentIndex, mergeWithNext)
1042
- updateDataWithHistory(newData, 'merge segment')
1043
- setEditModalSegment(null)
1044
- }, [data, updateDataWithHistory])
1045
-
1046
- const handleHandlerToggle = useCallback(async (handler: string, enabled: boolean) => {
1047
- if (!apiClient) return
1048
-
1049
- try {
1050
- setIsUpdatingHandlers(true);
1051
-
1052
- // Get current enabled handlers
1053
- const currentEnabled = new Set(data.metadata.enabled_handlers || [])
1054
-
1055
- // Update the set based on the toggle
1056
- if (enabled) {
1057
- currentEnabled.add(handler)
1058
- } else {
1059
- currentEnabled.delete(handler)
1060
- }
1061
-
1062
- // Call API to update handlers
1063
- const newData = await apiClient.updateHandlers(Array.from(currentEnabled))
1064
-
1065
- // Update local state with new correction data
1066
- // This API call returns the *entire* new state, so treat it as a single history step
1067
- updateDataWithHistory(newData, `toggle handler ${handler}`); // Update history
1068
-
1069
- // Clear any existing modals or highlights
1070
- setModalContent(null)
1071
- setFlashingType(null)
1072
- setHighlightInfo(null)
1073
-
1074
- // Flash the updated corrections
1075
- handleFlash('corrected')
1076
- } catch (error) {
1077
- console.error('Failed to update handlers:', error)
1078
- alert('Failed to update correction handlers. Please try again.')
1079
- } finally {
1080
- setIsUpdatingHandlers(false);
1081
- }
1082
- }, [apiClient, data.metadata.enabled_handlers, handleFlash, updateDataWithHistory])
1083
-
1084
- const handleHandlerClick = useCallback((handler: string) => {
1085
- if (debugLog) {
1086
- console.log('Handler clicked:', handler);
1087
- }
1088
- setFlashingHandler(handler);
1089
- setFlashingType('handler');
1090
- if (debugLog) {
1091
- console.log('Set flashingHandler to:', handler);
1092
- console.log('Set flashingType to: handler');
1093
- }
1094
- // Clear the flash after a short delay
1095
- setTimeout(() => {
1096
- if (debugLog) {
1097
- console.log('Clearing flash state');
1098
- }
1099
- setFlashingHandler(null);
1100
- setFlashingType(null);
1101
- }, 1500);
1102
- }, []);
1103
-
1104
- // Wrap setModalSpacebarHandler in useCallback
1105
- const handleSetModalSpacebarHandler = useCallback((handler: (() => (e: KeyboardEvent) => void) | undefined) => {
1106
- if (debugLog) {
1107
- console.log('LyricsAnalyzer - Setting modal handler:', {
1108
- hasHandler: !!handler
1109
- })
1110
- }
1111
- // Update the global modal handler
1112
- setModalHandler(handler ? handler() : undefined, !!handler)
1113
- }, [])
1114
-
1115
- // Add new handler for adding lyrics
1116
- const handleAddLyrics = useCallback(async (source: string, lyrics: string) => {
1117
- if (!apiClient) return
1118
-
1119
- try {
1120
- setIsAddingLyrics(true)
1121
- const newData = await apiClient.addLyrics(source, lyrics)
1122
- // This API call returns the *entire* new state
1123
- updateDataWithHistory(newData, 'add lyrics'); // Update history
1124
- } finally {
1125
- setIsAddingLyrics(false)
1126
- }
1127
- }, [apiClient, updateDataWithHistory])
1128
-
1129
- const handleFindReplace = (findText: string, replaceText: string, options: { caseSensitive: boolean, useRegex: boolean, fullTextMode: boolean }) => {
1130
- const newData = findAndReplace(data, findText, replaceText, options)
1131
- updateDataWithHistory(newData, 'find/replace'); // Update history
1132
- }
1133
-
1134
- // Add handler for Un-Correct All functionality
1135
- const handleUnCorrectAll = useCallback(() => {
1136
- if (!originalData.original_segments) {
1137
- console.warn('No original segments available for un-correcting')
1138
- return
1139
- }
1140
-
1141
- if (window.confirm('Are you sure you want to revert all segments to their original transcribed state? This will undo all corrections made.')) {
1142
- console.log('Un-Correct All: Reverting all segments to original transcribed state', {
1143
- originalSegmentCount: originalData.original_segments.length,
1144
- currentSegmentCount: data.corrected_segments.length
1145
- })
1146
-
1147
- // Create new data with original segments as corrected segments
1148
- const newData: CorrectionData = {
1149
- ...data,
1150
- corrected_segments: JSON.parse(JSON.stringify(originalData.original_segments))
1151
- }
1152
-
1153
- updateDataWithHistory(newData, 'un-correct all segments')
1154
- }
1155
- }, [originalData.original_segments, data, updateDataWithHistory])
1156
-
1157
- // Add handler for Replace All Lyrics functionality
1158
- const handleReplaceAllLyrics = useCallback(() => {
1159
- console.log('ReplaceAllLyrics - Opening modal')
1160
- setIsReplaceAllLyricsModalOpen(true)
1161
- }, [])
1162
-
1163
- // Handle saving new segments from Replace All Lyrics
1164
- const handleSaveReplaceAllLyrics = useCallback((newSegments: LyricsSegment[]) => {
1165
- console.log('ReplaceAllLyrics - Saving new segments:', {
1166
- segmentCount: newSegments.length,
1167
- totalWords: newSegments.reduce((count, segment) => count + segment.words.length, 0)
1168
- })
1169
-
1170
- // Create new data with the replaced segments
1171
- const newData = {
1172
- ...data,
1173
- corrected_segments: newSegments
1174
- }
1175
-
1176
- updateDataWithHistory(newData, 'replace all lyrics')
1177
- setIsReplaceAllLyricsModalOpen(false)
1178
- }, [data, updateDataWithHistory])
1179
-
1180
-
1181
-
1182
- // Undo/Redo handlers
1183
- const handleUndo = useCallback(() => {
1184
- if (historyIndex > 0) {
1185
- const newIndex = historyIndex - 1;
1186
- if (debugLog) {
1187
- console.log(`[DEBUG] Undo: moving from index ${historyIndex} to ${newIndex}. History length: ${history.length}`);
1188
- }
1189
- setHistoryIndex(newIndex);
1190
- } else {
1191
- if (debugLog) {
1192
- console.log(`[DEBUG] Undo: already at the beginning (index ${historyIndex})`);
1193
- }
1194
- }
1195
- }, [historyIndex, history])
1196
-
1197
- const handleRedo = useCallback(() => {
1198
- if (historyIndex < history.length - 1) {
1199
- const newIndex = historyIndex + 1;
1200
- if (debugLog) {
1201
- console.log(`[DEBUG] Redo: moving from index ${historyIndex} to ${newIndex}. History length: ${history.length}`);
1202
- }
1203
- setHistoryIndex(newIndex);
1204
- } else {
1205
- if (debugLog) {
1206
- console.log(`[DEBUG] Redo: already at the end (index ${historyIndex}, history length ${history.length})`);
1207
- }
1208
- }
1209
- }, [historyIndex, history])
1210
-
1211
- // Determine if Undo/Redo is possible
1212
- const canUndo = historyIndex > 0
1213
- const canRedo = historyIndex < history.length - 1
1214
-
1215
- // Memoize the metric click handlers
1216
- const metricClickHandlers = useMemo(() => ({
1217
- anchor: () => handleFlash('anchor'),
1218
- corrected: () => handleFlash('corrected'),
1219
- uncorrected: () => handleFlash('uncorrected')
1220
- }), [handleFlash]);
1221
-
1222
- // Determine if any modal is open to disable highlighting
1223
- const isAnyModalOpenMemo = useMemo(() => isAnyModalOpen, [isAnyModalOpen]);
1224
-
1225
- // For the TranscriptionView, we need to apply the timing offset when displaying
1226
- const displayData = useMemo(() => {
1227
- return timingOffsetMs !== 0
1228
- ? applyOffsetToCorrectionData(data, timingOffsetMs)
1229
- : data;
1230
- }, [data, timingOffsetMs]);
1231
-
1232
- // Handler for opening the timing offset modal
1233
- const handleOpenTimingOffsetModal = useCallback(() => {
1234
- setIsTimingOffsetModalOpen(true)
1235
- }, [])
1236
-
1237
- // Handler for applying the timing offset
1238
- const handleApplyTimingOffset = useCallback((offsetMs: number) => {
1239
- // Only update if the offset has changed
1240
- if (offsetMs !== timingOffsetMs) {
1241
- console.log(`[TIMING] handleApplyTimingOffset: Changing offset from ${timingOffsetMs}ms to ${offsetMs}ms`);
1242
- setTimingOffsetMs(offsetMs)
1243
-
1244
- // If we're applying an offset, we don't need to update history
1245
- // since we're not modifying the original data
1246
- if (debugLog) {
1247
- console.log(`[DEBUG] handleApplyTimingOffset: Setting offset to ${offsetMs}ms`);
1248
- }
1249
- } else {
1250
- console.log(`[TIMING] handleApplyTimingOffset: Offset unchanged at ${offsetMs}ms`);
1251
- }
1252
- }, [timingOffsetMs])
1253
-
1254
- // Add logging for timing offset changes
1255
- useEffect(() => {
1256
- console.log(`[TIMING] timingOffsetMs changed to: ${timingOffsetMs}ms`);
1257
- }, [timingOffsetMs]);
1258
-
1259
- return (
1260
- <Box sx={{
1261
- maxWidth: '100%',
1262
- overflowX: 'hidden'
1263
- }}>
1264
- <MemoizedHeader
1265
- isReadOnly={isReadOnly}
1266
- onFileLoad={onFileLoad}
1267
- data={data}
1268
- onMetricClick={metricClickHandlers}
1269
- effectiveMode={effectiveMode}
1270
- onModeChange={setInteractionMode}
1271
- apiClient={apiClient}
1272
- audioHash={audioHash}
1273
- onTimeUpdate={setCurrentAudioTime}
1274
- onHandlerToggle={handleHandlerToggle}
1275
- isUpdatingHandlers={isUpdatingHandlers}
1276
- onHandlerClick={handleHandlerClick}
1277
- onFindReplace={() => setIsFindReplaceModalOpen(true)}
1278
- onEditAll={handleReplaceAllLyrics}
1279
- onTimingOffset={handleOpenTimingOffsetModal}
1280
- timingOffsetMs={timingOffsetMs}
1281
- onUndo={handleUndo}
1282
- onRedo={handleRedo}
1283
- canUndo={canUndo}
1284
- canRedo={canRedo}
1285
- onUnCorrectAll={handleUnCorrectAll}
1286
- onResetCorrections={handleResetCorrections}
1287
- annotationsEnabled={annotationsEnabled}
1288
- onAnnotationsToggle={handleAnnotationsToggle}
1289
- reviewMode={reviewMode}
1290
- onReviewModeToggle={setReviewMode}
1291
- onAcceptAllCorrections={handleAcceptAllCorrections}
1292
- onAcceptHighConfidenceCorrections={handleAcceptHighConfidenceCorrections}
1293
- onRevertAllCorrections={handleRevertAllCorrections}
1294
- />
1295
-
1296
- <Grid container direction={isMobile ? 'column' : 'row'} spacing={1}>
1297
- <Grid item xs={12} md={6}>
1298
- <MemoizedTranscriptionView
1299
- data={displayData}
1300
- mode={effectiveMode}
1301
- onElementClick={setModalContent}
1302
- onWordClick={handleWordClick}
1303
- flashingType={flashingType}
1304
- flashingHandler={flashingHandler}
1305
- highlightInfo={highlightInfo}
1306
- onPlaySegment={handlePlaySegment}
1307
- currentTime={currentAudioTime}
1308
- anchors={data.anchor_sequences}
1309
- disableHighlighting={isAnyModalOpenMemo}
1310
- onDataChange={(updatedData) => {
1311
- // Direct data change from TranscriptionView (e.g., drag-and-drop)
1312
- // needs to update history
1313
- updateDataWithHistory(updatedData, 'direct data change');
1314
- }}
1315
- reviewMode={reviewMode}
1316
- onRevertCorrection={handleRevertCorrection}
1317
- onEditCorrection={handleEditCorrection}
1318
- onAcceptCorrection={handleAcceptCorrection}
1319
- onShowCorrectionDetail={handleShowCorrectionDetail}
1320
- />
1321
- </Grid>
1322
- <Grid item xs={12} md={6}>
1323
- <MemoizedReferenceView
1324
- referenceSources={data.reference_lyrics}
1325
- anchors={data.anchor_sequences}
1326
- gaps={data.gap_sequences}
1327
- mode={effectiveMode}
1328
- onElementClick={setModalContent}
1329
- onWordClick={handleWordClick}
1330
- flashingType={flashingType}
1331
- highlightInfo={highlightInfo}
1332
- currentSource={currentSource}
1333
- onSourceChange={setCurrentSource}
1334
- corrected_segments={data.corrected_segments}
1335
- corrections={data.corrections}
1336
- onAddLyrics={() => setIsAddLyricsModalOpen(true)}
1337
- />
1338
- </Grid>
1339
- </Grid>
1340
-
1341
- {/* Spacer for sticky footer */}
1342
- {!isReadOnly && apiClient && <Box sx={{ height: 64 }} />}
1343
-
1344
- {/* Sticky footer bar with Preview Video button */}
1345
- {!isReadOnly && apiClient && (
1346
- <Box
1347
- sx={{
1348
- position: 'fixed',
1349
- bottom: 0,
1350
- left: 0,
1351
- right: 0,
1352
- bgcolor: 'background.paper',
1353
- borderTop: 1,
1354
- borderColor: 'divider',
1355
- boxShadow: '0 -2px 10px rgba(0,0,0,0.1)',
1356
- py: 1.5,
1357
- px: 2,
1358
- zIndex: 1100,
1359
- display: 'flex',
1360
- justifyContent: 'center',
1361
- alignItems: 'center',
1362
- gap: 2
1363
- }}
1364
- >
1365
- <Typography
1366
- variant="body2"
1367
- sx={{ color: 'text.secondary' }}
1368
- >
1369
- Lyrics look good?
1370
- </Typography>
1371
- <Button
1372
- variant="contained"
1373
- onClick={handleFinishReview}
1374
- disabled={isReviewComplete}
1375
- endIcon={<OndemandVideo />}
1376
- sx={{
1377
- px: 2.5,
1378
- py: 0.75,
1379
- fontWeight: 500
1380
- }}
1381
- >
1382
- {isReviewComplete ? 'Review Complete' : 'Preview Video'}
1383
- </Button>
1384
- </Box>
1385
- )}
1386
-
1387
-
1388
-
1389
- <EditModal
1390
- open={Boolean(editModalSegment)}
1391
- onClose={() => {
1392
- setEditModalSegment(null)
1393
- handleSetModalSpacebarHandler(undefined)
1394
- }}
1395
- segment={editModalSegment?.segment ?
1396
- timingOffsetMs !== 0 ?
1397
- applyOffsetToSegment(editModalSegment.segment, timingOffsetMs) :
1398
- editModalSegment.segment :
1399
- null}
1400
- segmentIndex={editModalSegment?.index ?? null}
1401
- originalSegment={editModalSegment?.originalSegment ?
1402
- timingOffsetMs !== 0 ?
1403
- applyOffsetToSegment(editModalSegment.originalSegment, timingOffsetMs) :
1404
- editModalSegment.originalSegment :
1405
- null}
1406
- onSave={handleUpdateSegment}
1407
- onDelete={handleDeleteSegment}
1408
- onAddSegment={handleAddSegment}
1409
- onSplitSegment={handleSplitSegment}
1410
- onMergeSegment={handleMergeSegment}
1411
- onPlaySegment={handlePlaySegment}
1412
- currentTime={currentAudioTime}
1413
- setModalSpacebarHandler={handleSetModalSpacebarHandler}
1414
- originalTranscribedSegment={
1415
- editModalSegment?.segment && editModalSegment?.index !== null && originalData.original_segments
1416
- ? (() => {
1417
- const origSegment = originalData.original_segments.find(
1418
- (s: LyricsSegment) => s.id === editModalSegment.segment.id
1419
- ) || null;
1420
-
1421
- return origSegment && timingOffsetMs !== 0
1422
- ? applyOffsetToSegment(origSegment, timingOffsetMs)
1423
- : origSegment;
1424
- })()
1425
- : null
1426
- }
1427
- />
1428
-
1429
- <ReviewChangesModal
1430
- open={isReviewModalOpen}
1431
- onClose={() => setIsReviewModalOpen(false)}
1432
- originalData={originalData}
1433
- updatedData={data}
1434
- onSubmit={handleSubmitToServer}
1435
- apiClient={apiClient}
1436
- setModalSpacebarHandler={handleSetModalSpacebarHandler}
1437
- timingOffsetMs={timingOffsetMs}
1438
- />
1439
-
1440
- <AddLyricsModal
1441
- open={isAddLyricsModalOpen}
1442
- onClose={() => setIsAddLyricsModalOpen(false)}
1443
- onSubmit={handleAddLyrics}
1444
- isSubmitting={isAddingLyrics}
1445
- />
1446
-
1447
- <FindReplaceModal
1448
- open={isFindReplaceModalOpen}
1449
- onClose={() => setIsFindReplaceModalOpen(false)}
1450
- onReplace={handleFindReplace}
1451
- data={data}
1452
- />
1453
-
1454
- <TimingOffsetModal
1455
- open={isTimingOffsetModalOpen}
1456
- onClose={() => setIsTimingOffsetModalOpen(false)}
1457
- currentOffset={timingOffsetMs}
1458
- onApply={handleApplyTimingOffset}
1459
- />
1460
-
1461
- <ReplaceAllLyricsModal
1462
- open={isReplaceAllLyricsModalOpen}
1463
- onClose={() => setIsReplaceAllLyricsModalOpen(false)}
1464
- onSave={handleSaveReplaceAllLyrics}
1465
- onPlaySegment={handlePlaySegment}
1466
- currentTime={currentAudioTime}
1467
- setModalSpacebarHandler={handleSetModalSpacebarHandler}
1468
- existingSegments={data.corrected_segments}
1469
- />
1470
-
1471
- {pendingAnnotation && (
1472
- <CorrectionAnnotationModal
1473
- open={isAnnotationModalOpen}
1474
- onClose={() => {
1475
- setIsAnnotationModalOpen(false)
1476
- setPendingAnnotation(null)
1477
- }}
1478
- onSave={handleSaveAnnotation}
1479
- onSkip={handleSkipAnnotation}
1480
- originalText={pendingAnnotation.originalText}
1481
- correctedText={pendingAnnotation.correctedText}
1482
- wordIdsAffected={pendingAnnotation.wordIdsAffected}
1483
- agenticProposal={undefined} // TODO: Pass agentic proposal if available
1484
- referenceSources={Object.keys(data.reference_lyrics || {})}
1485
- audioHash={audioHash}
1486
- artist={data.metadata?.audio_filepath?.split('/').pop()?.split('.')[0] || 'Unknown'}
1487
- title={data.metadata?.audio_filepath?.split('/').pop()?.split('.')[0] || 'Unknown'}
1488
- sessionId={audioHash} // Use audio hash as session ID for now
1489
- gapId={pendingAnnotation.gapId}
1490
- />
1491
- )}
1492
-
1493
- {selectedCorrection && (
1494
- <CorrectionDetailCard
1495
- open={correctionDetailOpen}
1496
- onClose={() => {
1497
- setCorrectionDetailOpen(false)
1498
- setSelectedCorrection(null)
1499
- }}
1500
- originalWord={selectedCorrection.originalWord}
1501
- correctedWord={selectedCorrection.correctedWord}
1502
- category={selectedCorrection.category}
1503
- confidence={selectedCorrection.confidence}
1504
- reason={selectedCorrection.reason}
1505
- handler={selectedCorrection.handler}
1506
- source={selectedCorrection.source}
1507
- onRevert={() => {
1508
- handleRevertCorrection(selectedCorrection.wordId)
1509
- setCorrectionDetailOpen(false)
1510
- setSelectedCorrection(null)
1511
- }}
1512
- onEdit={() => {
1513
- handleEditCorrection(selectedCorrection.wordId)
1514
- setCorrectionDetailOpen(false)
1515
- setSelectedCorrection(null)
1516
- }}
1517
- onAccept={() => {
1518
- handleAcceptCorrection(selectedCorrection.wordId)
1519
- setCorrectionDetailOpen(false)
1520
- setSelectedCorrection(null)
1521
- }}
1522
- />
1523
- )}
1524
- </Box>
1525
- )
1526
- }