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

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