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
@@ -84,6 +84,7 @@ class ReviewServer:
84
84
  self._configure_cors()
85
85
  self._register_routes()
86
86
  self._mount_frontend()
87
+ self._register_spa_routes()
87
88
  # Initialize optional SQLite store for sessions/feedback (legacy)
88
89
  try:
89
90
  default_db = os.path.join(self.output_config.cache_dir, "agentic_feedback.sqlite3")
@@ -135,18 +136,111 @@ class ReviewServer:
135
136
  return JSONResponse(status_code=500, content={"error": "InternalServerError", "message": str(exc), "details": {}})
136
137
 
137
138
  def _mount_frontend(self) -> None:
138
- """Mount the frontend static files."""
139
- current_dir = os.path.dirname(os.path.abspath(__file__))
140
- from lyrics_transcriber.frontend import get_frontend_assets_dir
141
- frontend_dir = get_frontend_assets_dir()
139
+ """Mount the unified Next.js frontend static files."""
140
+ from karaoke_gen.nextjs_frontend import get_nextjs_assets_dir, is_nextjs_frontend_available
142
141
 
143
- if not os.path.exists(frontend_dir):
144
- raise FileNotFoundError(f"Frontend assets not found at {frontend_dir}")
142
+ if not is_nextjs_frontend_available():
143
+ raise FileNotFoundError(
144
+ "Next.js frontend assets not found. Please ensure the frontend is built "
145
+ "and copied to karaoke_gen/nextjs_frontend/out/"
146
+ )
145
147
 
146
- self.app.mount("/", StaticFiles(directory=frontend_dir, html=True), name="frontend")
148
+ frontend_dir = str(get_nextjs_assets_dir())
149
+ self.logger.info(f"Using Next.js frontend from {frontend_dir}")
150
+ self._frontend_dir = frontend_dir # Store for use in route handlers
151
+
152
+ # Mount Next.js static assets directory
153
+ nextjs_static = os.path.join(frontend_dir, "_next")
154
+ if os.path.exists(nextjs_static):
155
+ self.app.mount("/_next", StaticFiles(directory=nextjs_static), name="nextjs_static")
156
+
157
+ # Mount the entire frontend directory for static files, but use html=False
158
+ # so it doesn't serve index.html automatically for directories
159
+ self.app.mount("/static", StaticFiles(directory=frontend_dir), name="frontend_static")
160
+
161
+ def _register_spa_routes(self) -> None:
162
+ """Register SPA fallback routes for Next.js client-side routing."""
163
+ frontend_dir = self._frontend_dir
164
+
165
+ # Root route - serve index.html
166
+ @self.app.get("/")
167
+ async def serve_root():
168
+ index_html = os.path.join(frontend_dir, "index.html")
169
+ if os.path.exists(index_html):
170
+ return FileResponse(index_html, media_type="text/html")
171
+ raise HTTPException(status_code=404, detail="Frontend not found")
172
+
173
+ # Local review route - serve pre-rendered HTML for local mode
174
+ @self.app.get("/app/jobs/local/review")
175
+ async def serve_local_review():
176
+ """Serve pre-rendered local review page with patched chunk loading."""
177
+ from fastapi.responses import HTMLResponse
178
+ local_review_html = os.path.join(frontend_dir, "app", "jobs", "local", "review", "index.html")
179
+ if os.path.exists(local_review_html):
180
+ # Read the HTML and inject the missing chunk script
181
+ # This works around a Turbopack static export issue where the RSC stream
182
+ # references module chunks that don't contain the actual code
183
+ with open(local_review_html, 'r', encoding='utf-8') as f:
184
+ html_content = f.read()
185
+
186
+ # Find the missing chunk that contains JobRouterClient (module 78280)
187
+ # The chunk name is determined at build time, so we need to find it dynamically
188
+ # We look for ",78280," which is the Turbopack module ID pattern
189
+ import glob
190
+ chunks_dir = os.path.join(frontend_dir, "_next", "static", "chunks")
191
+ for chunk_file in glob.glob(os.path.join(chunks_dir, "*.js")):
192
+ chunk_name = os.path.basename(chunk_file)
193
+ # Check if this chunk contains module 78280 (JobRouterClient)
194
+ with open(chunk_file, 'r', encoding='utf-8') as cf:
195
+ chunk_content = cf.read(500) # Module ID is near the start
196
+ if ",78280," in chunk_content:
197
+ # Inject this chunk script if not already present
198
+ script_tag = f'<script src="/_next/static/chunks/{chunk_name}" async=""></script>'
199
+ if chunk_name not in html_content:
200
+ # Insert before closing </head>
201
+ html_content = html_content.replace('</head>', f'{script_tag}</head>')
202
+ break
203
+
204
+ return HTMLResponse(content=html_content, media_type="text/html")
205
+ # Fallback to jobs index
206
+ jobs_html = os.path.join(frontend_dir, "app", "jobs", "index.html")
207
+ if os.path.exists(jobs_html):
208
+ return FileResponse(jobs_html, media_type="text/html")
209
+ raise HTTPException(status_code=404, detail="Review page not found")
210
+
211
+ # Job routes - serve the jobs page HTML for client-side routing
212
+ @self.app.get("/app/jobs/{path:path}")
213
+ async def serve_jobs_routes(path: str):
214
+ """Serve jobs index.html for all /app/jobs/* routes (SPA routing)."""
215
+ jobs_html = os.path.join(frontend_dir, "app", "jobs", "index.html")
216
+ if os.path.exists(jobs_html):
217
+ return FileResponse(jobs_html, media_type="text/html")
218
+ raise HTTPException(status_code=404, detail="Jobs page not found")
219
+
220
+ # Other app routes - serve the app index.html
221
+ @self.app.get("/app/{path:path}")
222
+ async def serve_app_routes(path: str):
223
+ """Serve app index.html for other /app/* routes."""
224
+ app_html = os.path.join(frontend_dir, "app", "index.html")
225
+ if os.path.exists(app_html):
226
+ return FileResponse(app_html, media_type="text/html")
227
+ # Fallback to root index.html
228
+ index_html = os.path.join(frontend_dir, "index.html")
229
+ if os.path.exists(index_html):
230
+ return FileResponse(index_html, media_type="text/html")
231
+ raise HTTPException(status_code=404, detail="Frontend not found")
232
+
233
+ # Favicon
234
+ @self.app.get("/favicon.ico")
235
+ async def serve_favicon():
236
+ favicon_path = os.path.join(frontend_dir, "favicon.ico")
237
+ if os.path.exists(favicon_path):
238
+ return FileResponse(favicon_path)
239
+ raise HTTPException(status_code=404, detail="Not found")
147
240
 
