@goscribe/server 1.0.10 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/ANALYSIS_PROGRESS_SPEC.md +463 -0
  2. package/PROGRESS_QUICK_REFERENCE.md +239 -0
  3. package/dist/lib/ai-session.d.ts +20 -9
  4. package/dist/lib/ai-session.js +316 -80
  5. package/dist/lib/auth.d.ts +35 -2
  6. package/dist/lib/auth.js +88 -15
  7. package/dist/lib/env.d.ts +32 -0
  8. package/dist/lib/env.js +46 -0
  9. package/dist/lib/errors.d.ts +33 -0
  10. package/dist/lib/errors.js +78 -0
  11. package/dist/lib/inference.d.ts +4 -1
  12. package/dist/lib/inference.js +9 -11
  13. package/dist/lib/logger.d.ts +62 -0
  14. package/dist/lib/logger.js +342 -0
  15. package/dist/lib/podcast-prompts.d.ts +43 -0
  16. package/dist/lib/podcast-prompts.js +135 -0
  17. package/dist/lib/pusher.d.ts +1 -0
  18. package/dist/lib/pusher.js +14 -2
  19. package/dist/lib/storage.d.ts +3 -3
  20. package/dist/lib/storage.js +51 -47
  21. package/dist/lib/validation.d.ts +51 -0
  22. package/dist/lib/validation.js +64 -0
  23. package/dist/routers/_app.d.ts +697 -111
  24. package/dist/routers/_app.js +5 -0
  25. package/dist/routers/auth.d.ts +11 -1
  26. package/dist/routers/chat.d.ts +11 -1
  27. package/dist/routers/flashcards.d.ts +205 -6
  28. package/dist/routers/flashcards.js +144 -66
  29. package/dist/routers/members.d.ts +165 -0
  30. package/dist/routers/members.js +531 -0
  31. package/dist/routers/podcast.d.ts +78 -63
  32. package/dist/routers/podcast.js +330 -393
  33. package/dist/routers/studyguide.d.ts +11 -1
  34. package/dist/routers/worksheets.d.ts +124 -13
  35. package/dist/routers/worksheets.js +123 -50
  36. package/dist/routers/workspace.d.ts +213 -26
  37. package/dist/routers/workspace.js +303 -181
  38. package/dist/server.js +12 -4
  39. package/dist/services/flashcard-progress.service.d.ts +183 -0
  40. package/dist/services/flashcard-progress.service.js +383 -0
  41. package/dist/services/flashcard.service.d.ts +183 -0
  42. package/dist/services/flashcard.service.js +224 -0
  43. package/dist/services/podcast-segment-reorder.d.ts +0 -0
  44. package/dist/services/podcast-segment-reorder.js +107 -0
  45. package/dist/services/podcast.service.d.ts +0 -0
  46. package/dist/services/podcast.service.js +326 -0
  47. package/dist/services/worksheet.service.d.ts +0 -0
  48. package/dist/services/worksheet.service.js +295 -0
  49. package/dist/trpc.d.ts +13 -2
  50. package/dist/trpc.js +55 -6
  51. package/dist/types/index.d.ts +126 -0
  52. package/dist/types/index.js +1 -0
  53. package/package.json +3 -2
  54. package/prisma/schema.prisma +142 -4
  55. package/src/lib/ai-session.ts +356 -85
  56. package/src/lib/auth.ts +113 -19
  57. package/src/lib/env.ts +59 -0
  58. package/src/lib/errors.ts +92 -0
  59. package/src/lib/inference.ts +11 -11
  60. package/src/lib/logger.ts +405 -0
  61. package/src/lib/pusher.ts +15 -3
  62. package/src/lib/storage.ts +56 -51
  63. package/src/lib/validation.ts +75 -0
  64. package/src/routers/_app.ts +5 -0
  65. package/src/routers/chat.ts +2 -23
  66. package/src/routers/flashcards.ts +108 -24
  67. package/src/routers/members.ts +586 -0
  68. package/src/routers/podcast.ts +385 -420
  69. package/src/routers/worksheets.ts +117 -35
  70. package/src/routers/workspace.ts +328 -195
  71. package/src/server.ts +13 -4
  72. package/src/services/flashcard-progress.service.ts +541 -0
  73. package/src/trpc.ts +59 -6
  74. package/src/types/index.ts +165 -0
  75. package/AUTH_FRONTEND_SPEC.md +0 -21
  76. package/CHAT_FRONTEND_SPEC.md +0 -474
  77. package/DATABASE_SETUP.md +0 -165
  78. package/MEETINGSUMMARY_FRONTEND_SPEC.md +0 -28
  79. package/PODCAST_FRONTEND_SPEC.md +0 -595
  80. package/STUDYGUIDE_FRONTEND_SPEC.md +0 -18
  81. package/WORKSHEETS_FRONTEND_SPEC.md +0 -26
  82. package/WORKSPACE_FRONTEND_SPEC.md +0 -47
  83. package/test-ai-integration.js +0 -134
