@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
@@ -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
+ }