148
241
  def _register_routes(self) -> None:
149
242
  """Register API routes."""
243
+ # Legacy routes (for backward compatibility with old frontend)
150
244
  self.app.add_api_route("/api/correction-data", self.get_correction_data, methods=["GET"])
151
245
  self.app.add_api_route("/api/complete", self.complete_review, methods=["POST"])
152
246
  self.app.add_api_route("/api/preview-video", self.generate_preview_video, methods=["POST"])
@@ -156,6 +250,17 @@ class ReviewServer:
156
250
  self.app.add_api_route("/api/handlers", self.update_handlers, methods=["POST"])
157
251
  self.app.add_api_route("/api/add-lyrics", self.add_lyrics, methods=["POST"])
158
252
 
253
+ # Cloud-compatible routes (for unified Next.js frontend)
254
+ # These use the same handlers but with cloud-style URL patterns
255
+ # Job ID is always "local" for local CLI mode
256
+ self.app.add_api_route("/api/jobs/{job_id}", self.get_local_job, methods=["GET"])
257
+ self.app.add_api_route("/api/jobs/{job_id}/corrections", self.get_correction_data, methods=["GET"])
258
+ self.app.add_api_route("/api/jobs/{job_id}/corrections", self.complete_review, methods=["PUT"])
259
+ self.app.add_api_route("/api/jobs/{job_id}/preview-video", self.generate_preview_video, methods=["POST"])
260
+ self.app.add_api_route("/api/jobs/{job_id}/handlers", self.update_handlers_cloud, methods=["PATCH"])
261
+ self.app.add_api_route("/api/jobs/{job_id}/lyrics", self.add_lyrics, methods=["POST"])
262
+ self.app.add_api_route("/api/jobs/{job_id}/annotations", self.post_annotation, methods=["POST"])
263
+
159
264
  # Agentic AI v1 endpoints (contract-compliant scaffolds)
160
265
  self.app.add_api_route("/api/v1/correction/agentic", self.post_correction_agentic, methods=["POST"])
161
266
  self.app.add_api_route("/api/v1/correction/session/{session_id}", self.get_correction_session_v1, methods=["GET"])
@@ -163,16 +268,56 @@ class ReviewServer:
163
268
  self.app.add_api_route("/api/v1/models", self.get_models_v1, methods=["GET"])
