@goscribe/server 1.3.4 → 1.6.0

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 (383) hide show
  1. package/.env.example +12 -0
  2. package/.vscode/settings.json +3 -0
  3. package/REFACTOR_NOTES.md +60 -0
  4. package/TESTING_PROMPT.md +225 -0
  5. package/dist/context.d.ts +14 -1
  6. package/dist/context.js +23 -2
  7. package/dist/controllers/admin.controller.d.ts +715 -0
  8. package/dist/controllers/admin.controller.js +9 -0
  9. package/dist/controllers/annotations.controller.d.ts +439 -0
  10. package/dist/controllers/annotations.controller.js +9 -0
  11. package/dist/controllers/app-router.controller.d.ts +3011 -0
  12. package/dist/controllers/app-router.controller.js +38 -0
  13. package/dist/controllers/app-router.controller.test.d.ts +1 -0
  14. package/dist/controllers/app-router.controller.test.js +36 -0
  15. package/dist/controllers/auth.controller.d.ts +323 -0
  16. package/dist/controllers/auth.controller.js +9 -0
  17. package/dist/controllers/base.controller.d.ts +4 -0
  18. package/dist/controllers/base.controller.js +5 -0
  19. package/dist/controllers/chat.controller.d.ts +341 -0
  20. package/dist/controllers/chat.controller.js +9 -0
  21. package/dist/controllers/copilot.controller.d.ts +397 -0
  22. package/dist/controllers/copilot.controller.js +9 -0
  23. package/dist/controllers/flashcards.controller.d.ts +651 -0
  24. package/dist/controllers/flashcards.controller.js +9 -0
  25. package/dist/controllers/members.controller.d.ts +339 -0
  26. package/dist/controllers/members.controller.js +9 -0
  27. package/dist/controllers/notifications.controller.d.ts +199 -0
  28. package/dist/controllers/notifications.controller.js +9 -0
  29. package/dist/controllers/payment.controller.d.ts +181 -0
  30. package/dist/controllers/payment.controller.js +9 -0
  31. package/dist/controllers/podcast.controller.d.ts +575 -0
  32. package/dist/controllers/podcast.controller.js +9 -0
  33. package/dist/controllers/router-module.controller.d.ts +5 -0
  34. package/dist/controllers/router-module.controller.js +6 -0
  35. package/dist/controllers/studyguide.controller.d.ts +73 -0
  36. package/dist/controllers/studyguide.controller.js +9 -0
  37. package/dist/controllers/worksheets.controller.d.ts +829 -0
  38. package/dist/controllers/worksheets.controller.js +9 -0
  39. package/dist/controllers/workspace.controller.d.ts +1207 -0
  40. package/dist/controllers/workspace.controller.js +9 -0
  41. package/dist/lib/activity_human_description.test.js +16 -15
  42. package/dist/lib/activity_log_service.test.js +28 -23
  43. package/dist/lib/ai/config.d.ts +20 -0
  44. package/dist/lib/ai/config.js +31 -0
  45. package/dist/lib/ai/embedding-client.d.ts +8 -0
  46. package/dist/lib/ai/embedding-client.js +30 -0
  47. package/dist/lib/ai/index.d.ts +48 -0
  48. package/dist/lib/ai/index.js +29 -0
  49. package/dist/lib/ai/inference-backend/client.d.ts +28 -0
  50. package/dist/lib/ai/inference-backend/client.js +301 -0
  51. package/dist/lib/ai/inference-backend/mocks.d.ts +12 -0
  52. package/dist/lib/ai/inference-backend/mocks.js +133 -0
  53. package/dist/lib/ai/inference-backend/types.d.ts +44 -0
  54. package/dist/lib/ai/inference-backend/types.js +1 -0
  55. package/dist/lib/ai/json-parse.d.ts +2 -0
  56. package/dist/lib/ai/json-parse.js +34 -0
  57. package/dist/lib/ai/llm-client.d.ts +7 -0
  58. package/dist/lib/ai/llm-client.js +36 -0
  59. package/dist/lib/ai/mock.d.ts +2 -0
  60. package/dist/lib/ai/mock.js +10 -0
  61. package/dist/lib/ai/types.d.ts +9 -0
  62. package/dist/lib/ai/types.js +1 -0
  63. package/dist/lib/chunking.d.ts +19 -0
  64. package/dist/lib/chunking.js +47 -0
  65. package/dist/lib/curated-kb-seed.d.ts +12 -0
  66. package/dist/lib/curated-kb-seed.js +155 -0
  67. package/dist/lib/email.js +67 -108
  68. package/dist/lib/embeddings.d.ts +2 -0
  69. package/dist/lib/embeddings.js +1 -0
  70. package/dist/lib/ensure-curated-kb-catalog.d.ts +6 -0
  71. package/dist/lib/ensure-curated-kb-catalog.js +53 -0
  72. package/dist/lib/env.d.ts +1 -5
  73. package/dist/lib/env.js +2 -7
  74. package/dist/lib/inference.d.ts +1 -8
  75. package/dist/lib/inference.js +1 -19
  76. package/dist/lib/kb-meta.d.ts +8 -0
  77. package/dist/lib/kb-meta.js +77 -0
  78. package/dist/lib/note-text.d.ts +1 -0
  79. package/dist/lib/note-text.js +47 -0
  80. package/dist/lib/notification-service.test.js +37 -36
  81. package/dist/lib/pdf.d.ts +11 -0
  82. package/dist/lib/pdf.js +11 -0
  83. package/dist/lib/usage_service.d.ts +2 -1
  84. package/dist/lib/usage_service.js +30 -12
  85. package/dist/lib/worksheet-generation.js +4 -4
  86. package/dist/lib/worksheet-generation.test.js +32 -17
  87. package/dist/lib/workspace-kb.d.ts +5 -0
  88. package/dist/lib/workspace-kb.js +7 -0
  89. package/dist/models/controller-context.model.d.ts +8 -0
  90. package/dist/models/controller-context.model.js +1 -0
  91. package/dist/repositories/artifact.repository.d.ts +60 -0
  92. package/dist/repositories/artifact.repository.js +40 -0
  93. package/dist/repositories/base.repository.d.ts +14 -0
  94. package/dist/repositories/base.repository.js +14 -0
  95. package/dist/repositories/invitation.repository.d.ts +94 -0
  96. package/dist/repositories/invitation.repository.js +44 -0
  97. package/dist/repositories/notification.repository.d.ts +72 -0
  98. package/dist/repositories/notification.repository.js +44 -0
  99. package/dist/repositories/router-module.repository.d.ts +10 -0
  100. package/dist/repositories/router-module.repository.js +14 -0
  101. package/dist/repositories/user.repository.d.ts +74 -0
  102. package/dist/repositories/user.repository.js +37 -0
  103. package/dist/repositories/workspace-member.repository.d.ts +31 -0
  104. package/dist/repositories/workspace-member.repository.js +31 -0
  105. package/dist/repositories/workspace.repository.d.ts +97 -0
  106. package/dist/repositories/workspace.repository.js +79 -0
  107. package/dist/routers/_app.d.ts +566 -111
  108. package/dist/routers/_app.js +4 -0
  109. package/dist/routers/admin.d.ts +0 -4
  110. package/dist/routers/admin.js +21 -549
  111. package/dist/routers/annotations.js +12 -170
  112. package/dist/routers/artifactVersions.d.ts +65 -0
  113. package/dist/routers/artifactVersions.js +14 -0
  114. package/dist/routers/auth.d.ts +0 -6
  115. package/dist/routers/auth.js +37 -422
  116. package/dist/routers/chat.js +15 -229
  117. package/dist/routers/copilot.d.ts +14 -13
  118. package/dist/routers/copilot.js +13 -532
  119. package/dist/routers/flashcards.d.ts +17 -6
  120. package/dist/routers/flashcards.js +23 -349
  121. package/dist/routers/knowledgeBase.d.ts +421 -0
  122. package/dist/routers/knowledgeBase.js +118 -0
  123. package/dist/routers/members.d.ts +0 -41
  124. package/dist/routers/members.js +22 -710
  125. package/dist/routers/notes.d.ts +94 -0
  126. package/dist/routers/notes.js +37 -0
  127. package/dist/routers/notifications.js +7 -109
  128. package/dist/routers/payment.d.ts +2 -12
  129. package/dist/routers/payment.js +11 -393
  130. package/dist/routers/podcast.d.ts +1 -1
  131. package/dist/routers/podcast.js +11 -784
  132. package/dist/routers/studyguide.js +3 -129
  133. package/dist/routers/worksheets.d.ts +29 -14
  134. package/dist/routers/worksheets.js +49 -628
  135. package/dist/routers/workspace.d.ts +27 -71
  136. package/dist/routers/workspace.js +29 -922
  137. package/dist/scripts/purge-deleted-users.js +2 -2
  138. package/dist/server.js +10 -3
  139. package/dist/services/activity/activity-human-description.service.d.ts +13 -0
  140. package/dist/services/activity/activity-human-description.service.js +221 -0
  141. package/dist/services/activity/activity-human-description.service.test.d.ts +1 -0
  142. package/dist/services/activity/activity-human-description.service.test.js +16 -0
  143. package/dist/services/activity/activity-log.service.d.ts +87 -0
  144. package/dist/services/activity/activity-log.service.js +276 -0
  145. package/dist/services/activity/activity-log.service.test.d.ts +1 -0
  146. package/dist/services/activity/activity-log.service.test.js +27 -0
  147. package/dist/services/activity-human-description.service.d.ts +13 -0
  148. package/dist/services/activity-human-description.service.js +221 -0
  149. package/dist/services/activity-human-description.service.test.d.ts +1 -0
  150. package/dist/services/activity-human-description.service.test.js +16 -0
  151. package/dist/services/activity-log.service.d.ts +87 -0
  152. package/dist/services/activity-log.service.js +276 -0
  153. package/dist/services/activity-log.service.test.d.ts +1 -0
  154. package/dist/services/activity-log.service.test.js +27 -0
  155. package/dist/services/admin/admin.service.d.ts +270 -0
  156. package/dist/services/admin/admin.service.js +476 -0
  157. package/dist/services/admin.service.d.ts +270 -0
  158. package/dist/services/admin.service.js +476 -0
  159. package/dist/services/ai/ai-session.service.d.ts +5 -0
  160. package/dist/services/ai/ai-session.service.js +4 -0
  161. package/dist/services/ai-session.service.d.ts +60 -0
  162. package/dist/services/ai-session.service.js +561 -0
  163. package/dist/services/annotation.service.d.ts +177 -0
  164. package/dist/services/annotation.service.js +154 -0
  165. package/dist/services/artifact-notification.service.d.ts +14 -0
  166. package/dist/services/artifact-notification.service.js +20 -0
  167. package/dist/services/artifact-version.service.d.ts +38 -0
  168. package/dist/services/artifact-version.service.js +129 -0
  169. package/dist/services/artifacts/annotation.service.d.ts +177 -0
  170. package/dist/services/artifacts/annotation.service.js +154 -0
  171. package/dist/services/artifacts/artifact-version.service.d.ts +38 -0
  172. package/dist/services/artifacts/artifact-version.service.js +129 -0
  173. package/dist/services/artifacts/chat.service.d.ts +127 -0
  174. package/dist/services/artifacts/chat.service.js +182 -0
  175. package/dist/services/artifacts/study-guide.service.d.ts +18 -0
  176. package/dist/services/artifacts/study-guide.service.js +65 -0
  177. package/dist/services/auth/auth.service.d.ts +94 -0
  178. package/dist/services/auth/auth.service.js +368 -0
  179. package/dist/services/auth.service.d.ts +94 -0
  180. package/dist/services/auth.service.js +368 -0
  181. package/dist/services/base.service.d.ts +14 -0
  182. package/dist/services/base.service.js +14 -0
  183. package/dist/services/billing/payment.service.d.ts +44 -0
  184. package/dist/services/billing/payment.service.js +365 -0
  185. package/dist/services/billing/subscription.service.d.ts +37 -0
  186. package/dist/services/billing/subscription.service.js +654 -0
  187. package/dist/services/billing/usage.service.d.ts +47 -0
  188. package/dist/services/billing/usage.service.js +149 -0
  189. package/dist/services/chat.service.d.ts +127 -0
  190. package/dist/services/chat.service.js +182 -0
  191. package/dist/services/content/copilot.service.d.ts +113 -0
  192. package/dist/services/content/copilot.service.js +439 -0
  193. package/dist/services/content/flashcard-progress.service.d.ts +159 -0
  194. package/dist/services/content/flashcard-progress.service.js +432 -0
  195. package/dist/services/content/flashcard.service.d.ts +184 -0
  196. package/dist/services/content/flashcard.service.js +339 -0
  197. package/dist/services/content/media-analysis.service.d.ts +23 -0
  198. package/dist/services/content/media-analysis.service.js +404 -0
  199. package/dist/services/content/podcast.service.d.ts +267 -0
  200. package/dist/services/content/podcast.service.js +653 -0
  201. package/dist/services/content/worksheet-content.service.d.ts +37 -0
  202. package/dist/services/content/worksheet-content.service.js +84 -0
  203. package/dist/services/content/worksheet-content.service.test.d.ts +1 -0
  204. package/dist/services/content/worksheet-content.service.test.js +69 -0
  205. package/dist/services/content/worksheet-generation.service.d.ts +91 -0
  206. package/dist/services/content/worksheet-generation.service.js +95 -0
  207. package/dist/services/content/worksheet-generation.service.test.d.ts +1 -0
  208. package/dist/services/content/worksheet-generation.service.test.js +20 -0
  209. package/dist/services/content/worksheet.service.d.ts +347 -0
  210. package/dist/services/content/worksheet.service.js +599 -0
  211. package/dist/services/copilot.service.d.ts +116 -0
  212. package/dist/services/copilot.service.js +447 -0
  213. package/dist/services/flashcard-progress.service.d.ts +2 -2
  214. package/dist/services/flashcard-progress.service.js +3 -2
  215. package/dist/services/flashcard.service.d.ts +140 -0
  216. package/dist/services/flashcard.service.js +325 -0
  217. package/dist/services/invitation.service.d.ts +66 -0
  218. package/dist/services/invitation.service.js +348 -0
  219. package/dist/services/knowledge/knowledge-base.service.d.ts +316 -0
  220. package/dist/services/knowledge/knowledge-base.service.js +544 -0
  221. package/dist/services/knowledge-base.service.d.ts +316 -0
  222. package/dist/services/knowledge-base.service.js +536 -0
  223. package/dist/services/media-analysis.service.d.ts +23 -0
  224. package/dist/services/media-analysis.service.js +384 -0
  225. package/dist/services/member.service.d.ts +36 -0
  226. package/dist/services/member.service.js +193 -0
  227. package/dist/services/members/invitation.service.d.ts +66 -0
  228. package/dist/services/members/invitation.service.js +348 -0
  229. package/dist/services/members/member.service.d.ts +36 -0
  230. package/dist/services/members/member.service.js +193 -0
  231. package/dist/services/note.service.d.ts +55 -0
  232. package/dist/services/note.service.js +111 -0
  233. package/dist/services/notification.service.d.ts +214 -0
  234. package/dist/services/notification.service.js +550 -0
  235. package/dist/services/notification.service.test.d.ts +1 -0
  236. package/dist/services/notification.service.test.js +87 -0
  237. package/dist/services/notifications/notification.service.d.ts +214 -0
  238. package/dist/services/notifications/notification.service.js +550 -0
  239. package/dist/services/notifications/notification.service.test.d.ts +1 -0
  240. package/dist/services/notifications/notification.service.test.js +87 -0
  241. package/dist/services/payment.service.d.ts +55 -0
  242. package/dist/services/payment.service.js +368 -0
  243. package/dist/services/podcast.service.d.ts +267 -0
  244. package/dist/services/podcast.service.js +654 -0
  245. package/dist/services/router-module.service.d.ts +7 -0
  246. package/dist/services/router-module.service.js +10 -0
  247. package/dist/services/study-guide.service.d.ts +18 -0
  248. package/dist/services/study-guide.service.js +65 -0
  249. package/dist/services/subscription.service.d.ts +37 -0
  250. package/dist/services/subscription.service.js +654 -0
  251. package/dist/services/usage-limit-policy.service.d.ts +12 -0
  252. package/dist/services/usage-limit-policy.service.js +22 -0
  253. package/dist/services/usage-limit-policy.service.test.d.ts +1 -0
  254. package/dist/services/usage-limit-policy.service.test.js +46 -0
  255. package/dist/services/usage.service.d.ts +27 -0
  256. package/dist/services/usage.service.js +77 -0
  257. package/dist/services/worksheet-content.service.d.ts +42 -0
  258. package/dist/services/worksheet-content.service.js +84 -0
  259. package/dist/services/worksheet-content.service.test.d.ts +1 -0
  260. package/dist/services/worksheet-content.service.test.js +69 -0
  261. package/dist/services/worksheet-generation.service.d.ts +91 -0
  262. package/dist/services/worksheet-generation.service.js +95 -0
  263. package/dist/services/worksheet-generation.service.test.d.ts +1 -0
  264. package/dist/services/worksheet-generation.service.test.js +20 -0
  265. package/dist/services/worksheet.service.d.ts +385 -0
  266. package/dist/services/worksheet.service.js +596 -0
  267. package/dist/services/workspace/workspace-analytics.service.d.ts +24 -0
  268. package/dist/services/workspace/workspace-analytics.service.js +95 -0
  269. package/dist/services/workspace/workspace-kb.service.d.ts +40 -0
  270. package/dist/services/workspace/workspace-kb.service.js +184 -0
  271. package/dist/services/workspace/workspace.service.d.ts +263 -0
  272. package/dist/services/workspace/workspace.service.js +401 -0
  273. package/dist/services/workspace-analytics.service.d.ts +24 -0
  274. package/dist/services/workspace-analytics.service.js +95 -0
  275. package/dist/services/workspace-kb.service.d.ts +40 -0
  276. package/dist/services/workspace-kb.service.js +184 -0
  277. package/dist/services/workspace-progress.service.d.ts +27 -0
  278. package/dist/services/workspace-progress.service.js +56 -0
  279. package/dist/services/workspace-progress.service.test.d.ts +1 -0
  280. package/dist/services/workspace-progress.service.test.js +49 -0
  281. package/dist/services/workspace.service.d.ts +307 -0
  282. package/dist/services/workspace.service.js +390 -0
  283. package/dist/trpc.d.ts +12 -4
  284. package/dist/trpc.js +7 -13
  285. package/package.json +5 -6
  286. package/prisma/migrations/20260509000001_add_knowledge_base/migration.sql +99 -0
  287. package/prisma/migrations/20260509000002_curate_knowledge_base/migration.sql +52 -0
  288. package/prisma/migrations/20260522000000_add_notes/migration.sql +27 -0
  289. package/prisma/migrations/20260524000000_remove_notes/migration.sql +3 -0
  290. package/prisma/schema.prisma +150 -48
  291. package/prisma/seed.mjs +67 -0
  292. package/scripts/debug/README.md +4 -0
  293. package/src/README.md +63 -0
  294. package/src/context.ts +33 -3
  295. package/src/lib/ai/config.ts +34 -0
  296. package/src/lib/ai/embedding-client.ts +47 -0
  297. package/src/lib/ai/index.ts +65 -0
  298. package/src/lib/ai/inference-backend/client.ts +479 -0
  299. package/src/lib/ai/inference-backend/mocks.ts +171 -0
  300. package/src/lib/ai/inference-backend/types.ts +50 -0
  301. package/src/lib/ai/json-parse.ts +35 -0
  302. package/src/lib/ai/llm-client.ts +54 -0
  303. package/src/lib/ai/mock.ts +12 -0
  304. package/src/lib/ai/types.ts +11 -0
  305. package/src/lib/chunking.ts +81 -0
  306. package/src/lib/curated-kb-seed.ts +164 -0
  307. package/src/lib/email.ts +77 -115
  308. package/src/lib/embeddings.ts +9 -0
  309. package/src/lib/ensure-curated-kb-catalog.ts +60 -0
  310. package/src/lib/env.ts +2 -7
  311. package/src/lib/inference.ts +1 -21
  312. package/src/lib/kb-meta.ts +81 -0
  313. package/src/lib/pdf.ts +23 -0
  314. package/src/lib/workspace-kb.ts +7 -0
  315. package/src/repositories/artifact.repository.ts +55 -0
  316. package/src/repositories/base.repository.ts +19 -0
  317. package/src/repositories/invitation.repository.ts +53 -0
  318. package/src/repositories/notification.repository.ts +53 -0
  319. package/src/repositories/user.repository.ts +44 -0
  320. package/src/repositories/workspace-member.repository.ts +38 -0
  321. package/src/repositories/workspace.repository.ts +89 -0
  322. package/src/routers/_app.ts +4 -0
  323. package/src/routers/admin.ts +124 -692
  324. package/src/routers/annotations.ts +25 -203
  325. package/src/routers/artifactVersions.ts +32 -0
  326. package/src/routers/auth.ts +82 -520
  327. package/src/routers/chat.ts +42 -245
  328. package/src/routers/copilot.ts +41 -666
  329. package/src/routers/flashcards.ts +108 -404
  330. package/src/routers/knowledgeBase.ts +216 -0
  331. package/src/routers/members.ts +60 -782
  332. package/src/routers/notifications.ts +15 -117
  333. package/src/routers/payment.ts +37 -446
  334. package/src/routers/podcast.ts +36 -898
  335. package/src/routers/studyguide.ts +5 -144
  336. package/src/routers/worksheets.ts +171 -735
  337. package/src/routers/workspace.ts +141 -1108
  338. package/src/scripts/purge-deleted-users.ts +2 -2
  339. package/src/server.ts +10 -3
  340. package/src/{lib/activity_human_description.test.ts → services/activity/activity-human-description.service.test.ts} +1 -1
  341. package/src/{lib/activity_log_service.test.ts → services/activity/activity-log.service.test.ts} +1 -1
  342. package/src/{lib/activity_log_service.ts → services/activity/activity-log.service.ts} +2 -2
  343. package/src/services/admin/admin.service.ts +612 -0
  344. package/src/services/ai/ai-session.service.ts +5 -0
  345. package/src/services/artifacts/annotation.service.ts +189 -0
  346. package/src/services/artifacts/artifact-version.service.ts +151 -0
  347. package/src/services/artifacts/chat.service.ts +197 -0
  348. package/src/services/artifacts/study-guide.service.ts +72 -0
  349. package/src/services/auth/auth.service.ts +473 -0
  350. package/src/services/base.service.ts +19 -0
  351. package/src/services/billing/payment.service.ts +433 -0
  352. package/src/{lib/subscription_service.ts → services/billing/subscription.service.ts} +5 -5
  353. package/src/services/billing/usage.service.ts +207 -0
  354. package/src/services/content/copilot.service.ts +587 -0
  355. package/src/services/{flashcard-progress.service.ts → content/flashcard-progress.service.ts} +18 -12
  356. package/src/services/content/flashcard.service.ts +417 -0
  357. package/src/services/content/media-analysis.service.ts +561 -0
  358. package/src/services/content/podcast.service.ts +777 -0
  359. package/src/services/content/worksheet-content.service.test.ts +83 -0
  360. package/src/services/content/worksheet-content.service.ts +117 -0
  361. package/src/{lib/worksheet-generation.test.ts → services/content/worksheet-generation.service.test.ts} +3 -3
  362. package/src/services/content/worksheet.service.ts +751 -0
  363. package/src/services/knowledge/knowledge-base.service.ts +705 -0
  364. package/src/services/members/invitation.service.ts +427 -0
  365. package/src/services/members/member.service.ts +241 -0
  366. package/src/{lib/notification-service.test.ts → services/notifications/notification.service.test.ts} +2 -2
  367. package/src/{lib/notification-service.ts → services/notifications/notification.service.ts} +102 -1
  368. package/src/services/workspace/workspace-analytics.service.ts +107 -0
  369. package/src/services/workspace/workspace-kb.service.ts +273 -0
  370. package/src/services/workspace/workspace.service.ts +488 -0
  371. package/src/trpc.ts +7 -15
  372. package/src/lib/ai-session.ts +0 -704
  373. package/src/lib/usage_service.ts +0 -74
  374. package/src/lib/workspace-access.ts +0 -13
  375. /package/{check-difficulty.cjs → scripts/debug/check-difficulty.cjs} +0 -0
  376. /package/{check-questions.cjs → scripts/debug/check-questions.cjs} +0 -0
  377. /package/{db-summary.cjs → scripts/debug/db-summary.cjs} +0 -0
  378. /package/{mcq-test.cjs → scripts/debug/mcq-test.cjs} +0 -0
  379. /package/{test-generate.js → scripts/debug/test-generate.js} +0 -0
  380. /package/{test-ratio.cjs → scripts/debug/test-ratio.cjs} +0 -0
  381. /package/{zod-test.cjs → scripts/debug/zod-test.cjs} +0 -0
  382. /package/src/{lib/activity_human_description.ts → services/activity/activity-human-description.service.ts} +0 -0
  383. /package/src/{lib/worksheet-generation.ts → services/content/worksheet-generation.service.ts} +0 -0
