"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var ContentProcessor_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.ContentProcessor = void 0; const bullmq_1 = require("@nestjs/bullmq"); const common_1 = require("@nestjs/common"); const bullmq_2 = require("bullmq"); const prisma_service_1 = require("../prisma/prisma.service"); const ai_service_1 = require("../ai/ai.service"); const export_renderer_service_1 = require("../common/export/export-renderer.service"); let ContentProcessor = ContentProcessor_1 = class ContentProcessor extends bullmq_1.WorkerHost { prisma; aiService; exportRenderer; logger = new common_1.Logger(ContentProcessor_1.name); constructor(prisma, aiService, exportRenderer) { super(); this.prisma = prisma; this.aiService = aiService; this.exportRenderer = exportRenderer; } async process(job) { this.logger.log(`Processing content job ${job.id} of type ${job.name}`); switch (job.name) { case "transcribe": return this.handleTranscription(job.data, job); case "generate-clips": return this.handleClipGeneration(job.data, job); case "export": return this.handleExport(job.data, job); default: this.logger.warn(`Unknown job type: ${job.name}`); } } async handleTranscription(data, job) { this.logger.log(`Starting transcription for episode ${data.episodeId}`); await job.updateProgress(10); const episode = await this.prisma.episode.findUnique({ where: { id: data.episodeId }, include: { podcast: { select: { workspaceId: true } } } }); if (!episode) { throw new Error(`Episode with ID ${data.episodeId} not found`); } const workspaceId = episode.podcast.workspaceId; await job.updateProgress(30); let audioBuffer; try { if (data.audioUrl && data.audioUrl.startsWith("http")) { const audioResponse = await fetch(data.audioUrl); if (!audioResponse.ok) { throw new Error(`Failed to download audio: ${audioResponse.statusText}`); } const arrayBuffer = await audioResponse.arrayBuffer(); audioBuffer = Buffer.from(arrayBuffer); } else { audioBuffer = Buffer.from("placeholder audio data"); } } catch (downloadError) { this.logger.warn(`Could not download audio from ${data.audioUrl}, using placeholder buffer. Error: ${downloadError instanceof Error ? downloadError.message : String(downloadError)}`); audioBuffer = Buffer.from("placeholder audio data"); } await job.updateProgress(50); const filename = data.audioUrl ? data.audioUrl.split("/").pop() || "audio.wav" : "audio.wav"; const result = await this.aiService.transcribeAudio(workspaceId, audioBuffer, filename, { language: data.language || "en", }); await job.updateProgress(80); const transcript = await this.prisma.transcript.create({ data: { episodeId: data.episodeId, fullText: result.text, segments: (result.segments || []), speakerDiarization: {}, language: result.language || data.language || "en", confidenceScore: null, processingDuration: null, }, }); await job.updateProgress(100); this.logger.log(`Transcription completed and saved for episode ${data.episodeId}`); return { transcriptId: transcript.id, text: result.text, duration: result.duration || 3600, }; } async handleClipGeneration(data, job) { this.logger.log(`Generating clips for episode ${data.episodeId}`); const candidates = await this.prisma.clip.findMany({ where: { episodeId: data.episodeId }, select: { id: true, startSeconds: true, endSeconds: true }, }); const clipIds = []; for (let i = 0; i < data.clips.length; i++) { const want = data.clips[i]; const progress = Math.round(((i + 1) / data.clips.length) * 100); await job.updateProgress(progress); const matched = candidates.find((c) => Math.abs(c.startSeconds - want.startSeconds) < 0.5 && Math.abs(c.endSeconds - want.endSeconds) < 0.5); if (matched) { clipIds.push(matched.id); } else { this.logger.warn(`No pre-existing Clip row matched AI-detected range ` + `${want.startSeconds}s-${want.endSeconds}s for episode ${data.episodeId}`); } } if (clipIds.length === 0) { this.logger.warn(`Clip generation produced no linked rows for episode ${data.episodeId}. ` + `The AI may have detected ranges that did not match the persisted clips.`); } return { clipIds }; } async handleExport(data, job) { this.logger.log(`Exporting clip ${data.clipId} for ${data.platform}`); await job.updateProgress(5); const exportJob = data.exportJobId ? await this.prisma.exportJob.findUnique({ where: { id: data.exportJobId }, select: { id: true, status: true }, }) : await this.prisma.exportJob.findFirst({ where: { clipId: data.clipId, platform: data.platform, }, select: { id: true, status: true }, orderBy: { createdAt: "desc" }, }); if (!exportJob) { throw new Error(`ExportJob not found for clip=${data.clipId} platform=${data.platform}`); } await this.prisma.exportJob.update({ where: { id: exportJob.id }, data: { status: "rendering", startedAt: new Date() }, }); const clip = await this.prisma.clip.findUnique({ where: { id: data.clipId }, select: { id: true, episodeId: true, startSeconds: true, endSeconds: true, suggestedHook: true, suggestedCaption: true, episode: { select: { recordingSession: { select: { tracks: { select: { trackUrl: true }, where: { status: "completed" }, orderBy: { createdAt: "desc" }, take: 1, }, }, }, podcast: { select: { workspaceId: true }, }, }, }, }, }); if (!clip) { throw new Error(`Clip ${data.clipId} not found for export`); } const workspaceId = data.workspaceId || clip.episode?.podcast?.workspaceId; if (!workspaceId) { throw new Error(`Cannot resolve workspaceId for export job ${exportJob.id} ` + `(clip=${data.clipId}, platform=${data.platform})`); } const trackUrl = clip.episode?.recordingSession?.tracks?.[0]?.trackUrl ?? null; const sourceUrl = data.sourceUrl ?? trackUrl ?? null; if (!sourceUrl) { this.logger.error(`Clip ${data.clipId} has no sourceUrl and no recording track URL. Export cannot proceed.`); await this.prisma.exportJob.update({ where: { id: exportJob.id }, data: { status: "failed", errorMessage: "No source audio URL available. Ensure the recording is completed before exporting.", }, }); throw new Error(`Clip ${data.clipId} has no source media URL. ` + `Complete the recording and processing pipeline before exporting.`); } await job.updateProgress(10); const caption = data.caption || clip.suggestedCaption || undefined; const hook = data.hook || clip.suggestedHook || undefined; try { const result = await this.exportRenderer.renderAndUpload({ exportJobId: exportJob.id, clipId: data.clipId, platform: data.platform, sourceUrl, caption, hook, renderingParams: data.renderingParams || {}, workspaceId, episodeId: data.episodeId || clip.episodeId, }, sourceUrl, clip.startSeconds, clip.endSeconds, (progress) => job.updateProgress(10 + Math.round(progress * 0.85))); if (!result) { throw new Error(`Export rendering returned null for clip ${data.clipId}`); } await this.prisma.exportJob.update({ where: { id: exportJob.id }, data: { outputUrl: result.cdnUrl, status: "completed", completedAt: new Date(), }, }); await job.updateProgress(100); this.logger.log(`Export completed for clip ${data.clipId} (${data.platform}): ${result.cdnUrl} (${result.fileSizeBytes} bytes)`); return { url: result.cdnUrl, r2Key: result.r2Key }; } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); this.logger.error(`Export failed for clip ${data.clipId} (${data.platform}): ${errorMessage}`); await this.prisma.exportJob.update({ where: { id: exportJob.id }, data: { status: "failed", errorMessage, }, }); throw err; } } onCompleted(job) { this.logger.log(`Content job ${job.id} completed successfully`); } onFailed(job, error) { this.logger.error(`Content job ${job.id} failed: ${error.message}`, error.stack); } onProgress(job, progress) { this.logger.debug(`Content job ${job.id} progress: ${progress}%`); } }; exports.ContentProcessor = ContentProcessor; __decorate([ (0, bullmq_1.OnWorkerEvent)("completed"), __metadata("design:type", Function), __metadata("design:paramtypes", [bullmq_2.Job]), __metadata("design:returntype", void 0) ], ContentProcessor.prototype, "onCompleted", null); __decorate([ (0, bullmq_1.OnWorkerEvent)("failed"), __metadata("design:type", Function), __metadata("design:paramtypes", [bullmq_2.Job, Error]), __metadata("design:returntype", void 0) ], ContentProcessor.prototype, "onFailed", null); __decorate([ (0, bullmq_1.OnWorkerEvent)("progress"), __metadata("design:type", Function), __metadata("design:paramtypes", [bullmq_2.Job, Number]), __metadata("design:returntype", void 0) ], ContentProcessor.prototype, "onProgress", null); exports.ContentProcessor = ContentProcessor = ContentProcessor_1 = __decorate([ (0, bullmq_1.Processor)("content", { concurrency: 2, }), __metadata("design:paramtypes", [prisma_service_1.PrismaService, ai_service_1.AIService, export_renderer_service_1.ExportRendererService]) ], ContentProcessor); //# sourceMappingURL=content.processor.js.map