164
269
  self.app.add_api_route("/api/v1/models", self.put_models_v1, methods=["PUT"])
165
270
  self.app.add_api_route("/api/v1/metrics", self.get_metrics_v1, methods=["GET"])
166
-
271
+
167
272
  # Annotation endpoints
168
273
  self.app.add_api_route("/api/v1/annotations", self.post_annotation, methods=["POST"])
169
274
  self.app.add_api_route("/api/v1/annotations/{audio_hash}", self.get_annotations_by_song, methods=["GET"])
170
275
  self.app.add_api_route("/api/v1/annotations/stats", self.get_annotation_stats, methods=["GET"])
171
276
 
277
+ # Tenant config endpoint (returns default config for local mode)
278
+ self.app.add_api_route("/api/tenant/config", self.get_tenant_config, methods=["GET"])
279
+
172
280
  async def get_correction_data(self):
173
281
  """Get the correction data."""
174
282
  return self.correction_result.to_dict()
175
283
 
284
+ async def get_tenant_config(self):
285
+ """Get tenant configuration for local mode.
286
+
287
+ Returns a default tenant config that enables all features
288
+ for local CLI mode.
289
+ """
290
+ return {
291
+ "tenant": None,
292
+ "is_default": True
293
+ }
294
+
295
+ async def get_local_job(self, job_id: str):
296
+ """Get mock job data for local mode.
297
+
298
+ This endpoint returns job-like data that the unified frontend expects.
299
+ In local mode, the job_id is always "local".
300
+ """
301
+ metadata = self.correction_result.metadata or {}
302
+ return {
303
+ "job_id": job_id,
304
+ "status": "awaiting_review",
305
+ "progress": 50,
306
+ "created_at": None,
307
+ "updated_at": None,
308
+ "artist": metadata.get("artist", "Local Artist"),
309
+ "title": metadata.get("title", "Local Title"),
310
+ "user_email": "local@localhost",
311
+ "audio_hash": metadata.get("audio_hash", "local"),
312
+ }
313
+
314
+ async def update_handlers_cloud(self, job_id: str, enabled_handlers: List[str] = Body(...)):
315
+ """Cloud-compatible handler update endpoint (PATCH method).
316
+
317
+ This wraps the existing update_handlers method with cloud-style routing.
318
+ """
319
+ return await self.update_handlers(enabled_handlers)
320
+
176
321
  # ------------------------------
177
322
  # API v1: Agentic AI scaffolds
178
323
  # ------------------------------
@@ -588,24 +733,27 @@ class ReviewServer:
588
733
  server_thread = None
589
734
  sock = None
590
735
 
736
+ # Get port from environment variable (default 8000)
737
+ port = int(os.environ.get("LYRICS_REVIEW_PORT", "8000"))
738
+
591
739
  try:
592
740
  # Check port availability
593
741
  while True:
594
742
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
595
743
  sock.settimeout(1)
596
- if sock.connect_ex(("127.0.0.1", 8000)) == 0:
744
+ if sock.connect_ex(("127.0.0.1", port)) == 0:
597
745
  # Port is in use, get process info
598
746
  process_info = ""
599
747
  if os.name != "nt": # Unix-like systems
600
748
  try:
601
- process_info = os.popen("lsof -i:8000").read().strip()
749
+ process_info = os.popen(f"lsof -i:{port}").read().strip()
602
750
  except:
603
751
  pass
604
752
 
605
753
  self.logger.warning(
606
- f"Port 8000 is in use. Waiting for it to become available...\n"
607
- f"Process using port 8000:\n{process_info}\n"
608
- f"To manually free the port, you can run: lsof -ti:8000 | xargs kill -9"
754
+ f"Port {port} is in use. Waiting for it to become available...\n"
755
+ f"Process using port {port}:\n{process_info}\n"
756
+ f"To manually free the port, you can run: lsof -ti:{port} | xargs kill -9"
609
757
  )
610
758
  sock.close()
611
759
  time.sleep(30)
@@ -614,31 +762,15 @@ class ReviewServer:
614
762
  break
615
763
 
616
764
  # Start server
617
- config = uvicorn.Config(self.app, host="127.0.0.1", port=8000, log_level="error")
765
+ config = uvicorn.Config(self.app, host="127.0.0.1", port=port, log_level="error")
618
766
  server = uvicorn.Server(config)
619
767
  server_thread = Thread(target=server.run, daemon=True)
