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