@@ -1,923 +1,61 @@
1
1
  import { z } from 'zod';
2
- import { TRPCError } from '@trpc/server';
3
- import { router, authedProcedure, verifiedProcedure,limitedProcedure } from '../trpc.js';
4
- import { v4 as uuidv4 } from 'uuid';
5
- import inference from '../lib/inference.js';
6
- import { uploadToSupabase, generateSignedUrl, deleteFromSupabase } from '../lib/storage.js';
7
- import PusherService from '../lib/pusher.js';
8
- import { aiSessionService } from '../lib/ai-session.js';
9
- import { ArtifactType } from '../lib/constants.js';
10
- import { workspaceAccessFilter } from '../lib/workspace-access.js';
11
- import { logger } from '../lib/logger.js';
12
- import { notifyArtifactFailed, notifyArtifactReady } from '../lib/notification-service.js';
13
-
14
- // Podcast segment schema
15
- const podcastSegmentSchema = z.object({
16
- id: z.string(),
17
- title: z.string(),
18
- content: z.string(),
19
- startTime: z.number(), // in seconds
20
- duration: z.number(), // in seconds
21
- keyPoints: z.array(z.string()),
22
- order: z.number().int(),
23
- audioUrl: z.string().optional(),
24
- objectKey: z.string().optional(), // Supabase Storage object key
25
- });
26
-
27
- // Speaker schema
28
- const speakerSchema = z.object({
29
- id: z.string(),
30
- role: z.enum(['host', 'guest', 'expert']),
31
- name: z.string().optional(),
32
- });
33
-
34
- // Podcast creation input schema
35
- const podcastInputSchema = z.object({
36
- title: z.string(),
37
- description: z.string().optional(),
38
- userPrompt: z.string(),
39
- speakers: z.array(speakerSchema).min(1).default([{ id: 'pNInz6obpgDQGcFmaJgB', role: 'host' }]),
40
- speed: z.number().min(0.25).max(4.0).default(1.0),
41
- generateIntro: z.boolean().default(true),
42
- generateOutro: z.boolean().default(true),
43
- segmentByTopics: z.boolean().default(true),
44
- });
45
-
46
- // Podcast metadata schema for version data (segments stored separately in database)
47
- const podcastMetadataSchema = z.object({
48
- title: z.string(),
49
- description: z.string().optional(),
50
- totalDuration: z.number(),
51
- speakers: z.array(speakerSchema),
52
- summary: z.object({
53
- executiveSummary: z.string(),
54
- learningObjectives: z.array(z.string()),
55
- keyConcepts: z.array(z.string()),
56
- followUpActions: z.array(z.string()),
57
- targetAudience: z.string(),
58
- prerequisites: z.array(z.string()),
59
- tags: z.array(z.string()),
60
- }),
61
- generatedAt: z.string(),
62
- });
2
+ import { router, authedProcedure, limitedProcedure } from '../trpc.js';
3
+ import { PodcastService, podcastInputSchema } from '../services/content/podcast.service.js';
63
4
 