620
768
  server_thread.start()
621
769
  time.sleep(0.5) # Reduced wait time
622
770
 
623
- # Open browser and wait for completion
624
- base_api_url = "http://localhost:8000/api"
625
- encoded_api_url = urllib.parse.quote(base_api_url, safe="")
626
- audio_hash_param = (
627
- f"&audioHash={self.correction_result.metadata.get('audio_hash', '')}"
628
- if self.correction_result.metadata and "audio_hash" in self.correction_result.metadata
629
- else ""
630
- )
631
-
632
- # Use bundled local frontend by default for local karaoke-gen runs
633
- # Can override with LYRICS_REVIEW_UI_URL env var (e.g., http://localhost:5173 for Vite dev)
634
- review_ui_url = os.environ.get("LYRICS_REVIEW_UI_URL", "local")
635
- if review_ui_url.lower() == "local":
636
- # Use the bundled local frontend served by this FastAPI server
637
- browser_url = f"http://localhost:8000?baseApiUrl={encoded_api_url}{audio_hash_param}"
638
- else:
639
- # Use an external review UI (dev server or hosted)
640
- browser_url = f"{review_ui_url}?baseApiUrl={encoded_api_url}{audio_hash_param}"
641
-
771
+ # Open browser to the Next.js review UI
772
+ # The frontend will automatically detect local mode and skip auth
773
+ browser_url = f"http://localhost:{port}/app/jobs/local/review"
642
774
  self.logger.info(f"Opening review UI: {browser_url}")
643
775
  webbrowser.open(browser_url)
644
776
 
