@goscribe/server 1.6.0 → 1.7.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/dist/generated/prisma/client.d.ts +224 -0
- package/dist/generated/prisma/client.js +34 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +941 -0
- package/dist/generated/prisma/commonInputTypes.js +10 -0
- package/dist/generated/prisma/enums.d.ts +67 -0
- package/dist/generated/prisma/enums.js +66 -0
- package/dist/generated/prisma/internal/class.d.ts +539 -0
- package/dist/generated/prisma/internal/class.js +49 -0
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +3924 -0
- package/dist/generated/prisma/internal/prismaNamespace.js +557 -0
- package/dist/generated/prisma/models/ActivityLog.d.ts +1847 -0
- package/dist/generated/prisma/models/ActivityLog.js +1 -0
- package/dist/generated/prisma/models/Artifact.d.ts +2345 -0
- package/dist/generated/prisma/models/Artifact.js +1 -0
- package/dist/generated/prisma/models/ArtifactVersion.d.ts +1550 -0
- package/dist/generated/prisma/models/ArtifactVersion.js +1 -0
- package/dist/generated/prisma/models/Channel.d.ts +1257 -0
- package/dist/generated/prisma/models/Channel.js +1 -0
- package/dist/generated/prisma/models/Chat.d.ts +1339 -0
- package/dist/generated/prisma/models/Chat.js +1 -0
- package/dist/generated/prisma/models/CopilotConversation.d.ts +1450 -0
- package/dist/generated/prisma/models/CopilotConversation.js +1 -0
- package/dist/generated/prisma/models/CopilotMessage.d.ts +1179 -0
- package/dist/generated/prisma/models/CopilotMessage.js +1 -0
- package/dist/generated/prisma/models/FileAsset.d.ts +1832 -0
- package/dist/generated/prisma/models/FileAsset.js +1 -0
- package/dist/generated/prisma/models/Flashcard.d.ts +1460 -0
- package/dist/generated/prisma/models/Flashcard.js +1 -0
- package/dist/generated/prisma/models/FlashcardProgress.d.ts +1782 -0
- package/dist/generated/prisma/models/FlashcardProgress.js +1 -0
- package/dist/generated/prisma/models/Folder.d.ts +1685 -0
- package/dist/generated/prisma/models/Folder.js +1 -0
- package/dist/generated/prisma/models/IdempotencyRecord.d.ts +1319 -0
- package/dist/generated/prisma/models/IdempotencyRecord.js +1 -0
- package/dist/generated/prisma/models/Invoice.d.ts +1586 -0
- package/dist/generated/prisma/models/Invoice.js +1 -0
- package/dist/generated/prisma/models/KnowledgeBase.d.ts +1721 -0
- package/dist/generated/prisma/models/KnowledgeBase.js +1 -0
- package/dist/generated/prisma/models/KnowledgeBaseChunk.d.ts +1333 -0
- package/dist/generated/prisma/models/KnowledgeBaseChunk.js +1 -0
- package/dist/generated/prisma/models/KnowledgeBaseDocument.d.ts +1695 -0
- package/dist/generated/prisma/models/KnowledgeBaseDocument.js +1 -0
- package/dist/generated/prisma/models/Notification.d.ts +1992 -0
- package/dist/generated/prisma/models/Notification.js +1 -0
- package/dist/generated/prisma/models/PasswordResetToken.d.ts +1210 -0
- package/dist/generated/prisma/models/PasswordResetToken.js +1 -0
- package/dist/generated/prisma/models/Plan.d.ts +1431 -0
- package/dist/generated/prisma/models/Plan.js +1 -0
- package/dist/generated/prisma/models/PlanLimit.d.ts +1328 -0
- package/dist/generated/prisma/models/PlanLimit.js +1 -0
- package/dist/generated/prisma/models/PodcastSegment.d.ts +1564 -0
- package/dist/generated/prisma/models/PodcastSegment.js +1 -0
- package/dist/generated/prisma/models/ResourcePrice.d.ts +1008 -0
- package/dist/generated/prisma/models/ResourcePrice.js +1 -0
- package/dist/generated/prisma/models/Role.d.ts +1065 -0
- package/dist/generated/prisma/models/Role.js +1 -0
- package/dist/generated/prisma/models/Session.d.ts +1105 -0
- package/dist/generated/prisma/models/Session.js +1 -0
- package/dist/generated/prisma/models/StripeEvent.d.ts +1081 -0
- package/dist/generated/prisma/models/StripeEvent.js +1 -0
- package/dist/generated/prisma/models/StudyGuideComment.d.ts +1321 -0
- package/dist/generated/prisma/models/StudyGuideComment.js +1 -0
- package/dist/generated/prisma/models/StudyGuideHighlight.d.ts +1629 -0
- package/dist/generated/prisma/models/StudyGuideHighlight.js +1 -0
- package/dist/generated/prisma/models/Subscription.d.ts +1677 -0
- package/dist/generated/prisma/models/Subscription.js +1 -0
- package/dist/generated/prisma/models/User.d.ts +7559 -0
- package/dist/generated/prisma/models/User.js +1 -0
- package/dist/generated/prisma/models/UserCredit.d.ts +1249 -0
- package/dist/generated/prisma/models/UserCredit.js +1 -0
- package/dist/generated/prisma/models/VerificationToken.d.ts +946 -0
- package/dist/generated/prisma/models/VerificationToken.js +1 -0
- package/dist/generated/prisma/models/WorksheetPreset.d.ts +1433 -0
- package/dist/generated/prisma/models/WorksheetPreset.js +1 -0
- package/dist/generated/prisma/models/WorksheetQuestion.d.ts +1491 -0
- package/dist/generated/prisma/models/WorksheetQuestion.js +1 -0
- package/dist/generated/prisma/models/WorksheetQuestionProgress.d.ts +1620 -0
- package/dist/generated/prisma/models/WorksheetQuestionProgress.js +1 -0
- package/dist/generated/prisma/models/Workspace.d.ts +3620 -0
- package/dist/generated/prisma/models/Workspace.js +1 -0
- package/dist/generated/prisma/models/WorkspaceInvitation.d.ts +1490 -0
- package/dist/generated/prisma/models/WorkspaceInvitation.js +1 -0
- package/dist/generated/prisma/models/WorkspaceKnowledgeBase.d.ts +1410 -0
- package/dist/generated/prisma/models/WorkspaceKnowledgeBase.js +1 -0
- package/dist/generated/prisma/models/WorkspaceMember.d.ts +1326 -0
- package/dist/generated/prisma/models/WorkspaceMember.js +1 -0
- package/dist/generated/prisma/models.d.ts +39 -0
- package/dist/generated/prisma/models.js +1 -0
- package/dist/src/context.d.ts +27 -0
- package/dist/src/context.js +33 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +1 -0
- package/dist/src/lib/ai/config.d.ts +20 -0
- package/dist/src/lib/ai/config.js +31 -0
- package/dist/src/lib/ai/embedding-client.d.ts +8 -0
- package/dist/src/lib/ai/embedding-client.js +30 -0
- package/dist/src/lib/ai/index.d.ts +48 -0
- package/dist/src/lib/ai/index.js +29 -0
- package/dist/src/lib/ai/inference-backend/client.d.ts +28 -0
- package/dist/src/lib/ai/inference-backend/client.js +301 -0
- package/dist/src/lib/ai/inference-backend/mocks.d.ts +12 -0
- package/dist/src/lib/ai/inference-backend/mocks.js +133 -0
- package/dist/src/lib/ai/inference-backend/types.d.ts +44 -0
- package/dist/src/lib/ai/inference-backend/types.js +1 -0
- package/dist/src/lib/ai/json-parse.d.ts +2 -0
- package/dist/src/lib/ai/json-parse.js +34 -0
- package/dist/src/lib/ai/llm-client.d.ts +7 -0
- package/dist/src/lib/ai/llm-client.js +36 -0
- package/dist/src/lib/ai/mock.d.ts +2 -0
- package/dist/src/lib/ai/mock.js +10 -0
- package/dist/src/lib/ai/types.d.ts +9 -0
- package/dist/src/lib/ai/types.js +1 -0
- package/dist/src/lib/auth.d.ts +36 -0
- package/dist/src/lib/auth.js +117 -0
- package/dist/src/lib/chunking.d.ts +19 -0
- package/dist/src/lib/chunking.js +47 -0
- package/dist/src/lib/constants.d.ts +13 -0
- package/dist/src/lib/constants.js +12 -0
- package/dist/src/lib/curated-kb-seed.d.ts +12 -0
- package/dist/src/lib/curated-kb-seed.js +155 -0
- package/dist/src/lib/email.d.ts +11 -0
- package/dist/src/lib/email.js +152 -0
- package/dist/src/lib/embeddings.d.ts +2 -0
- package/dist/src/lib/embeddings.js +1 -0
- package/dist/src/lib/ensure-curated-kb-catalog.d.ts +6 -0
- package/dist/src/lib/ensure-curated-kb-catalog.js +53 -0
- package/dist/src/lib/env.d.ts +41 -0
- package/dist/src/lib/env.js +57 -0
- package/dist/src/lib/errors.d.ts +33 -0
- package/dist/src/lib/errors.js +78 -0
- package/dist/src/lib/file.d.ts +0 -0
- package/dist/src/lib/file.js +1 -0
- package/dist/src/lib/inference.d.ts +1 -0
- package/dist/src/lib/inference.js +1 -0
- package/dist/src/lib/kb-meta.d.ts +8 -0
- package/dist/src/lib/kb-meta.js +77 -0
- package/dist/src/lib/logger.d.ts +62 -0
- package/dist/src/lib/logger.js +364 -0
- package/dist/src/lib/pdf.d.ts +11 -0
- package/dist/src/lib/pdf.js +11 -0
- package/dist/src/lib/prisma.d.ts +3 -0
- package/dist/src/lib/prisma.js +15 -0
- package/dist/src/lib/pusher.d.ts +38 -0
- package/dist/src/lib/pusher.js +170 -0
- package/dist/src/lib/retry.d.ts +15 -0
- package/dist/src/lib/retry.js +37 -0
- package/dist/src/lib/storage.d.ts +11 -0
- package/dist/src/lib/storage.js +71 -0
- package/dist/src/lib/stripe.d.ts +10 -0
- package/dist/src/lib/stripe.js +36 -0
- package/dist/src/lib/validation.d.ts +51 -0
- package/dist/src/lib/validation.js +64 -0
- package/dist/src/lib/workspace-kb.d.ts +5 -0
- package/dist/src/lib/workspace-kb.js +7 -0
- package/dist/src/repositories/artifact.repository.d.ts +64 -0
- package/dist/src/repositories/artifact.repository.js +40 -0
- package/dist/src/repositories/base.repository.d.ts +14 -0
- package/dist/src/repositories/base.repository.js +14 -0
- package/dist/src/repositories/invitation.repository.d.ts +104 -0
- package/dist/src/repositories/invitation.repository.js +44 -0
- package/dist/src/repositories/notification.repository.d.ts +76 -0
- package/dist/src/repositories/notification.repository.js +44 -0
- package/dist/src/repositories/user.repository.d.ts +84 -0
- package/dist/src/repositories/user.repository.js +37 -0
- package/dist/src/repositories/workspace-member.repository.d.ts +35 -0
- package/dist/src/repositories/workspace-member.repository.js +31 -0
- package/dist/src/repositories/workspace.repository.d.ts +101 -0
- package/dist/src/repositories/workspace.repository.js +79 -0
- package/dist/src/routers/_app.d.ts +3464 -0
- package/dist/src/routers/_app.js +36 -0
- package/dist/src/routers/admin.d.ts +358 -0
- package/dist/src/routers/admin.js +105 -0
- package/dist/src/routers/annotations.d.ts +219 -0
- package/dist/src/routers/annotations.js +29 -0
- package/dist/src/routers/artifactVersions.d.ts +65 -0
- package/dist/src/routers/artifactVersions.js +14 -0
- package/dist/src/routers/auth.d.ts +161 -0
- package/dist/src/routers/auth.js +97 -0
- package/dist/src/routers/chat.d.ts +170 -0
- package/dist/src/routers/chat.js +32 -0
- package/dist/src/routers/copilot.d.ts +200 -0
- package/dist/src/routers/copilot.js +52 -0
- package/dist/src/routers/flashcards.d.ts +336 -0
- package/dist/src/routers/flashcards.js +93 -0
- package/dist/src/routers/knowledgeBase.d.ts +421 -0
- package/dist/src/routers/knowledgeBase.js +118 -0
- package/dist/src/routers/members.d.ts +169 -0
- package/dist/src/routers/members.js +47 -0
- package/dist/src/routers/notifications.d.ts +99 -0
- package/dist/src/routers/notifications.js +25 -0
- package/dist/src/routers/payment.d.ts +80 -0
- package/dist/src/routers/payment.js +21 -0
- package/dist/src/routers/podcast.d.ts +287 -0
- package/dist/src/routers/podcast.js +34 -0
- package/dist/src/routers/studyguide.d.ts +36 -0
- package/dist/src/routers/studyguide.js +8 -0
- package/dist/src/routers/worksheets.d.ts +429 -0
- package/dist/src/routers/worksheets.js +139 -0
- package/dist/src/routers/workspace.d.ts +563 -0
- package/dist/src/routers/workspace.js +104 -0
- package/dist/src/scripts/purge-deleted-users.d.ts +1 -0
- package/dist/src/scripts/purge-deleted-users.js +148 -0
- package/dist/src/server.d.ts +1 -0
- package/dist/src/server.js +190 -0
- package/dist/src/services/activity/activity-human-description.service.d.ts +13 -0
- package/dist/src/services/activity/activity-human-description.service.js +221 -0
- package/dist/src/services/activity/activity-human-description.service.test.d.ts +1 -0
- package/dist/src/services/activity/activity-human-description.service.test.js +16 -0
- package/dist/src/services/activity/activity-log.service.d.ts +87 -0
- package/dist/src/services/activity/activity-log.service.js +276 -0
- package/dist/src/services/activity/activity-log.service.test.d.ts +1 -0
- package/dist/src/services/activity/activity-log.service.test.js +27 -0
- package/dist/src/services/admin/admin.service.d.ts +270 -0
- package/dist/src/services/admin/admin.service.js +476 -0
- package/dist/src/services/ai/ai-session.service.d.ts +5 -0
- package/dist/src/services/ai/ai-session.service.js +4 -0
- package/dist/src/services/artifacts/annotation.service.d.ts +177 -0
- package/dist/src/services/artifacts/annotation.service.js +154 -0
- package/dist/src/services/artifacts/artifact-version.service.d.ts +38 -0
- package/dist/src/services/artifacts/artifact-version.service.js +129 -0
- package/dist/src/services/artifacts/chat.service.d.ts +127 -0
- package/dist/src/services/artifacts/chat.service.js +182 -0
- package/dist/src/services/artifacts/study-guide.service.d.ts +18 -0
- package/dist/src/services/artifacts/study-guide.service.js +65 -0
- package/dist/src/services/auth/auth.service.d.ts +94 -0
- package/dist/src/services/auth/auth.service.js +368 -0
- package/dist/src/services/base.service.d.ts +14 -0
- package/dist/src/services/base.service.js +14 -0
- package/dist/src/services/billing/payment.service.d.ts +44 -0
- package/dist/src/services/billing/payment.service.js +365 -0
- package/dist/src/services/billing/subscription.service.d.ts +37 -0
- package/dist/src/services/billing/subscription.service.js +654 -0
- package/dist/src/services/billing/usage.service.d.ts +47 -0
- package/dist/src/services/billing/usage.service.js +149 -0
- package/dist/src/services/content/copilot.service.d.ts +113 -0
- package/dist/src/services/content/copilot.service.js +439 -0
- package/dist/src/services/content/flashcard-progress.service.d.ts +159 -0
- package/dist/src/services/content/flashcard-progress.service.js +432 -0
- package/dist/src/services/content/flashcard.service.d.ts +184 -0
- package/dist/src/services/content/flashcard.service.js +339 -0
- package/dist/src/services/content/media-analysis.service.d.ts +23 -0
- package/dist/src/services/content/media-analysis.service.js +404 -0
- package/dist/src/services/content/podcast.service.d.ts +267 -0
- package/dist/src/services/content/podcast.service.js +653 -0
- package/dist/src/services/content/worksheet-content.service.d.ts +37 -0
- package/dist/src/services/content/worksheet-content.service.js +84 -0
- package/dist/src/services/content/worksheet-content.service.test.d.ts +1 -0
- package/dist/src/services/content/worksheet-content.service.test.js +69 -0
- package/dist/src/services/content/worksheet-generation.service.d.ts +91 -0
- package/dist/src/services/content/worksheet-generation.service.js +95 -0
- package/dist/src/services/content/worksheet-generation.service.test.d.ts +1 -0
- package/dist/src/services/content/worksheet-generation.service.test.js +20 -0
- package/dist/src/services/content/worksheet.service.d.ts +347 -0
- package/dist/src/services/content/worksheet.service.js +599 -0
- package/dist/src/services/knowledge/knowledge-base.service.d.ts +316 -0
- package/dist/src/services/knowledge/knowledge-base.service.js +544 -0
- package/dist/src/services/members/invitation.service.d.ts +66 -0
- package/dist/src/services/members/invitation.service.js +348 -0
- package/dist/src/services/members/member.service.d.ts +36 -0
- package/dist/src/services/members/member.service.js +193 -0
- package/dist/src/services/notifications/notification.service.d.ts +214 -0
- package/dist/src/services/notifications/notification.service.js +550 -0
- package/dist/src/services/notifications/notification.service.test.d.ts +1 -0
- package/dist/src/services/notifications/notification.service.test.js +87 -0
- package/dist/src/services/workspace/workspace-analytics.service.d.ts +24 -0
- package/dist/src/services/workspace/workspace-analytics.service.js +95 -0
- package/dist/src/services/workspace/workspace-kb.service.d.ts +40 -0
- package/dist/src/services/workspace/workspace-kb.service.js +184 -0
- package/dist/src/services/workspace/workspace.service.d.ts +263 -0
- package/dist/src/services/workspace/workspace.service.js +401 -0
- package/dist/src/trpc.d.ts +60 -0
- package/dist/src/trpc.js +217 -0
- package/dist/src/types/index.d.ts +126 -0
- package/dist/src/types/index.js +1 -0
- package/package.json +8 -9
- package/prisma/schema.prisma +3 -4
- package/prisma/seed.mjs +5 -2
- package/prisma.config.ts +16 -0
- package/src/lib/prisma.ts +18 -9
- package/src/scripts/purge-deleted-users.ts +1 -3
- package/tsconfig.json +3 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import { TRPCError } from '@trpc/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
import { BaseService } from '../base.service.js';
|
|
5
|
+
import { ArtifactType } from '../../lib/constants.js';
|
|
6
|
+
import { ai } from '../../lib/ai/index.js';
|
|
7
|
+
import { generateSignedUrl, deleteFromSupabase } from '../../lib/storage.js';
|
|
8
|
+
import PusherService from '../../lib/pusher.js';
|
|
9
|
+
import { notifyArtifactFailed, notifyArtifactReady } from '../notifications/notification.service.js';
|
|
10
|
+
import { workspaceAccessWhere } from '../../repositories/workspace.repository.js';
|
|
11
|
+
export const speakerSchema = z.object({
|
|
12
|
+
id: z.string(),
|
|
13
|
+
role: z.enum(['host', 'guest', 'expert']),
|
|
14
|
+
name: z.string().optional(),
|
|
15
|
+
});
|
|
16
|
+
export const podcastInputSchema = z.object({
|
|
17
|
+
title: z.string(),
|
|
18
|
+
description: z.string().optional(),
|
|
19
|
+
userPrompt: z.string(),
|
|
20
|
+
speakers: z
|
|
21
|
+
.array(speakerSchema)
|
|
22
|
+
.min(1)
|
|
23
|
+
.default([{ id: 'pNInz6obpgDQGcFmaJgB', role: 'host' }]),
|
|
24
|
+
speed: z.number().min(0.25).max(4.0).default(1.0),
|
|
25
|
+
generateIntro: z.boolean().default(true),
|
|
26
|
+
generateOutro: z.boolean().default(true),
|
|
27
|
+
segmentByTopics: z.boolean().default(true),
|
|
28
|
+
});
|
|
29
|
+
export const podcastMetadataSchema = z.object({
|
|
30
|
+
title: z.string(),
|
|
31
|
+
description: z.string().optional(),
|
|
32
|
+
totalDuration: z.number(),
|
|
33
|
+
speakers: z.array(speakerSchema),
|
|
34
|
+
summary: z.object({
|
|
35
|
+
executiveSummary: z.string(),
|
|
36
|
+
learningObjectives: z.array(z.string()),
|
|
37
|
+
keyConcepts: z.array(z.string()),
|
|
38
|
+
followUpActions: z.array(z.string()),
|
|
39
|
+
targetAudience: z.string(),
|
|
40
|
+
prerequisites: z.array(z.string()),
|
|
41
|
+
tags: z.array(z.string()),
|
|
42
|
+
}),
|
|
43
|
+
generatedAt: z.string(),
|
|
44
|
+
});
|
|
45
|
+
export class PodcastService extends BaseService {
|
|
46
|
+
constructor(db) {
|
|
47
|
+
super(db);
|
|
48
|
+
}
|
|
49
|
+
async listEpisodes(userId, workspaceId) {
|
|
50
|
+
const workspace = await this.db.workspace.findFirst({
|
|
51
|
+
where: { id: workspaceId, ownerId: userId },
|
|
52
|
+
});
|
|
53
|
+
if (!workspace)
|
|
54
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
55
|
+
const artifacts = await this.db.artifact.findMany({
|
|
56
|
+
where: { workspaceId, type: ArtifactType.PODCAST_EPISODE },
|
|
57
|
+
include: {
|
|
58
|
+
versions: { orderBy: { version: 'desc' }, take: 1 },
|
|
59
|
+
podcastSegments: { orderBy: { order: 'asc' } },
|
|
60
|
+
},
|
|
61
|
+
orderBy: { updatedAt: 'desc' },
|
|
62
|
+
});
|
|
63
|
+
this.logger.debug(`Found ${artifacts.length} podcast artifacts`);
|
|
64
|
+
artifacts.forEach((artifact, i) => {
|
|
65
|
+
this.logger.debug(` Podcast ${i + 1}: "${artifact.title}" - ${artifact.podcastSegments.length} segments`);
|
|
66
|
+
});
|
|
67
|
+
const episodesWithUrls = await Promise.all(artifacts.map(async (artifact) => {
|
|
68
|
+
const latestVersion = artifact.versions[0];
|
|
69
|
+
let objectUrl = null;
|
|
70
|
+
if (artifact.imageObjectKey) {
|
|
71
|
+
objectUrl = await generateSignedUrl(artifact.imageObjectKey, 24);
|
|
72
|
+
}
|
|
73
|
+
const segmentsWithUrls = await Promise.all(artifact.podcastSegments.map(async (segment) => {
|
|
74
|
+
if (segment.objectKey) {
|
|
75
|
+
try {
|
|
76
|
+
const signedUrl = await generateSignedUrl(segment.objectKey, 24);
|
|
77
|
+
return {
|
|
78
|
+
id: segment.id,
|
|
79
|
+
title: segment.title,
|
|
80
|
+
audioUrl: signedUrl,
|
|
81
|
+
objectKey: segment.objectKey,
|
|
82
|
+
startTime: segment.startTime,
|
|
83
|
+
duration: segment.duration,
|
|
84
|
+
order: segment.order,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
this.logger.error(`Failed to generate signed URL for segment ${segment.id}:`, error);
|
|
89
|
+
return {
|
|
90
|
+
id: segment.id,
|
|
91
|
+
title: segment.title,
|
|
92
|
+
audioUrl: null,
|
|
93
|
+
objectKey: segment.objectKey,
|
|
94
|
+
startTime: segment.startTime,
|
|
95
|
+
duration: segment.duration,
|
|
96
|
+
order: segment.order,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
id: segment.id,
|
|
102
|
+
title: segment.title,
|
|
103
|
+
audioUrl: null,
|
|
104
|
+
objectKey: segment.objectKey,
|
|
105
|
+
startTime: segment.startTime,
|
|
106
|
+
duration: segment.duration,
|
|
107
|
+
order: segment.order,
|
|
108
|
+
};
|
|
109
|
+
}));
|
|
110
|
+
let metadata = null;
|
|
111
|
+
if (latestVersion) {
|
|
112
|
+
try {
|
|
113
|
+
this.logger.debug(JSON.stringify(latestVersion.data));
|
|
114
|
+
metadata = podcastMetadataSchema.parse(latestVersion.data);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
this.logger.error('Failed to parse podcast metadata:', error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
id: artifact.id,
|
|
122
|
+
title: metadata?.title || artifact.title || 'Untitled Episode',
|
|
123
|
+
description: metadata?.description || artifact.description || null,
|
|
124
|
+
metadata,
|
|
125
|
+
imageUrl: objectUrl,
|
|
126
|
+
segments: segmentsWithUrls,
|
|
127
|
+
createdAt: artifact.createdAt,
|
|
128
|
+
updatedAt: artifact.updatedAt,
|
|
129
|
+
workspaceId: artifact.workspaceId,
|
|
130
|
+
generating: artifact.generating,
|
|
131
|
+
generatingMetadata: artifact.generatingMetadata,
|
|
132
|
+
type: artifact.type,
|
|
133
|
+
createdById: artifact.createdById,
|
|
134
|
+
isArchived: artifact.isArchived,
|
|
135
|
+
};
|
|
136
|
+
}));
|
|
137
|
+
return episodesWithUrls;
|
|
138
|
+
}
|
|
139
|
+
async getEpisode(userId, episodeId) {
|
|
140
|
+
const episode = await this.db.artifact.findFirst({
|
|
141
|
+
where: {
|
|
142
|
+
id: episodeId,
|
|
143
|
+
type: ArtifactType.PODCAST_EPISODE,
|
|
144
|
+
workspace: workspaceAccessWhere(userId),
|
|
145
|
+
},
|
|
146
|
+
include: {
|
|
147
|
+
versions: { orderBy: { version: 'desc' }, take: 1 },
|
|
148
|
+
podcastSegments: { orderBy: { order: 'asc' } },
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
this.logger.debug(JSON.stringify(episode));
|
|
152
|
+
if (!episode)
|
|
153
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
154
|
+
const latestVersion = episode.versions[0];
|
|
155
|
+
if (!latestVersion)
|
|
156
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'No version found' });
|
|
157
|
+
this.logger.debug(JSON.stringify(latestVersion));
|
|
158
|
+
try {
|
|
159
|
+
podcastMetadataSchema.parse(latestVersion.data);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
this.logger.error('Failed to parse podcast metadata:', error);
|
|
163
|
+
}
|
|
164
|
+
const metadata = podcastMetadataSchema.parse(latestVersion.data);
|
|
165
|
+
const imageUrl = episode.imageObjectKey
|
|
166
|
+
? await generateSignedUrl(episode.imageObjectKey, 24)
|
|
167
|
+
: null;
|
|
168
|
+
const segmentsWithUrls = await Promise.all(episode.podcastSegments.map(async (segment) => {
|
|
169
|
+
if (segment.objectKey) {
|
|
170
|
+
try {
|
|
171
|
+
const signedUrl = await generateSignedUrl(segment.objectKey, 24);
|
|
172
|
+
return {
|
|
173
|
+
id: segment.id,
|
|
174
|
+
title: segment.title,
|
|
175
|
+
content: segment.content,
|
|
176
|
+
audioUrl: signedUrl,
|
|
177
|
+
objectKey: segment.objectKey,
|
|
178
|
+
startTime: segment.startTime,
|
|
179
|
+
duration: segment.duration,
|
|
180
|
+
keyPoints: segment.keyPoints,
|
|
181
|
+
order: segment.order,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
this.logger.error(`Failed to generate signed URL for segment ${segment.id}:`, error);
|
|
186
|
+
return {
|
|
187
|
+
id: segment.id,
|
|
188
|
+
title: segment.title,
|
|
189
|
+
content: segment.content,
|
|
190
|
+
audioUrl: null,
|
|
191
|
+
objectKey: segment.objectKey,
|
|
192
|
+
startTime: segment.startTime,
|
|
193
|
+
duration: segment.duration,
|
|
194
|
+
keyPoints: segment.keyPoints,
|
|
195
|
+
order: segment.order,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
id: segment.id,
|
|
201
|
+
title: segment.title,
|
|
202
|
+
content: segment.content,
|
|
203
|
+
audioUrl: null,
|
|
204
|
+
objectKey: segment.objectKey,
|
|
205
|
+
startTime: segment.startTime,
|
|
206
|
+
duration: segment.duration,
|
|
207
|
+
keyPoints: segment.keyPoints,
|
|
208
|
+
order: segment.order,
|
|
209
|
+
};
|
|
210
|
+
}));
|
|
211
|
+
return {
|
|
212
|
+
id: episode.id,
|
|
213
|
+
title: metadata.title,
|
|
214
|
+
description: metadata.description,
|
|
215
|
+
metadata,
|
|
216
|
+
imageUrl,
|
|
217
|
+
segments: segmentsWithUrls,
|
|
218
|
+
content: latestVersion.content,
|
|
219
|
+
createdAt: episode.createdAt,
|
|
220
|
+
updatedAt: episode.updatedAt,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
async generateEpisode(userId, input) {
|
|
224
|
+
const workspace = await this.db.workspace.findFirst({
|
|
225
|
+
where: { id: input.workspaceId, ownerId: userId },
|
|
226
|
+
});
|
|
227
|
+
if (!workspace)
|
|
228
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
229
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_generation_start', {
|
|
230
|
+
title: input.podcastData.title,
|
|
231
|
+
});
|
|
232
|
+
const BEGIN_PODCAST_GENERATION_MESSAGE = 'Structuring podcast contents...';
|
|
233
|
+
const newArtifact = await this.db.artifact.create({
|
|
234
|
+
data: {
|
|
235
|
+
title: '----',
|
|
236
|
+
type: ArtifactType.PODCAST_EPISODE,
|
|
237
|
+
generating: true,
|
|
238
|
+
generatingMetadata: { message: BEGIN_PODCAST_GENERATION_MESSAGE },
|
|
239
|
+
workspace: { connect: { id: input.workspaceId } },
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
|
|
243
|
+
message: BEGIN_PODCAST_GENERATION_MESSAGE,
|
|
244
|
+
});
|
|
245
|
+
try {
|
|
246
|
+
const structureResult = await ai.backend.generatePodcastStructure(input.workspaceId, userId, input.podcastData.title, input.podcastData.description || '', input.podcastData.userPrompt, input.podcastData.speakers);
|
|
247
|
+
if (!structureResult.success || !structureResult.structure) {
|
|
248
|
+
throw new TRPCError({
|
|
249
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
250
|
+
message: 'Failed to generate podcast structure',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const structure = structureResult.structure;
|
|
254
|
+
await this.db.artifact.update({
|
|
255
|
+
where: { id: newArtifact.id },
|
|
256
|
+
data: { title: structure.episodeTitle },
|
|
257
|
+
});
|
|
258
|
+
const segments = [];
|
|
259
|
+
const failedSegments = [];
|
|
260
|
+
let totalDuration = 0;
|
|
261
|
+
let fullTranscript = '';
|
|
262
|
+
await this.db.artifact.update({
|
|
263
|
+
where: { id: newArtifact.id },
|
|
264
|
+
data: { generatingMetadata: { message: `Generating podcast image...` } },
|
|
265
|
+
});
|
|
266
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
|
|
267
|
+
message: `Generating podcast image...`,
|
|
268
|
+
});
|
|
269
|
+
const podcastImage = await ai.backend.generatePodcastImage(input.workspaceId, userId, structure.segments.map((segment) => segment.content).join('\n\n'));
|
|
270
|
+
await this.db.artifact.update({
|
|
271
|
+
where: { id: newArtifact.id },
|
|
272
|
+
data: { imageObjectKey: podcastImage },
|
|
273
|
+
});
|
|
274
|
+
for (let i = 0; i < structure.segments.length; i++) {
|
|
275
|
+
const segment = structure.segments[i];
|
|
276
|
+
try {
|
|
277
|
+
await this.db.artifact.update({
|
|
278
|
+
where: { id: newArtifact.id },
|
|
279
|
+
data: {
|
|
280
|
+
generatingMetadata: {
|
|
281
|
+
message: `Generating audio for "${segment.title}" (${i + 1} of ${structure.segments.length})...`,
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
|
|
286
|
+
message: `Generating audio for segment ${i + 1} of ${structure.segments.length}...`,
|
|
287
|
+
});
|
|
288
|
+
const audioResult = await ai.backend.generatePodcastAudioFromText(input.workspaceId, userId, newArtifact.id, i, segment.content, input.podcastData.speakers, segment.voiceId);
|
|
289
|
+
if (!audioResult.success) {
|
|
290
|
+
throw new Error('Failed to generate audio for segment');
|
|
291
|
+
}
|
|
292
|
+
segments.push({
|
|
293
|
+
id: uuidv4(),
|
|
294
|
+
title: segment.title,
|
|
295
|
+
content: segment.content,
|
|
296
|
+
objectKey: audioResult.objectKey,
|
|
297
|
+
startTime: totalDuration,
|
|
298
|
+
duration: audioResult.duration,
|
|
299
|
+
keyPoints: segment.keyPoints || [],
|
|
300
|
+
order: segment.order || i + 1,
|
|
301
|
+
});
|
|
302
|
+
totalDuration += audioResult.duration;
|
|
303
|
+
fullTranscript += `\n\n## ${segment.title}\n\n${segment.content}`;
|
|
304
|
+
}
|
|
305
|
+
catch (audioError) {
|
|
306
|
+
const errorMessage = audioError instanceof Error ? audioError.message : 'Unknown error';
|
|
307
|
+
this.logger.error(`❌ Error generating audio for segment ${i + 1}:`, {
|
|
308
|
+
title: segment.title,
|
|
309
|
+
error: errorMessage,
|
|
310
|
+
stack: audioError instanceof Error ? audioError.stack : undefined,
|
|
311
|
+
});
|
|
312
|
+
failedSegments.push({
|
|
313
|
+
index: i + 1,
|
|
314
|
+
title: segment.title || `Segment ${i + 1}`,
|
|
315
|
+
error: errorMessage,
|
|
316
|
+
});
|
|
317
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_segment_error', {
|
|
318
|
+
segmentIndex: i + 1,
|
|
319
|
+
segmentTitle: segment.title || `Segment ${i + 1}`,
|
|
320
|
+
error: errorMessage,
|
|
321
|
+
successfulSegments: segments.length,
|
|
322
|
+
failedSegments: failedSegments.length,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (segments.length === 0) {
|
|
327
|
+
this.logger.error('No segments were successfully generated');
|
|
328
|
+
await PusherService.emitError(input.workspaceId, `Failed to generate any segments. ${failedSegments.length} segment(s) failed.`, 'podcast');
|
|
329
|
+
await notifyArtifactFailed(this.db, {
|
|
330
|
+
userId,
|
|
331
|
+
workspaceId: input.workspaceId,
|
|
332
|
+
artifactType: ArtifactType.PODCAST_EPISODE,
|
|
333
|
+
artifactId: newArtifact.id,
|
|
334
|
+
title: input.podcastData.title,
|
|
335
|
+
message: `Failed to generate any audio segments. All ${failedSegments.length} attempts failed.`,
|
|
336
|
+
}).catch(() => { });
|
|
337
|
+
await this.db.artifact.delete({ where: { id: newArtifact.id } });
|
|
338
|
+
throw new TRPCError({
|
|
339
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
340
|
+
message: `Failed to generate any audio segments. All ${failedSegments.length} attempts failed.`,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
await this.db.artifact.update({
|
|
344
|
+
where: { id: newArtifact.id },
|
|
345
|
+
data: { generatingMetadata: { message: `Preparing podcast summary...` } },
|
|
346
|
+
});
|
|
347
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
|
|
348
|
+
message: `Preparing podcast summary...`,
|
|
349
|
+
});
|
|
350
|
+
const summaryPrompt = `Create a comprehensive podcast episode summary including:
|
|
351
|
+
- Executive summary
|
|
352
|
+
- Learning objectives
|
|
353
|
+
- Key concepts covered
|
|
354
|
+
- Recommended follow-up actions
|
|
355
|
+
- Target audience
|
|
356
|
+
- Prerequisites (if any)
|
|
357
|
+
|
|
358
|
+
Format as JSON:
|
|
359
|
+
{
|
|
360
|
+
"executiveSummary": "Brief overview of the episode",
|
|
361
|
+
"learningObjectives": ["objective1", "objective2"],
|
|
362
|
+
"keyConcepts": ["concept1", "concept2"],
|
|
363
|
+
"followUpActions": ["action1", "action2"],
|
|
364
|
+
"targetAudience": "Description of target audience",
|
|
365
|
+
"prerequisites": ["prerequisite1", "prerequisite2"],
|
|
366
|
+
"tags": ["tag1", "tag2", "tag3"]
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
Podcast Title: ${structure.episodeTitle}
|
|
370
|
+
Segments: ${JSON.stringify(segments.map((s) => ({ title: s.title, keyPoints: s.keyPoints })))}`;
|
|
371
|
+
const summaryResponse = await ai.llm.complete([{ role: 'user', content: summaryPrompt }]);
|
|
372
|
+
const summaryContent = summaryResponse.choices[0].message.content || '';
|
|
373
|
+
let episodeSummary;
|
|
374
|
+
try {
|
|
375
|
+
const jsonMatch = summaryContent.match(/\{[\s\S]*\}/);
|
|
376
|
+
if (!jsonMatch) {
|
|
377
|
+
throw new Error('No JSON found in summary response');
|
|
378
|
+
}
|
|
379
|
+
episodeSummary = JSON.parse(jsonMatch[0]);
|
|
380
|
+
}
|
|
381
|
+
catch (parseError) {
|
|
382
|
+
this.logger.error('Failed to parse summary response:', summaryContent);
|
|
383
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_summary_error', {
|
|
384
|
+
error: 'Failed to parse summary response',
|
|
385
|
+
});
|
|
386
|
+
episodeSummary = {
|
|
387
|
+
executiveSummary: 'AI-generated podcast episode',
|
|
388
|
+
learningObjectives: [],
|
|
389
|
+
keyConcepts: [],
|
|
390
|
+
followUpActions: [],
|
|
391
|
+
targetAudience: 'General audience',
|
|
392
|
+
prerequisites: [],
|
|
393
|
+
tags: [],
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
|
|
397
|
+
message: `Podcast summary generated.`,
|
|
398
|
+
});
|
|
399
|
+
const episodeTitle = structure.episodeTitle || input.podcastData.title;
|
|
400
|
+
await this.db.artifact.update({
|
|
401
|
+
where: { id: newArtifact.id },
|
|
402
|
+
data: {
|
|
403
|
+
workspaceId: input.workspaceId,
|
|
404
|
+
type: ArtifactType.PODCAST_EPISODE,
|
|
405
|
+
title: episodeTitle,
|
|
406
|
+
description: input.podcastData.description,
|
|
407
|
+
createdById: userId,
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
await this.db.podcastSegment.createMany({
|
|
411
|
+
data: segments.map((segment) => ({
|
|
412
|
+
artifactId: newArtifact.id,
|
|
413
|
+
title: segment.title,
|
|
414
|
+
content: segment.content,
|
|
415
|
+
startTime: segment.startTime,
|
|
416
|
+
duration: segment.duration,
|
|
417
|
+
order: segment.order,
|
|
418
|
+
objectKey: segment.objectKey,
|
|
419
|
+
keyPoints: segment.keyPoints,
|
|
420
|
+
meta: {
|
|
421
|
+
speed: input.podcastData.speed,
|
|
422
|
+
speakers: input.podcastData.speakers,
|
|
423
|
+
},
|
|
424
|
+
})),
|
|
425
|
+
});
|
|
426
|
+
const metadata = {
|
|
427
|
+
title: episodeTitle,
|
|
428
|
+
description: input.podcastData.description,
|
|
429
|
+
totalDuration,
|
|
430
|
+
summary: episodeSummary,
|
|
431
|
+
speakers: input.podcastData.speakers,
|
|
432
|
+
generatedAt: new Date().toISOString(),
|
|
433
|
+
};
|
|
434
|
+
await this.db.artifactVersion.create({
|
|
435
|
+
data: {
|
|
436
|
+
artifactId: newArtifact.id,
|
|
437
|
+
version: 1,
|
|
438
|
+
content: fullTranscript.trim(),
|
|
439
|
+
data: metadata,
|
|
440
|
+
createdById: userId,
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
await this.db.artifact.update({
|
|
444
|
+
where: { id: newArtifact.id },
|
|
445
|
+
data: { generating: false },
|
|
446
|
+
});
|
|
447
|
+
await PusherService.emitPodcastComplete(input.workspaceId, newArtifact);
|
|
448
|
+
await notifyArtifactReady(this.db, {
|
|
449
|
+
userId,
|
|
450
|
+
workspaceId: input.workspaceId,
|
|
451
|
+
artifactId: newArtifact.id,
|
|
452
|
+
artifactType: ArtifactType.PODCAST_EPISODE,
|
|
453
|
+
title: metadata.title,
|
|
454
|
+
}).catch(() => { });
|
|
455
|
+
return {
|
|
456
|
+
id: newArtifact.id,
|
|
457
|
+
title: metadata.title,
|
|
458
|
+
description: metadata.description,
|
|
459
|
+
metadata,
|
|
460
|
+
content: fullTranscript.trim(),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
this.logger.error('Error generating podcast episode:', error);
|
|
465
|
+
await notifyArtifactFailed(this.db, {
|
|
466
|
+
userId,
|
|
467
|
+
workspaceId: input.workspaceId,
|
|
468
|
+
artifactType: ArtifactType.PODCAST_EPISODE,
|
|
469
|
+
artifactId: newArtifact.id,
|
|
470
|
+
title: input.podcastData.title,
|
|
471
|
+
message: error instanceof Error ? error.message : 'Podcast generation failed.',
|
|
472
|
+
}).catch(() => { });
|
|
473
|
+
await this.db.artifact.delete({ where: { id: newArtifact.id } });
|
|
474
|
+
await PusherService.emitError(input.workspaceId, `Failed to generate podcast episode: ${error instanceof Error ? error.message : 'Unknown error'}`, 'podcast');
|
|
475
|
+
throw new TRPCError({
|
|
476
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
477
|
+
message: `Failed to generate podcast episode: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async deleteSegment(segmentId) {
|
|
482
|
+
return this.db.podcastSegment.delete({ where: { id: segmentId } });
|
|
483
|
+
}
|
|
484
|
+
async getEpisodeSchema(userId, episodeId) {
|
|
485
|
+
const episode = await this.db.artifact.findFirst({
|
|
486
|
+
where: {
|
|
487
|
+
id: episodeId,
|
|
488
|
+
type: ArtifactType.PODCAST_EPISODE,
|
|
489
|
+
workspace: workspaceAccessWhere(userId),
|
|
490
|
+
},
|
|
491
|
+
include: {
|
|
492
|
+
versions: { orderBy: { version: 'desc' }, take: 1 },
|
|
493
|
+
podcastSegments: { orderBy: { order: 'asc' } },
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
if (!episode)
|
|
497
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
498
|
+
const latestVersion = episode.versions[0];
|
|
499
|
+
if (!latestVersion)
|
|
500
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'No version found' });
|
|
501
|
+
const metadata = podcastMetadataSchema.parse(latestVersion.data);
|
|
502
|
+
return {
|
|
503
|
+
segments: episode.podcastSegments.map((s) => ({
|
|
504
|
+
id: s.id,
|
|
505
|
+
title: s.title,
|
|
506
|
+
startTime: s.startTime,
|
|
507
|
+
duration: s.duration,
|
|
508
|
+
keyPoints: s.keyPoints,
|
|
509
|
+
order: s.order,
|
|
510
|
+
})),
|
|
511
|
+
summary: metadata.summary,
|
|
512
|
+
metadata: {
|
|
513
|
+
title: metadata.title,
|
|
514
|
+
description: metadata.description,
|
|
515
|
+
totalDuration: metadata.totalDuration,
|
|
516
|
+
speakers: metadata.speakers,
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
async updateEpisode(userId, input) {
|
|
521
|
+
const episode = await this.db.artifact.findFirst({
|
|
522
|
+
where: {
|
|
523
|
+
id: input.episodeId,
|
|
524
|
+
type: ArtifactType.PODCAST_EPISODE,
|
|
525
|
+
workspace: workspaceAccessWhere(userId),
|
|
526
|
+
},
|
|
527
|
+
include: {
|
|
528
|
+
versions: { orderBy: { version: 'desc' }, take: 1 },
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
if (!episode)
|
|
532
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
533
|
+
const latestVersion = episode.versions[0];
|
|
534
|
+
if (!latestVersion)
|
|
535
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'No version found' });
|
|
536
|
+
const metadata = podcastMetadataSchema.parse(latestVersion.data);
|
|
537
|
+
if (input.title)
|
|
538
|
+
metadata.title = input.title;
|
|
539
|
+
if (input.description)
|
|
540
|
+
metadata.description = input.description;
|
|
541
|
+
const nextVersion = (latestVersion.version || 0) + 1;
|
|
542
|
+
await this.db.artifactVersion.create({
|
|
543
|
+
data: {
|
|
544
|
+
artifactId: input.episodeId,
|
|
545
|
+
version: nextVersion,
|
|
546
|
+
content: latestVersion.content,
|
|
547
|
+
data: metadata,
|
|
548
|
+
createdById: userId,
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
return this.db.artifact.update({
|
|
552
|
+
where: { id: input.episodeId },
|
|
553
|
+
data: {
|
|
554
|
+
title: input.title ?? episode.title,
|
|
555
|
+
description: input.description ?? episode.description,
|
|
556
|
+
updatedAt: new Date(),
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
async deleteEpisode(userId, episodeId) {
|
|
561
|
+
const episode = await this.db.artifact.findFirst({
|
|
562
|
+
where: {
|
|
563
|
+
id: episodeId,
|
|
564
|
+
type: ArtifactType.PODCAST_EPISODE,
|
|
565
|
+
workspace: workspaceAccessWhere(userId),
|
|
566
|
+
},
|
|
567
|
+
include: {
|
|
568
|
+
versions: { orderBy: { version: 'desc' }, take: 1 },
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
if (!episode)
|
|
572
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
573
|
+
try {
|
|
574
|
+
await PusherService.emitTaskComplete(episode.workspaceId, 'podcast_deletion_start', {
|
|
575
|
+
episodeId,
|
|
576
|
+
episodeTitle: episode.title || 'Untitled Episode',
|
|
577
|
+
});
|
|
578
|
+
const segments = await this.db.podcastSegment.findMany({
|
|
579
|
+
where: { artifactId: episodeId },
|
|
580
|
+
});
|
|
581
|
+
for (const segment of segments) {
|
|
582
|
+
if (segment.objectKey) {
|
|
583
|
+
try {
|
|
584
|
+
await deleteFromSupabase(segment.objectKey);
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
this.logger.error(`Failed to delete audio file ${segment.objectKey}:`, error);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
await this.db.podcastSegment.deleteMany({ where: { artifactId: episodeId } });
|
|
592
|
+
await this.db.artifactVersion.deleteMany({ where: { artifactId: episodeId } });
|
|
593
|
+
await this.db.artifact.delete({ where: { id: episodeId } });
|
|
594
|
+
await PusherService.emitTaskComplete(episode.workspaceId, 'podcast_deletion_complete', {
|
|
595
|
+
episodeId,
|
|
596
|
+
episodeTitle: episode.title || 'Untitled Episode',
|
|
597
|
+
});
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
this.logger.error('Error deleting episode:', error);
|
|
602
|
+
await PusherService.emitError(episode.workspaceId, `Failed to delete episode: ${error instanceof Error ? error.message : 'Unknown error'}`, 'podcast');
|
|
603
|
+
throw new TRPCError({
|
|
604
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
605
|
+
message: 'Failed to delete episode',
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async getSegment(userId, segmentId) {
|
|
610
|
+
const segment = await this.db.podcastSegment.findFirst({
|
|
611
|
+
where: {
|
|
612
|
+
id: segmentId,
|
|
613
|
+
artifact: { workspace: workspaceAccessWhere(userId) },
|
|
614
|
+
},
|
|
615
|
+
include: { artifact: true },
|
|
616
|
+
});
|
|
617
|
+
if (!segment)
|
|
618
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
619
|
+
let audioUrl = null;
|
|
620
|
+
if (segment.objectKey) {
|
|
621
|
+
try {
|
|
622
|
+
audioUrl = await generateSignedUrl(segment.objectKey, 24);
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
this.logger.error(`Failed to generate signed URL for segment ${segment.id}:`, error);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
id: segment.id,
|
|
630
|
+
title: segment.title,
|
|
631
|
+
content: segment.content,
|
|
632
|
+
startTime: segment.startTime,
|
|
633
|
+
duration: segment.duration,
|
|
634
|
+
order: segment.order,
|
|
635
|
+
keyPoints: segment.keyPoints,
|
|
636
|
+
audioUrl,
|
|
637
|
+
objectKey: segment.objectKey,
|
|
638
|
+
meta: segment.meta,
|
|
639
|
+
createdAt: segment.createdAt,
|
|
640
|
+
updatedAt: segment.updatedAt,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
getAvailableVoices() {
|
|
644
|
+
return [
|
|
645
|
+
{ id: 'alloy', name: 'Alloy', description: 'Neutral, balanced voice' },
|
|
646
|
+
{ id: 'echo', name: 'Echo', description: 'Clear, professional voice' },
|
|
647
|
+
{ id: 'fable', name: 'Fable', description: 'Warm, storytelling voice' },
|
|
648
|
+
{ id: 'onyx', name: 'Onyx', description: 'Deep, authoritative voice' },
|
|
649
|
+
{ id: 'nova', name: 'Nova', description: 'Friendly, conversational voice' },
|
|
650
|
+
{ id: 'shimmer', name: 'Shimmer', description: 'Bright, energetic voice' },
|
|
651
|
+
];
|
|
652
|
+
}
|
|
653
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { PrismaClient, WorksheetQuestion } from '@prisma/client';
|
|
2
|
+
import { BaseService } from '../base.service.js';
|
|
3
|
+
export type WorksheetQuestionWithProgress = Omit<WorksheetQuestion, 'meta'> & {
|
|
4
|
+
meta: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
type ProgressLike = {
|
|
7
|
+
worksheetQuestionId: string;
|
|
8
|
+
modified: boolean;
|
|
9
|
+
userAnswer: string | null;
|
|
10
|
+
completedAt: Date | null;
|
|
11
|
+
meta?: unknown;
|
|
12
|
+
};
|
|
13
|
+
export declare function mergeWorksheetQuestionProgress(questions: WorksheetQuestion[], progressRows: ProgressLike[]): WorksheetQuestionWithProgress[];
|
|
14
|
+
export declare function parseWorksheetGenerationContent(content: string): {
|
|
15
|
+
problems: Record<string, unknown>[];
|
|
16
|
+
title?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
estimatedTime?: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function parseQuestionMeta(meta: unknown): Record<string, unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Class wrapper around the pure worksheet-content helpers so consumers can
|
|
23
|
+
* depend on a service the same way they depend on other domain services.
|
|
24
|
+
* The standalone function exports remain part of the public surface.
|
|
25
|
+
*/
|
|
26
|
+
export declare class WorksheetContentService extends BaseService {
|
|
27
|
+
constructor(db: PrismaClient);
|
|
28
|
+
mergeProgress(questions: WorksheetQuestion[], progress: ProgressLike[]): WorksheetQuestionWithProgress[];
|
|
29
|
+
parseGenerationContent(content: string): {
|
|
30
|
+
problems: Record<string, unknown>[];
|
|
31
|
+
title?: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
estimatedTime?: string;
|
|
34
|
+
};
|
|
35
|
+
parseQuestionMeta(meta: unknown): Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
export {};
|