64
5
  export const podcast = router({
65
- // List all podcast episodes for a workspace
66
6
  listEpisodes: authedProcedure
67
7
  .input(z.object({ workspaceId: z.string() }))
68
- .query(async ({ ctx, input }) => {
69
- const workspace = await ctx.db.workspace.findFirst({
70
- where: { id: input.workspaceId, ownerId: ctx.session.user.id },
71
- });
72
-
73
- // Check if workspace exists
74
-
75
- if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
76
-
77
- const artifacts = await ctx.db.artifact.findMany({
78
- where: {
79
- workspaceId: input.workspaceId,
80
- type: ArtifactType.PODCAST_EPISODE
81
- },
82
- include: {
83
- versions: {
84
- orderBy: { version: 'desc' },
85
- take: 1, // Get only the latest version
86
- },
87
- podcastSegments: {
88
- orderBy: { order: 'asc' },
89
- },
90
- },
91
- orderBy: { updatedAt: 'desc' },
92
- });
93
-
94
- logger.debug(`Found ${artifacts.length} podcast artifacts`);
95
- artifacts.forEach((artifact, i) => {
96
- logger.debug(` Podcast ${i + 1}: "${artifact.title}" - ${artifact.podcastSegments.length} segments`);
97
- });
98
-
99
- // Transform to include segments with fresh signed URLs
100
-
8
+ .query(({ ctx, input }) =>
9
+ new PodcastService(ctx.db).listEpisodes(ctx.session.user.id, input.workspaceId),
10
+ ),
101
11
 
102
- const episodesWithUrls = await Promise.all(
103
- artifacts.map(async (artifact) => {
104
- const latestVersion = artifact.versions[0];
105
- let objectUrl = null;
106
- if (artifact.imageObjectKey) {
107
- objectUrl = await generateSignedUrl(artifact.imageObjectKey, 24);
108
- }
109
-
110
- // Generate fresh signed URLs for all segments
111
- const segmentsWithUrls = await Promise.all(
112
- artifact.podcastSegments.map(async (segment) => {
113
- if (segment.objectKey) {
114
- try {
115
- const signedUrl = await generateSignedUrl(segment.objectKey, 24); // 24 hours
116
- return {
117
- id: segment.id,
118
- title: segment.title,
119
- audioUrl: signedUrl,
120
- objectKey: segment.objectKey,
121
- startTime: segment.startTime,
122
- duration: segment.duration,
123
- order: segment.order,
124
- };
125
- } catch (error) {
126
- logger.error(`Failed to generate signed URL for segment ${segment.id}:`, error);
127
- return {
128
- id: segment.id,
129
- title: segment.title,
130
- audioUrl: null,
131
- objectKey: segment.objectKey,
132
- startTime: segment.startTime,
133
- duration: segment.duration,
134
- order: segment.order,
135
- };
136
- }
137
- }
138
- return {
139
- id: segment.id,
140
- title: segment.title,
141
- audioUrl: null,
142
- objectKey: segment.objectKey,
143
- startTime: segment.startTime,
144
- duration: segment.duration,
145
- order: segment.order,
146
- };
147
- })
148
- );
149
-
150
- // Parse metadata from latest version if available
151
- let metadata = null;
152
- if (latestVersion) {
153
- try {
154
- logger.debug(JSON.stringify(latestVersion.data))
155
- metadata = podcastMetadataSchema.parse(latestVersion.data);
156
- } catch (error) {
157
- logger.error('Failed to parse podcast metadata:', error);
158
- }
159
- }
160
-
161
- return {
162
- id: artifact.id,
163
- title: metadata?.title || artifact.title || 'Untitled Episode',
164
- description: metadata?.description || artifact.description || null,
165
- metadata: metadata,
166
- imageUrl: objectUrl,
167
- segments: segmentsWithUrls,
168
- createdAt: artifact.createdAt,
169
- updatedAt: artifact.updatedAt,
170
- workspaceId: artifact.workspaceId,
171
- generating: artifact.generating,
172
- generatingMetadata: artifact.generatingMetadata,
173
- type: artifact.type,
174
- createdById: artifact.createdById,
175
- isArchived: artifact.isArchived,
176
- };
177
- })
178
- );
179
-
180
- return episodesWithUrls;
181
- }),
182
-
183
- // Get a specific podcast episode with segments and signed URLs
184
12
  getEpisode: authedProcedure
185
13
  .input(z.object({ episodeId: z.string() }))
186
- .query(async ({ ctx, input }) => {
187
- const episode = await ctx.db.artifact.findFirst({
188
- where: {
189
- id: input.episodeId,
190
- type: ArtifactType.PODCAST_EPISODE,
191
- workspace: workspaceAccessFilter(ctx.session.user.id)
192
- },
193
- include: {
194
- versions: {
195
- orderBy: { version: 'desc' },
196
- take: 1,
197
- },
198
- podcastSegments: {
199
- orderBy: { order: 'asc' },
200
- },
201
- },
202
- });
203
-
204
- logger.debug(JSON.stringify(episode))
205
-
206
- if (!episode) throw new TRPCError({ code: 'NOT_FOUND' });
207
-
208
- const latestVersion = episode.versions[0];
209
- if (!latestVersion) throw new TRPCError({ code: 'NOT_FOUND', message: 'No version found' });
210
-
211
- logger.debug(JSON.stringify(latestVersion))
212
- try {
213
- const metadata = podcastMetadataSchema.parse(latestVersion.data);
214
- } catch (error) {
215
- logger.error('Failed to parse podcast metadata:', error);
216
- }
217
- const metadata = podcastMetadataSchema.parse(latestVersion.data);
218
-
219
- const imageUrl = episode.imageObjectKey ? await generateSignedUrl(episode.imageObjectKey, 24) : null;
220
-
221
-
222
- // Generate fresh signed URLs for all segments
223
- const segmentsWithUrls = await Promise.all(
224
- episode.podcastSegments.map(async (segment) => {
225
- if (segment.objectKey) {
226
- try {
227
- const signedUrl = await generateSignedUrl(segment.objectKey, 24); // 24 hours
228
- return {
229
- id: segment.id,
230
- title: segment.title,
231
- content: segment.content,
232
- audioUrl: signedUrl,
233
- objectKey: segment.objectKey,
234
- startTime: segment.startTime,
235
- duration: segment.duration,
236
- keyPoints: segment.keyPoints,
237
- order: segment.order,
238
- };
239
- } catch (error) {
240
- logger.error(`Failed to generate signed URL for segment ${segment.id}:`, error);
241
- return {
242
- id: segment.id,
243
- title: segment.title,
244
- content: segment.content,
245
- audioUrl: null,
246
- objectKey: segment.objectKey,
247
- startTime: segment.startTime,
248
- duration: segment.duration,
249
- keyPoints: segment.keyPoints,
250
- order: segment.order,
251
- };
252
- }
253
- }
254
- return {
255
- id: segment.id,
256
- title: segment.title,
257
- content: segment.content,
258
- audioUrl: null,
259
- objectKey: segment.objectKey,
260
- startTime: segment.startTime,
261
- duration: segment.duration,
262
- keyPoints: segment.keyPoints,
263
- order: segment.order,
264
- };
265
- })
266
- );
267
-
268
- return {
269
- id: episode.id,
270
- title: metadata.title, // Use title from version metadata
271
- description: metadata.description, // Use description from version metadata
272
- metadata,
273
- imageUrl: imageUrl,
274
- segments: segmentsWithUrls,
275
- content: latestVersion.content, // transcript
276
- createdAt: episode.createdAt,
277
- updatedAt: episode.updatedAt,
278
- };
279
- }),
14
+ .query(({ ctx, input }) =>
15
+ new PodcastService(ctx.db).getEpisode(ctx.session.user.id, input.episodeId),
16
+ ),
280
17
 
281
- // Generate podcast episode from text input
282
18
  generateEpisode: limitedProcedure
283
- .input(z.object({
284
- workspaceId: z.string(),
285
- podcastData: podcastInputSchema,
286
- }))
287
- .mutation(async ({ ctx, input }) => {
288
- const workspace = await ctx.db.workspace.findFirst({
289
- where: { id: input.workspaceId, ownerId: ctx.session.user.id },
290
- });
291
- if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
292
-
293
- // Emit podcast generation start notification
294
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_generation_start', {
295
- title: input.podcastData.title
296
- });
297
-
298
- const BEGIN_PODCAST_GENERATION_MESSAGE = 'Structuring podcast contents...';
299
-
300
- const newArtifact = await ctx.db.artifact.create({
301
- data: {
302
- title: '----',
303
- type: ArtifactType.PODCAST_EPISODE,
304
- generating: true,
305
- generatingMetadata: {
306
- message: BEGIN_PODCAST_GENERATION_MESSAGE,
307
- },
308
- workspace: {
309
- connect: {
310
- id: input.workspaceId,
311
- }
312
- }
313
- }
314
- });
315
-
316
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
317
- message: BEGIN_PODCAST_GENERATION_MESSAGE,
318
- });
319
-
320
- try {
321
-
322
- const structureResult = await aiSessionService.generatePodcastStructure(
323
- input.workspaceId,
324
- ctx.session.user.id,
325
- input.podcastData.title,
326
- input.podcastData.description || '',
327
- input.podcastData.userPrompt,
328
- input.podcastData.speakers
329
- );
330
-
331
- if (!structureResult.success || !structureResult.structure) {
332
- throw new TRPCError({
333
- code: 'INTERNAL_SERVER_ERROR',
334
- message: 'Failed to generate podcast structure'
335
- });
336
- }
337
-
338
- const structure = structureResult.structure;
339
-
340
- await ctx.db.artifact.update({
341
- where: {
342
- id: newArtifact.id,
343
- },
344
- data: {
345
- title: structure.episodeTitle,
346
- }
347
- });
348
-
349
- // Step 2: Generate audio for each segment
350
- const segments = [];
351
- const failedSegments = [];
352
- let totalDuration = 0;
353
- let fullTranscript = '';
354
-
355
- await ctx.db.artifact.update({
356
- where: {
357
- id: newArtifact.id,
358
- },
359
- data: {
360
- generatingMetadata: {
361
- message: `Generating podcast image...`,
362
- },
363
- }
364
- });
365
-
366
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
367
- message: `Generating podcast image...`,
368
- });
369
-
370
- const podcastImage = await aiSessionService.generatePodcastImage(
371
- input.workspaceId,
372
- ctx.session.user.id,
373
- structure.segments.map((segment: any) => segment.content).join('\n\n'),
374
- );
375
-
376
- await ctx.db.artifact.update({
377
- where: {
378
- id: newArtifact.id,
379
- },
380
- data: {
381
- imageObjectKey: podcastImage,
382
- }
383
- });
384
-
385
- for (let i = 0; i < structure.segments.length; i++) {
386
- const segment = structure.segments[i];
387
-
388
- try {
389
- // Emit segment generation progress
390
- // await PusherService.emitTaskComplete(input.workspaceId, 'podcast_segment_progress', {
391
- // currentSegment: i + 1,
392
- // totalSegments: structure.segments.length,
393
- // segmentTitle: segment.title || `Segment ${i + 1}`,
394
- // successfulSegments: segments.length,
395
- // failedSegments: failedSegments.length,
396
- // });
397
-
398
- await ctx.db.artifact.update({
399
- where: {
400
- id: newArtifact.id,
401
- },
402
- data: {
403
- generatingMetadata: {
404
- message: `Generating audio for "${segment.title}" (${i + 1} of ${structure.segments.length})...`,
405
- },
406
- }
407
- });
408
-
409
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
410
- message: `Generating audio for segment ${i + 1} of ${structure.segments.length}...`,
411
- });
412
-
413
- // Generate audio using new API
414
- const audioResult = await aiSessionService.generatePodcastAudioFromText(
415
- input.workspaceId,
416
- ctx.session.user.id,
417
- newArtifact.id,
418
- i,
419
- segment.content,
420
- input.podcastData.speakers,
421
- segment.voiceId
422
- );
423
-
424
- if (!audioResult.success) {
425
- throw new Error('Failed to generate audio for segment');
426
- }
427
-
428
- segments.push({
429
- id: uuidv4(),
430
- title: segment.title,
431
- content: segment.content,
432
- objectKey: audioResult.objectKey,
433
- startTime: totalDuration,
434
- duration: audioResult.duration,
435
- keyPoints: segment.keyPoints || [],
436
- order: segment.order || i + 1,
437
- });
438
-
439
- totalDuration += audioResult.duration;
440
- fullTranscript += `\n\n## ${segment.title}\n\n${segment.content}`;
441
-
442
- } catch (audioError) {
443
- const errorMessage = audioError instanceof Error ? audioError.message : 'Unknown error';
444
- logger.error(`❌ Error generating audio for segment ${i + 1}:`, {
445
- title: segment.title,
446
- error: errorMessage,
447
- stack: audioError instanceof Error ? audioError.stack : undefined,
448
- });
449
-
450
- // Track failed segment
451
- failedSegments.push({
452
- index: i + 1,
453
- title: segment.title || `Segment ${i + 1}`,
454
- error: errorMessage,
455
- });
456
-
457
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_segment_error', {
458
- segmentIndex: i + 1,
459
- segmentTitle: segment.title || `Segment ${i + 1}`,
460
- error: errorMessage,
461
- successfulSegments: segments.length,
462
- failedSegments: failedSegments.length,
463
- });
464
-
465
- // Continue with other segments even if one fails
466
- }
467
- }
468
-
469
- // Check if any segments were successfully generated
470
- if (segments.length === 0) {
471
- logger.error('No segments were successfully generated');
472
- await PusherService.emitError(input.workspaceId,
473
- `Failed to generate any segments. ${failedSegments.length} segment(s) failed.`,
474
- 'podcast'
475
- );
476
-
477
- // Cleanup the artifact
478
- await notifyArtifactFailed(ctx.db, {
479
- userId: ctx.session.user.id,
480
- workspaceId: input.workspaceId,
481
- artifactType: ArtifactType.PODCAST_EPISODE,
482
- artifactId: newArtifact.id,
483
- title: input.podcastData.title,
484
- message: `Failed to generate any audio segments. All ${failedSegments.length} attempts failed.`,
485
- }).catch(() => {});
486
-
487
- await ctx.db.artifact.delete({
488
- where: { id: newArtifact.id },
489
- });
490
-
491
- throw new TRPCError({
492
- code: 'INTERNAL_SERVER_ERROR',
493
- message: `Failed to generate any audio segments. All ${failedSegments.length} attempts failed.`
494
- });
495
- }
496
-
497
-
498
- await ctx.db.artifact.update({
499
- where: {
500
- id: newArtifact.id,
501
- },
502
- data: {
503
- generatingMetadata: {
504
- message: `Preparing podcast summary...`,
505
- },
506
- }
507
- });
508
-
509
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
510
- message: `Preparing podcast summary...`,
511
- });
512
-
513
- // Step 3: Generate episode summary using inference API
514
- const summaryPrompt = `Create a comprehensive podcast episode summary including:
515
- - Executive summary
516
- - Learning objectives
517
- - Key concepts covered
518
- - Recommended follow-up actions
519
- - Target audience
520
- - Prerequisites (if any)
521
-
522
- Format as JSON:
523
- {
524
- "executiveSummary": "Brief overview of the episode",
525
- "learningObjectives": ["objective1", "objective2"],
526
- "keyConcepts": ["concept1", "concept2"],
527
- "followUpActions": ["action1", "action2"],
528
- "targetAudience": "Description of target audience",
529
- "prerequisites": ["prerequisite1", "prerequisite2"],
530
- "tags": ["tag1", "tag2", "tag3"]
531
- }
532
-
533
- Podcast Title: ${structure.episodeTitle}
534
- Segments: ${JSON.stringify(segments.map(s => ({ title: s.title, keyPoints: s.keyPoints })))}`;
535
-
536
- const summaryResponse = await inference([{ role: "user", content: summaryPrompt }]);
537
- const summaryContent: string = summaryResponse.choices[0].message.content || '';
538
-
539
- let episodeSummary;
540
- try {
541
- // Extract JSON from the response
542
- const jsonMatch = summaryContent.match(/\{[\s\S]*\}/);
543
- if (!jsonMatch) {
544
- throw new Error('No JSON found in summary response');
545
- }
546
- episodeSummary = JSON.parse(jsonMatch[0]);
547
- } catch (parseError) {
548
- logger.error('Failed to parse summary response:', summaryContent);
549
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_summary_error', {
550
- error: 'Failed to parse summary response'
551
- });
552
- episodeSummary = {
553
- executiveSummary: 'AI-generated podcast episode',
554
- learningObjectives: [],
555
- keyConcepts: [],
556
- followUpActions: [],
557
- targetAudience: 'General audience',
558
- prerequisites: [],
559
- tags: [],
560
- };
561
- }
562
-
563
- // Emit summary generation completion notification
564
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
565
- message: `Podcast summary generated.`,
566
- });
567
-
568
- // Step 4: Create artifact and initial version
569
- const episodeTitle = structure.episodeTitle || input.podcastData.title;
570
-
571
- await ctx.db.artifact.update({
572
- where: {
573
- id: newArtifact.id,
574
- },
575
- data: {
576
- workspaceId: input.workspaceId,
577
- type: ArtifactType.PODCAST_EPISODE,
578
- title: episodeTitle, // Store basic title for listing/searching
579
- description: input.podcastData.description, // Store basic description for listing/searching
580
- createdById: ctx.session.user.id,
581
- },
582
- });
583
-
584
- const createdSegments = await ctx.db.podcastSegment.createMany({
585
- data: segments.map(segment => ({
586
- artifactId: newArtifact.id,
587
- title: segment.title,
588
- content: segment.content,
589
- startTime: segment.startTime,
590
- duration: segment.duration,
591
- order: segment.order,
592
- objectKey: segment.objectKey,
593
- keyPoints: segment.keyPoints,
594
- meta: {
595
- speed: input.podcastData.speed,
596
- speakers: input.podcastData.speakers,
597
- },
598
- })),
599
- });
600
-
601
- const metadata = {
602
- title: episodeTitle,
603
- description: input.podcastData.description,
604
- totalDuration: totalDuration,
605
- summary: episodeSummary,
606
- speakers: input.podcastData.speakers,
607
- generatedAt: new Date().toISOString(),
608
- };
609
-
610
- await ctx.db.artifactVersion.create({
611
- data: {
612
- artifactId: newArtifact.id,
613
- version: 1,
614
- content: fullTranscript.trim(), // Full transcript as markdown
615
- data: metadata,
616
- createdById: ctx.session.user.id,
617
- },
618
- });
619
-
620
- await ctx.db.artifact.update({
621
- where: {
622
- id: newArtifact.id,
623
- },
624
- data: {
625
- generating: false,
626
- },
627
- });
628
-
629
- // Emit podcast generation completion notification
630
- await PusherService.emitPodcastComplete(input.workspaceId, newArtifact);
631
- await notifyArtifactReady(ctx.db, {
632
- userId: ctx.session.user.id,
633
- workspaceId: input.workspaceId,
634
- artifactId: newArtifact.id,
635
- artifactType: ArtifactType.PODCAST_EPISODE,
636
- title: metadata.title,
637
- }).catch(() => {});
638
-
639
- return {
640
- id: newArtifact.id,
641
- title: metadata.title,
642
- description: metadata.description,
643
- metadata,
644
- content: fullTranscript.trim(),
645
- };
646
-
647
- } catch (error) {
648
-
649
- logger.error('Error generating podcast episode:', error);
650
-
651
- await notifyArtifactFailed(ctx.db, {
652
- userId: ctx.session.user.id,
653
- workspaceId: input.workspaceId,
654
- artifactType: ArtifactType.PODCAST_EPISODE,
655
- artifactId: newArtifact.id,
656
- title: input.podcastData.title,
657
- message:
658
- error instanceof Error
659
- ? error.message
660
- : 'Podcast generation failed.',
661
- }).catch(() => {});
662
-
663
- await ctx.db.artifact.delete({
664
- where: {
665
- id: newArtifact.id,
666
- },
667
- });
668
- await PusherService.emitError(input.workspaceId, `Failed to generate podcast episode: ${error instanceof Error ? error.message : 'Unknown error'}`, 'podcast');
669
- throw new TRPCError({
670
- code: 'INTERNAL_SERVER_ERROR',
671
- message: `Failed to generate podcast episode: ${error instanceof Error ? error.message : 'Unknown error'}`
672
- });
673
- }
674
- }),
19
+ .input(z.object({ workspaceId: z.string(), podcastData: podcastInputSchema }))
20
+ .mutation(({ ctx, input }) =>
21
+ new PodcastService(ctx.db).generateEpisode(ctx.session.user.id, input),
22
+ ),
675
23
 