@@ -0,0 +1,214 @@
1
+ """
2
+ OpenTelemetry tracing utilities for lyrics_transcriber library.
3
+
4
+ This module provides tracing utilities that integrate with the backend's
5
+ OpenTelemetry setup. When running in the backend context (Cloud Run),
6
+ traces will export to Google Cloud Trace. When running standalone (CLI),
7
+ tracing is no-op unless explicitly configured.
8
+
9
+ Usage:
10
+ from lyrics_transcriber.utils.tracing import create_span, traced
11
+
12
+ # Context manager for spans
13
+ with create_span("operation_name", {"key": "value"}) as span:
14
+ span.set_attribute("result_count", 42)
15
+ # ... do work ...
16
+
17
+ # Decorator for functions
18
+ @traced("custom_name")
19
+ def my_function():
20
+ pass
21
+ """
22
+ import logging
23
+ from typing import Any, Dict, Optional
24
+ from contextlib import contextmanager
25
+ from functools import wraps
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Try to import OpenTelemetry - gracefully handle missing dependency
30
+ try:
31
+ from opentelemetry import trace
32
+ from opentelemetry.trace import Status, StatusCode, Span
33
+ OTEL_AVAILABLE = True
34
+ except ImportError:
35
+ OTEL_AVAILABLE = False
36
+ trace = None
37
+ Status = None
38
+ StatusCode = None
39
+ Span = None
40
+
41
+
42
+ def get_tracer(name: str = "lyrics_transcriber") -> Any:
43
+ """
44
+ Get a tracer instance.
45
+
46
+ If OpenTelemetry is available and configured (by the backend),
47
+ returns a real tracer. Otherwise returns a no-op tracer.
48
+
49
+ Args:
50
+ name: Tracer name (typically module name)
51
+
52
+ Returns:
53
+ Tracer instance
54
+ """
55
+ if OTEL_AVAILABLE and trace:
56
+ return trace.get_tracer(name)
57
+ return None
58
+
59
+
60
+ @contextmanager
61
+ def create_span(
62
+ name: str,
63
+ attributes: Optional[Dict[str, Any]] = None,
64
+ tracer_name: str = "lyrics_transcriber",
65
+ ):
66
+ """
67
+ Create a traced span as a context manager.
68
+
69
+ If OpenTelemetry is not available or not configured, this is a no-op.
70
+
71
+ Usage:
72
+ with create_span("find_anchors", {"word_count": 500}) as span:
73
+ # ... do work ...
74
+ if span:
75
+ span.set_attribute("anchors_found", 42)
76
+
77
+ Args:
78
+ name: Span name (appears in Cloud Trace)
79
+ attributes: Initial span attributes
80
+ tracer_name: Tracer name to use
81
+
82
+ Yields:
83
+ The active span, or None if tracing is not available
84
+ """
85
+ tracer = get_tracer(tracer_name)
86
+
87
+ if tracer is None:
88
+ yield None
89
+ return
90
+
91
+ with tracer.start_as_current_span(name) as span:
92
+ if attributes:
93
+ for key, value in attributes.items():
94
+ # Only set if value is a valid type
95
+ if isinstance(value, (str, int, float, bool)):
96
+ span.set_attribute(key, value)
97
+ elif isinstance(value, (list, tuple)) and all(isinstance(v, (str, int, float, bool)) for v in value):
98
+ span.set_attribute(key, list(value))
99
+ try:
100
+ yield span
101
+ except Exception as e:
102
+ if OTEL_AVAILABLE and Status and StatusCode:
103
+ span.set_status(Status(StatusCode.ERROR, str(e)))
104
+ span.record_exception(e)
105
+ raise
106
+
107
+
108
+ def traced(
109
+ name: Optional[str] = None,
110
+ attributes: Optional[Dict[str, Any]] = None,
111
+ tracer_name: str = "lyrics_transcriber",
112
+ ):
113
+ """
114
+ Decorator to trace a function.
115
+
116
+ If OpenTelemetry is not available or not configured, this is a no-op decorator.
117
+
118
+ Usage:
119
+ @traced("process_correction")
120
+ def process_correction(job_id: str):
121
+ ...
122
+
123
+ @traced(attributes={"operation": "add-lyrics"})
124
+ async def add_lyrics(job_id: str, source: str):
125
+ ...
126
+
127
+ Args:
128
+ name: Span name (defaults to function name)
129
+ attributes: Static attributes to add to span
130
+ tracer_name: Tracer name to use
131
+ """
132
+ def decorator(func):
133
+ span_name = name or func.__name__
134
+
135
+ @wraps(func)
136
+ def sync_wrapper(*args, **kwargs):
137
+ with create_span(span_name, attributes, tracer_name) as span:
138
+ if span:
139
+ span.set_attribute("function", func.__name__)
140
+ return func(*args, **kwargs)
141
+
142
+ @wraps(func)
143
+ async def async_wrapper(*args, **kwargs):
144
+ with create_span(span_name, attributes, tracer_name) as span:
145
+ if span:
146
+ span.set_attribute("function", func.__name__)
147
+ return await func(*args, **kwargs)
148
+
149
+ # Return appropriate wrapper based on function type
150
+ import asyncio
151
+ if asyncio.iscoroutinefunction(func):
152
+ return async_wrapper
153
+ return sync_wrapper
154
+
155
+ return decorator
156
+
157
+
158
+ def add_span_attribute(key: str, value: Any) -> None:
159
+ """
160
+ Add an attribute to the current span.
161
+
162
+ If no span is active or tracing is not available, this is a no-op.
163
+
164
+ Args:
165
+ key: Attribute name
166
+ value: Attribute value (must be string, int, float, bool, or list thereof)
167
+ """
168
+ if not OTEL_AVAILABLE or not trace:
169
+ return
170
+
171
+ span = trace.get_current_span()
172
+ if span and span.is_recording():
173
+ # Validate type
174
+ if isinstance(value, (str, int, float, bool)):
175
+ span.set_attribute(key, value)
176
+ elif isinstance(value, (list, tuple)) and all(isinstance(v, (str, int, float, bool)) for v in value):
177
+ span.set_attribute(key, list(value))
178
+
179
+
180
+ def add_span_event(name: str, attributes: Optional[Dict[str, Any]] = None) -> None:
181
+ """
182
+ Add an event to the current span.
183
+
184
+ Events are timestamped annotations that appear in the trace timeline.
185
+ If no span is active or tracing is not available, this is a no-op.
186
+
187
+ Args:
188
+ name: Event name
189
+ attributes: Event attributes
190
+ """
191
+ if not OTEL_AVAILABLE or not trace:
192
+ return
193
+
194
+ span = trace.get_current_span()
195
+ if span and span.is_recording():
196
+ span.add_event(name, attributes=attributes or {})
197
+
198
+
199
+ def set_span_error(error: Exception) -> None:
200
+ """
201
+ Mark the current span as errored.
202
+
203
+ If no span is active or tracing is not available, this is a no-op.
204
+
205
+ Args:
206
+ error: The exception that occurred
207
+ """
208
+ if not OTEL_AVAILABLE or not trace or not Status or not StatusCode:
209
+ return
210
+
211
+ span = trace.get_current_span()
212
+ if span and span.is_recording():
213
+ span.set_status(Status(StatusCode.ERROR, str(error)))
214
+ span.record_exception(error)