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