676
24
  deleteSegment: authedProcedure
677
25
  .input(z.object({ segmentId: z.string() }))
678
- .mutation(async ({ ctx, input }) => {
679
- const segment = await ctx.db.podcastSegment.delete({ where: { id: input.segmentId } });
680
- return segment;
681
- }),
26
+ .mutation(({ ctx, input }) => new PodcastService(ctx.db).deleteSegment(input.segmentId)),
682
27
 
683
- // Get episode schema/structure for navigation
684
28
  getEpisodeSchema: authedProcedure
685
29
  .input(z.object({ episodeId: z.string() }))
686
- .query(async ({ ctx, input }) => {
687
- const episode = await ctx.db.artifact.findFirst({
688
- where: {
689
- id: input.episodeId,
690
- type: ArtifactType.PODCAST_EPISODE,
691
- workspace: workspaceAccessFilter(ctx.session.user.id)
692
- },
693
- include: {
694
- versions: {
695
- orderBy: { version: 'desc' },
696
- take: 1,
697
- },
698
- podcastSegments: {
699
- orderBy: { order: 'asc' },
700
- },
701
- },
702
- });
703
-
704
- if (!episode) throw new TRPCError({ code: 'NOT_FOUND' });
705
-
706
- const latestVersion = episode.versions[0];
707
- if (!latestVersion) throw new TRPCError({ code: 'NOT_FOUND', message: 'No version found' });
708
-
709
- const metadata = podcastMetadataSchema.parse(latestVersion.data);
30
+ .query(({ ctx, input }) =>
31
+ new PodcastService(ctx.db).getEpisodeSchema(ctx.session.user.id, input.episodeId),
32
+ ),
710
33
 
711
- return {
712
- segments: episode.podcastSegments.map(s => ({
713
- id: s.id,
714
- title: s.title,
715
- startTime: s.startTime,
716
- duration: s.duration,
717
- keyPoints: s.keyPoints,
718
- order: s.order,
719
- })),
720
- summary: metadata.summary,
721
- metadata: {
722
- title: metadata.title,
723
- description: metadata.description,
724
- totalDuration: metadata.totalDuration,
725
- speakers: metadata.speakers,
726
- },
727
- };
728
- }),
729
-
730
- // Update episode metadata
731
34
  updateEpisode: authedProcedure
