MicProtocol / apps /api /dist /queue /content.processor.js
Benaah's picture
Upload folder using huggingface_hub (part 2)
9f29339 verified
Raw History Blame Contribute Delete
12.6 kB
"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