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