732
- .input(z.object({
733
- episodeId: z.string(),
734
- title: z.string().optional(),
735
- description: z.string().optional(),
736
- }))
737
- .mutation(async ({ ctx, input }) => {
738
- const episode = await ctx.db.artifact.findFirst({
739
- where: {
740
- id: input.episodeId,
741
- type: ArtifactType.PODCAST_EPISODE,
742
- workspace: workspaceAccessFilter(ctx.session.user.id)
743
- },
744
- include: {
745
- versions: {
746
- orderBy: { version: 'desc' },
747
- take: 1,
748
- },
749
- },
750
- });
751
-
752
- if (!episode) throw new TRPCError({ code: 'NOT_FOUND' });
753
-
754
- const latestVersion = episode.versions[0];
755
- if (!latestVersion) throw new TRPCError({ code: 'NOT_FOUND', message: 'No version found' });
756
-
757
- const metadata = podcastMetadataSchema.parse(latestVersion.data);
758
-
759
- // Update metadata
760
- if (input.title) metadata.title = input.title;
761
- if (input.description) metadata.description = input.description;
762
-
763
- // Create new version with updated metadata
764
- const nextVersion = (latestVersion.version || 0) + 1;
765
- await ctx.db.artifactVersion.create({
766
- data: {
767
- artifactId: input.episodeId,
768
- version: nextVersion,
769
- content: latestVersion.content,
770
- data: metadata,
771
- createdById: ctx.session.user.id,
772
- },
773
- });
774
-
775
- // Update the artifact with basic info for listing/searching
776
- return ctx.db.artifact.update({
777
- where: { id: input.episodeId },
778
- data: {
779
- title: input.title ?? episode.title,
780
- description: input.description ?? episode.description,
781
- updatedAt: new Date(),
782
- },
783
- });
784
- }),
35
+ .input(
36
+ z.object({
37
+ episodeId: z.string(),
38
+ title: z.string().optional(),
39
+ description: z.string().optional(),
40
+ }),
41
+ )
42
+ .mutation(({ ctx, input }) =>
43
+ new PodcastService(ctx.db).updateEpisode(ctx.session.user.id, input),
44
+ ),
785
45
 