@@ -0,0 +1,342 @@
1
+ import { createWriteStream, existsSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ export var LogLevel;
4
+ (function (LogLevel) {
5
+ LogLevel[LogLevel["ERROR"] = 0] = "ERROR";
6
+ LogLevel[LogLevel["WARN"] = 1] = "WARN";
7
+ LogLevel[LogLevel["INFO"] = 2] = "INFO";
8
+ LogLevel[LogLevel["DEBUG"] = 3] = "DEBUG";
9
+ LogLevel[LogLevel["TRACE"] = 4] = "TRACE";
10
+ })(LogLevel || (LogLevel = {}));
11
+ // Icons and colors for different log levels
12
+ const LOG_STYLES = {
13
+ [LogLevel.ERROR]: {
14
+ icon: '❌',
15
+ color: '\x1b[31m', // Red
16
+ bgColor: '\x1b[41m', // Red background
17
+ bold: '\x1b[1m',
18
+ },
19
+ [LogLevel.WARN]: {
20
+ icon: '⚠️ ',
21
+ color: '\x1b[33m', // Yellow
22
+ bgColor: '\x1b[43m', // Yellow background
23
+ bold: '\x1b[1m',
24
+ },
25
+ [LogLevel.INFO]: {
26
+ icon: 'ℹ️ ',
27
+ color: '\x1b[36m', // Cyan
28
+ bgColor: '\x1b[46m', // Cyan background
29
+ bold: '\x1b[1m',
30
+ },
31
+ [LogLevel.DEBUG]: {
32
+ icon: '🐛',
33
+ color: '\x1b[35m', // Magenta
34
+ bgColor: '\x1b[45m', // Magenta background
35
+ bold: '\x1b[1m',
36
+ },
37
+ [LogLevel.TRACE]: {
38
+ icon: '🔍',
39
+ color: '\x1b[37m', // White
40
+ bgColor: '\x1b[47m', // White background
41
+ bold: '\x1b[1m',
42
+ },
43
+ };
44
+ // Context icons for common services
45
+ const CONTEXT_ICONS = {
46
+ 'SERVER': '🖥️ ',
47
+ 'HTTP': '🌐',
48
+ 'API': '🔌',
49
+ 'AUTH': '🔐',
50
+ 'DATABASE': '🗄️ ',
51
+ 'DB': '🗄️ ',
52
+ 'TRPC': '⚡',
53
+ 'WORKSPACE': '📁',
54
+ 'WORKSHEET': '📝',
55
+ 'FLASHCARD': '🃏',
56
+ 'STUDYGUIDE': '📚',
57
+ 'PODCAST': '🎙️ ',
58
+ 'MEETING': '🤝',
59
+ 'CHAT': '💬',
60
+ 'FILE': '📄',
61
+ 'STORAGE': '💾',
62
+ 'CACHE': '⚡',
63
+ 'MIDDLEWARE': '🔧',
64
+ 'PERFORMANCE': '⚡',
65
+ 'SECURITY': '🛡️ ',
66
+ 'VALIDATION': '✅',
67
+ 'ERROR': '❌',
68
+ 'SUCCESS': '✅',
69
+ 'LOGGER': '📋',
70
+ };
71
+ class Logger {
72
+ constructor(config = {}) {
73
+ this.config = {
74
+ level: LogLevel.INFO,
75
+ enableConsole: true,
76
+ enableFile: false,
77
+ logDir: './logs',
78
+ maxFileSize: 10 * 1024 * 1024, // 10MB
79
+ maxFiles: 5,
80
+ format: 'pretty',
81
+ ...config,
82
+ };
83
+ if (this.config.enableFile) {
84
+ this.setupFileLogging();
85
+ }
86
+ }
87
+ setupFileLogging() {
88
+ if (!this.config.logDir)
89
+ return;
90
+ // Ensure log directory exists
91
+ if (!existsSync(this.config.logDir)) {
92
+ mkdirSync(this.config.logDir, { recursive: true });
93
+ }
94
+ const logFile = join(this.config.logDir, `app-${new Date().toISOString().split('T')[0]}.log`);
95
+ this.logStream = createWriteStream(logFile, { flags: 'a' });
96
+ }
97
+ shouldLog(level) {
98
+ return level <= this.config.level;
99
+ }
100
+ formatLogEntry(entry) {
101
+ if (this.config.format === 'json') {
102
+ return JSON.stringify(entry) + '\n';
103
+ }
104
+ // Pretty format with enhanced styling
105
+ const timestamp = this.formatTimestamp(entry.timestamp);
106
+ const level = this.formatLevel(entry.level);
107
+ const context = entry.context ? this.formatContext(entry.context) : '';
108
+ const metadata = entry.metadata ? this.formatMetadata(entry.metadata) : '';
109
+ const error = entry.error ? this.formatError(entry.error) : '';
110
+ return `${timestamp} ${level} ${context}${entry.message}${metadata}${error}`;
111
+ }
112
+ formatLevel(level) {
113
+ const levelNum = LogLevel[level];
114
+ const style = LOG_STYLES[levelNum];
115
+ return `${style.color}${style.bold}${level.padEnd(5)}\x1b[0m`;
116
+ }
117
+ formatTimestamp(timestamp) {
118
+ const date = new Date(timestamp);
119
+ const time = date.toLocaleTimeString('en-US', {
120
+ hour12: false,
121
+ hour: '2-digit',
122
+ minute: '2-digit',
123
+ second: '2-digit',
124
+ });
125
+ return `\x1b[90m${time}\x1b[0m`; // Gray color
126
+ }
127
+ formatContext(context) {
128
+ const icon = CONTEXT_ICONS[context.toUpperCase()] || '📦';
129
+ return `\x1b[94m${icon}${context}\x1b[0m `; // Blue color
130
+ }
131
+ formatMetadata(metadata) {
132
+ const entries = Object.entries(metadata)
133
+ .map(([key, value]) => {
134
+ const formattedValue = this.formatValue(value);
135
+ return `\x1b[93m${key}\x1b[0m=\x1b[96m${formattedValue}\x1b[0m`;
136
+ })
137
+ .join(' \x1b[90m|\x1b[0m ');
138
+ return entries ? `\n \x1b[90m└─\x1b[0m \x1b[90m{\x1b[0m ${entries} \x1b[90m}\x1b[0m` : '';
139
+ }
140
+ formatValue(value) {
141
+ if (value === null)
142
+ return '\x1b[90mnull\x1b[0m';
143
+ if (value === undefined)
144
+ return '\x1b[90mundefined\x1b[0m';
145
+ if (typeof value === 'boolean')
146
+ return value ? '\x1b[92mtrue\x1b[0m' : '\x1b[91mfalse\x1b[0m';
147
+ if (typeof value === 'number')
148
+ return `\x1b[95m${value}\x1b[0m`;
149
+ if (typeof value === 'string')
150
+ return `\x1b[96m"${value}"\x1b[0m`;
151
+ if (typeof value === 'object') {
152
+ if (Array.isArray(value)) {
153
+ const items = value.map(item => this.formatValue(item)).join('\x1b[90m, \x1b[0m');
154
+ return `\x1b[90m[\x1b[0m${items}\x1b[90m]\x1b[0m`;
155
+ }
156
+ const objEntries = Object.entries(value)
157
+ .map(([k, v]) => `\x1b[93m${k}\x1b[0m:\x1b[96m${this.formatValue(v)}\x1b[0m`)
158
+ .join('\x1b[90m, \x1b[0m');
159
+ return `\x1b[90m{\x1b[0m${objEntries}\x1b[90m}\x1b[0m`;
160
+ }
161
+ return `\x1b[96m${String(value)}\x1b[0m`;
162
+ }
163
+ formatError(error) {
164
+ let errorStr = `\n \x1b[90m└─\x1b[0m \x1b[31m❌ \x1b[93m${error.name}\x1b[0m: \x1b[91m${error.message}\x1b[0m`;
165
+ if (error.stack) {
166
+ const stackLines = error.stack.split('\n').slice(1, 4); // Show first 3 stack lines
167
+ errorStr += `\n \x1b[90m└─ Stack:\x1b[0m`;
168
+ stackLines.forEach((line, index) => {
169
+ const isLast = index === stackLines.length - 1;
170
+ const connector = isLast ? '└─' : '├─';
171
+ errorStr += `\n \x1b[90m${connector} \x1b[0m\x1b[90m${line.trim()}\x1b[0m`;
172
+ });
173
+ }
174
+ return errorStr;
175
+ }
176
+ log(level, message, context, metadata, error) {
177
+ if (!this.shouldLog(level))
178
+ return;
179
+ const entry = {
180
+ timestamp: new Date().toISOString(),
181
+ level: LogLevel[level],
182
+ message,
183
+ context,
184
+ metadata,
185
+ error: error ? {
186
+ name: error.name,
187
+ message: error.message,
188
+ stack: error.stack,
189
+ } : undefined,
190
+ };
191
+ const formattedLog = this.formatLogEntry(entry);
192
+ if (this.config.enableConsole) {
193
+ // Enhanced console output with icons and colors
194
+ const style = LOG_STYLES[level];
195
+ const reset = '\x1b[0m';
196
+ // Create a beautiful log line with proper spacing and colors
197
+ const logLine = `${style.color}${style.icon} ${formattedLog}${reset}`;
198
+ console.log(logLine);
199
+ }
200
+ if (this.config.enableFile && this.logStream) {
201
+ this.logStream.write(formattedLog + '\n');
202
+ }
203
+ }
204
+ error(message, context, metadata, error) {
205
+ this.log(LogLevel.ERROR, message, context, metadata, error);
206
+ }
207
+ warn(message, context, metadata) {
208
+ this.log(LogLevel.WARN, message, context, metadata);
209
+ }
210
+ info(message, context, metadata) {
211
+ this.log(LogLevel.INFO, message, context, metadata);
212
+ }
213
+ debug(message, context, metadata) {
214
+ this.log(LogLevel.DEBUG, message, context, metadata);
215
+ }
216
+ trace(message, context, metadata) {
217
+ this.log(LogLevel.TRACE, message, context, metadata);
218
+ }
219
+ // Convenience methods for common use cases
220
+ http(method, url, statusCode, responseTime, context) {
221
+ const statusIcon = this.getHttpStatusIcon(statusCode);
222
+ const responseTimeStr = responseTime ? `${responseTime}ms` : undefined;
223
+ const metadata = {
224
+ method,
225
+ url,
226
+ statusCode,
227
+ responseTime: responseTimeStr,
228
+ };
229
+ this.info(`${statusIcon} ${method} ${url} - ${statusCode}`, context || 'HTTP', metadata);
230
+ }
231
+ getHttpStatusIcon(statusCode) {
232
+ if (statusCode >= 200 && statusCode < 300)
233
+ return '✅';
234
+ if (statusCode >= 300 && statusCode < 400)
235
+ return '↩️ ';
236
+ if (statusCode >= 400 && statusCode < 500)
237
+ return '⚠️ ';
238
+ if (statusCode >= 500)
239
+ return '❌';
240
+ return '❓';
241
+ }
242
+ database(operation, table, duration, context) {
243
+ const operationIcon = this.getDatabaseOperationIcon(operation);
244
+ const durationStr = duration ? `${duration}ms` : undefined;
245
+ const metadata = {
246
+ operation,
247
+ table,
248
+ duration: durationStr,
249
+ };
250
+ this.debug(`${operationIcon} ${operation} on ${table}`, context || 'DATABASE', metadata);
251
+ }
252
+ getDatabaseOperationIcon(operation) {
253
+ const op = operation.toUpperCase();
254
+ if (op.includes('SELECT'))
255
+ return '🔍';
256
+ if (op.includes('INSERT'))
257
+ return '➕';
258
+ if (op.includes('UPDATE'))
259
+ return '✏️ ';
260
+ if (op.includes('DELETE'))
261
+ return '🗑️ ';
262
+ if (op.includes('CREATE'))
263
+ return '🏗️ ';
264
+ if (op.includes('DROP'))
265
+ return '💥';
266
+ return '🗄️ ';
267
+ }
268
+ auth(action, userId, context) {
269
+ const metadata = {
270
+ action,
271
+ userId,
272
+ };
273
+ this.info(`Auth ${action}`, context, metadata);
274
+ }
275
+ trpc(procedure, input, output, duration, context) {
276
+ const metadata = {
277
+ procedure,
278
+ input: input ? JSON.stringify(input) : undefined,
279
+ output: output ? JSON.stringify(output) : undefined,
280
+ duration: duration ? `${duration}ms` : undefined,
281
+ };
282
+ this.debug(`tRPC ${procedure}`, context, metadata);
283
+ }
284
+ // Method to update configuration at runtime
285
+ updateConfig(newConfig) {
286
+ this.config = { ...this.config, ...newConfig };
287
+ if (newConfig.enableFile && !this.logStream) {
288
+ this.setupFileLogging();
289
+ }
290
+ else if (!newConfig.enableFile && this.logStream) {
291
+ this.logStream.end();
292
+ this.logStream = undefined;
293
+ }
294
+ }
295
+ // Progress indicator for long-running operations
296
+ progress(message, current, total, context) {
297
+ const percentage = Math.round((current / total) * 100);
298
+ const progressBar = this.createProgressBar(percentage);
299
+ this.info(`${progressBar} ${message} (${current}/${total} - ${percentage}%)`, context || 'PROGRESS');
300
+ }
301
+ createProgressBar(percentage, width = 20) {
302
+ const filled = Math.round((percentage / 100) * width);
303
+ const empty = width - filled;
304
+ const bar = '█'.repeat(filled) + '░'.repeat(empty);
305
+ return `[${bar}]`;
306
+ }
307
+ // Success and failure helpers
308
+ success(message, context, metadata) {
309
+ this.info(`✅ ${message}`, context, metadata);
310
+ }
311
+ failure(message, context, metadata, error) {
312
+ this.error(`❌ ${message}`, context, metadata, error);
313
+ }
314
+ // Method to close file streams (useful for graceful shutdown)
315
+ close() {
316
+ if (this.logStream) {
317
+ this.logStream.end();
318
+ }
319
+ }
320
+ }
321
+ // Create default logger instance
322
+ const defaultConfig = {
323
+ level: process.env.NODE_ENV === 'production' ? LogLevel.INFO : LogLevel.DEBUG,
324
+ enableConsole: true,
325
+ enableFile: process.env.NODE_ENV === 'production',
326
+ logDir: './logs',
327
+ format: process.env.NODE_ENV === 'production' ? 'json' : 'pretty',
328
+ };
329
+ export const logger = new Logger(defaultConfig);
330
+ // Export the Logger class for custom instances
331
+ export { Logger };
332
+ // Graceful shutdown handling
333
+ process.on('SIGINT', () => {
334
+ logger.info('Received SIGINT, closing logger...');
335
+ logger.close();
336
+ process.exit(0);
337
+ });
338
+ process.on('SIGTERM', () => {
339
+ logger.info('Received SIGTERM, closing logger...');
340
+ logger.close();
341
+ process.exit(0);
342
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Podcast generation prompts
3
+ */
4
+ export interface StructurePromptParams {
5
+ title: string;
6
+ description?: string;
7
+ userPrompt: string;
8
+ studyGuideContent: string;
9
+ generateIntro: boolean;
10
+ generateOutro: boolean;
11
+ }
12
+ export interface RegenerateSegmentPromptParams {
13
+ oldContent: string;
14
+ userPrompt: string;
15
+ segmentTitle: string;
16
+ context: {
17
+ episodeTitle: string;
18
+ allSegmentTitles: string[];
19
+ segmentIndex: number;
20
+ };
21
+ }
22
+ /**
23
+ * Generate prompt for structuring podcast content
24
+ */
25
+ export declare function createStructurePrompt(params: StructurePromptParams): string;
26
+ /**
27
+ * Generate prompt for episode summary
28
+ */
29
+ export declare function createSummaryPrompt(episodeTitle: string, segments: Array<{
30
+ title: string;
31
+ keyPoints: string[];
32
+ }>): string;
33
+ /**
34
+ * Generate improved prompt for segment regeneration
35
+ */
36
+ export declare function createRegenerateSegmentPrompt(params: RegenerateSegmentPromptParams): string;
37
+ /**
38
+ * Validate environment for podcast generation
39
+ */
40
+ export declare function validatePodcastEnvironment(): {
41
+ valid: boolean;
42
+ errors: string[];
43
+ };
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Podcast generation prompts
3
+ */
4
+ /**
5
+ * Generate prompt for structuring podcast content
6
+ */
7
+ export function createStructurePrompt(params) {
8
+ const { title, description, userPrompt, studyGuideContent, generateIntro, generateOutro } = params;
9
+ return `You are an expert podcast content creator and scriptwriter. Your task is to create an engaging, educational podcast episode with professional structure and natural conversational language.
10
+
11
+ OBJECTIVE:
12
+ Create a complete podcast episode that is informative, engaging, and flows naturally. The content should be suitable for audio delivery with a conversational tone.
13
+
14
+ USER REQUEST:
15
+ Title: ${title}
16
+ Description: ${description || 'No description provided'}
17
+ Prompt: ${userPrompt}
18
+
19
+ REQUIREMENTS:
20
+ 1. Create segments that are 2-5 minutes each when spoken at a normal pace
21
+ 2. Use natural, conversational language (avoid reading lists, use storytelling)
22
+ 3. Include smooth transitions between segments
23
+ 4. Each segment should have a clear focus and takeaways
24
+ 5. Make content accessible but informative
25
+ ${generateIntro ? '6. Start with an engaging hook that captures attention immediately' : ''}
26
+ ${generateOutro ? '7. End with a strong conclusion that summarizes and motivates action' : ''}
27
+
28
+ CONTEXT (if available):
29
+ ${studyGuideContent ? `Study Guide Content (use as reference, don't copy verbatim):\n${studyGuideContent.substring(0, 2000)}` : 'No study guide available'}
30
+
31
+ OUTPUT FORMAT (strict JSON):
32
+ {
33
+ "episodeTitle": "Enhanced, engaging title for the podcast",
34
+ "totalEstimatedDuration": "XX minutes",
35
+ "segments": [
36
+ {
37
+ "title": "Segment title (concise and descriptive)",
38
+ "content": "Natural conversational script. Use 'I', 'we', 'you'. Tell stories. Ask rhetorical questions. Use analogies. Make it sound like a real conversation, not a lecture.",
39
+ "keyPoints": ["Main point 1", "Main point 2", "Main point 3"],
40
+ "estimatedDuration": "X minutes",
41
+ "order": 1
42
+ }
43
+ ]
44
+ }
45
+
46
+ IMPORTANT:
47
+ - Write like you're having a conversation with a friend
48
+ - Use contractions (I'm, you're, let's)
49
+ - Include verbal cues ("Now, here's the interesting part...", "You know what's fascinating?")
50
+ - Vary sentence length for natural rhythm
51
+ - Each segment should be self-contained but connected to the narrative
52
+
53
+ Return ONLY the JSON, no additional text.`;
54
+ }
55
+ /**
56
+ * Generate prompt for episode summary
57
+ */
58
+ export function createSummaryPrompt(episodeTitle, segments) {
59
+ return `Create a comprehensive analysis and summary for this podcast episode.
60
+
61
+ EPISODE: "${episodeTitle}"
62
+
63
+ SEGMENTS:
64
+ ${JSON.stringify(segments, null, 2)}
65
+
66
+ Generate a detailed summary with the following structure (return as JSON):
67
+
68
+ {
69
+ "executiveSummary": "2-3 sentence overview of what listeners will learn",
70
+ "learningObjectives": ["Specific, actionable objectives (3-5 items)"],
71
+ "keyConcepts": ["Main concepts covered (5-7 items)"],
72
+ "followUpActions": ["Concrete next steps listeners can take (3-5 items)"],
73
+ "targetAudience": "Detailed description of who will benefit most",
74
+ "prerequisites": ["Knowledge or background helpful but not required (2-3 items, or empty array)"],
75
+ "tags": ["Relevant searchable tags (5-8 items)"]
76
+ }
77
+
78
+ Make it specific, actionable, and useful for potential listeners trying to decide if this episode is for them.
79
+
80
+ Return ONLY the JSON, no additional text.`;
81
+ }
82
+ /**
83
+ * Generate improved prompt for segment regeneration
84
+ */
85
+ export function createRegenerateSegmentPrompt(params) {
86
+ const { oldContent, userPrompt, segmentTitle, context } = params;
87
+ return `You are revising a specific segment of a podcast episode.
88
+
89
+ EPISODE CONTEXT:
90
+ - Episode Title: "${context.episodeTitle}"
91
+ - All Segments: ${context.allSegmentTitles.map((t, i) => `${i + 1}. ${t}`).join(', ')}
92
+ - Current Segment: #${context.segmentIndex + 1} - "${segmentTitle}"
93
+
94
+ CURRENT CONTENT:
95
+ """
96
+ ${oldContent}
97
+ """
98
+
99
+ USER REQUEST FOR REVISION:
100
+ """
101
+ ${userPrompt}
102
+ """
103
+
104
+ TASK:
105
+ Rewrite this segment following the user's request while maintaining:
106
+ 1. Natural, conversational tone
107
+ 2. Connection to the overall episode narrative
108
+ 3. Appropriate length (2-5 minutes spoken)
109
+ 4. Engaging and informative content
110
+
111
+ GUIDELINES:
112
+ - If user asks to make it "longer", expand on concepts with examples and stories
113
+ - If user asks to make it "shorter", focus on key points and remove tangents
114
+ - If user asks to "add more detail", include specific examples, data, or anecdotes
115
+ - If user asks to "simplify", use clearer language and better analogies
116
+ - If user asks to "make it more engaging", add hooks, questions, and storytelling
117
+
118
+ Return ONLY the revised content as plain text, no JSON or formatting. Write as if you're speaking to listeners.`;
119
+ }
120
+ /**
121
+ * Validate environment for podcast generation
122
+ */
123
+ export function validatePodcastEnvironment() {
124
+ const errors = [];
125
+ if (!process.env.MURF_TTS_KEY) {
126
+ errors.push('MURF_TTS_KEY environment variable is not set');
127
+ }
128
+ if (!process.env.INFERENCE_API_URL && !process.env.OPENAI_API_KEY) {
129
+ errors.push('No AI inference API configured (need INFERENCE_API_URL or OPENAI_API_KEY)');
130
+ }
131
+ return {
132
+ valid: errors.length === 0,
133
+ errors,
134
+ };
135
+ }
@@ -9,6 +9,7 @@ export declare class PusherService {
9
9
  static emitPodcastComplete(workspaceId: string, artifact: any): Promise<void>;
10
10
  static emitOverallComplete(workspaceId: string, filename: string, artifacts: any): Promise<void>;
11
11
  static emitError(workspaceId: string, error: string, analysisType?: string): Promise<void>;
12
+ static emitAnalysisProgress(workspaceId: string, progress: any): Promise<void>;
12
13
  static emitChannelEvent(channelId: string, event: string, data: any): Promise<void>;
13
14
  }
14
15
  export default PusherService;
@@ -1,4 +1,5 @@
1
1
  import Pusher from 'pusher';
2
+ import { logger } from './logger.js';
2
3
  // Server-side Pusher instance
3
4
  export const pusher = new Pusher({
4
5
  appId: process.env.PUSHER_APP_ID || '',
@@ -13,9 +14,9 @@ export class PusherService {
13
14
  static async emitTaskComplete(workspaceId, event, data) {
14
15
  try {
15
16
  const channel = `workspace_${workspaceId}`;
16
- const eventName = `${workspaceId}_${event}`;
17
+ const eventName = event;
17
18
  await pusher.trigger(channel, eventName, data);
18
- console.log(`📡 Pusher notification sent: ${eventName} to ${channel}`);
19
+ logger.info(`📡 Pusher notification sent: ${eventName} to ${channel}`);
19
20
  }
20
21
  catch (error) {
21
22
  console.error('❌ Pusher notification error:', error);
@@ -78,6 +79,17 @@ export class PusherService {
78
79
  timestamp: new Date().toISOString(),
79
80
  });
80
81
  }
82
+ // Emit analysis progress update (single event for all progress updates)
83
+ static async emitAnalysisProgress(workspaceId, progress) {
84
+ try {
85
+ const channel = `workspace_${workspaceId}`;
86
+ await pusher.trigger(channel, 'analysis_progress', progress);
87
+ logger.info(`📡 Analysis progress sent to ${channel}: ${progress.status}`);
88
+ }
89
+ catch (error) {
90
+ console.error('❌ Pusher progress notification error:', error);
91
+ }
92
+ }
81
93
  // Emit channel-specific events (for chat messages)
82
94
  static async emitChannelEvent(channelId, event, data) {
83
95
  try {
@@ -3,9 +3,9 @@ export interface UploadResult {
3
3
  signedUrl?: string;
4
4
  objectKey: string;
5
5
  }
6
- export declare function uploadToGCS(fileBuffer: Buffer, fileName: string, contentType: string, makePublic?: boolean): Promise<UploadResult>;
6
+ export declare function uploadToSupabase(fileBuffer: Buffer, fileName: string, contentType: string, makePublic?: boolean): Promise<UploadResult>;
7
7
  export declare function generateSignedUrl(objectKey: string, expiresInHours?: number): Promise<string>;
8
- export declare function deleteFromGCS(objectKey: string): Promise<void>;
8
+ export declare function deleteFromSupabase(objectKey: string): Promise<void>;
9
9
  export declare function makeFilePublic(objectKey: string): Promise<void>;
10
10
  export declare function makeFilePrivate(objectKey: string): Promise<void>;
11
- export declare const bucket: import("@google-cloud/storage").Bucket;
11
+ export declare const supabaseClient: import("@supabase/supabase-js").SupabaseClient<any, "public", "public", any, any>;