786
- // Delete episode and associated audio files
787
46
  deleteEpisode: authedProcedure
788
47
  .input(z.object({ episodeId: z.string() }))
789
- .mutation(async ({ ctx, input }) => {
790
- const episode = await ctx.db.artifact.findFirst({
791
- where: {
792
- id: input.episodeId,
793
- type: ArtifactType.PODCAST_EPISODE,
794
- workspace: workspaceAccessFilter(ctx.session.user.id)
795
- },
796
- include: {
797
- versions: {
798
- orderBy: { version: 'desc' },
799
- take: 1,
800
- },
801
- },
802
- });
803
-
804
- if (!episode) throw new TRPCError({ code: 'NOT_FOUND' });
805
-
806
- try {
807
- // Emit episode deletion start notification
808
- await PusherService.emitTaskComplete(episode.workspaceId, 'podcast_deletion_start', {
809
- episodeId: input.episodeId,
810
- episodeTitle: episode.title || 'Untitled Episode'
811
- });
812
-
813
- // Get segments to delete audio files
814
- const segments = await ctx.db.podcastSegment.findMany({
815
- where: { artifactId: input.episodeId },
816
- });
817
-
818
- // Delete audio files from Supabase Storage
819
- for (const segment of segments) {
820
- if (segment.objectKey) {
821
- try {
822
- await deleteFromSupabase(segment.objectKey);
823
- } catch (error) {
824
- logger.error(`Failed to delete audio file ${segment.objectKey}:`, error);
825
- }
826
- }
827
- }
828
-
829
- // Delete associated segments
830
- await ctx.db.podcastSegment.deleteMany({
831
- where: { artifactId: input.episodeId },
832
- });
833
-
834
- // Delete associated versions
835
- await ctx.db.artifactVersion.deleteMany({
836
- where: { artifactId: input.episodeId },
837
- });
48
+ .mutation(({ ctx, input }) =>
49
+ new PodcastService(ctx.db).deleteEpisode(ctx.session.user.id, input.episodeId),
50
+ ),
838
51
 
839
- // Delete the artifact
840
- await ctx.db.artifact.delete({
841
- where: { id: input.episodeId },
842
- });
843
-
844
- // Emit episode deletion completion notification
845
- await PusherService.emitTaskComplete(episode.workspaceId, 'podcast_deletion_complete', {
846
- episodeId: input.episodeId,
847
- episodeTitle: episode.title || 'Untitled Episode'
848
- });
849
-
850
- return true;
851
-
852
- } catch (error) {
853
- logger.error('Error deleting episode:', error);
854
- await PusherService.emitError(episode.workspaceId, `Failed to delete episode: ${error instanceof Error ? error.message : 'Unknown error'}`, 'podcast');
855
- throw new TRPCError({
856
- code: 'INTERNAL_SERVER_ERROR',
857
- message: 'Failed to delete episode'
858
- });
859
- }
860
- }),
861
-
862
- // Get a specific segment with signed URL
863
52
  getSegment: authedProcedure
864
53
  .input(z.object({ segmentId: z.string() }))
865
- .query(async ({ ctx, input }) => {
866
- const segment = await ctx.db.podcastSegment.findFirst({
867
- where: {
868
- id: input.segmentId,
869
- artifact: {
870
- workspace: workspaceAccessFilter(ctx.session.user.id)
871
- }
872
- },
873
- include: {
874
- artifact: true,
875
- },
876
- });
877
-
878
- if (!segment) throw new TRPCError({ code: 'NOT_FOUND' });
879
-
880
- // Generate fresh signed URL
881
- let audioUrl = null;
882
- if (segment.objectKey) {
883
- try {
884
- audioUrl = await generateSignedUrl(segment.objectKey, 24); // 24 hours
885
- } catch (error) {
886
- logger.error(`Failed to generate signed URL for segment ${segment.id}:`, error);
887
- }
888
- }
889
-
890
- return {
891
- id: segment.id,
892
- title: segment.title,
893
- content: segment.content,
894
- startTime: segment.startTime,
895
- duration: segment.duration,
896
- order: segment.order,
897
- keyPoints: segment.keyPoints,
898
- audioUrl,
899
- objectKey: segment.objectKey,
900
- meta: segment.meta,
901
- createdAt: segment.createdAt,
902
- updatedAt: segment.updatedAt,
903
- };
904
- }),
905
- // Get available voices for TTS
906
- getAvailableVoices: authedProcedure
907
- .query(async () => {
908
- return [
909
- { id: 'alloy', name: 'Alloy', description: 'Neutral, balanced voice' },
910
- { id: 'echo', name: 'Echo', description: 'Clear, professional voice' },
911
- { id: 'fable', name: 'Fable', description: 'Warm, storytelling voice' },
912
- { id: 'onyx', name: 'Onyx', description: 'Deep, authoritative voice' },
913
- { id: 'nova', name: 'Nova', description: 'Friendly, conversational voice' },
914
- { id: 'shimmer', name: 'Shimmer', description: 'Bright, energetic voice' },
915
- ];
916
- }),
54
+ .query(({ ctx, input }) =>
55
+ new PodcastService(ctx.db).getSegment(ctx.session.user.id, input.segmentId),
56
+ ),
917
57
 
918
-
919
-
920
-
921
-
922
-
923
- });
58
+ getAvailableVoices: authedProcedure.query(({ ctx }) =>
59
+ new PodcastService(ctx.db).getAvailableVoices(),
60
+ ),
61
+ });