UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

231 lines 11.6 kB
/** * Presentation Orchestrator * * Main orchestration module for PPT generation. * Coordinates the full pipeline: validation → content planning → slide generation → assembly → file output. * * Follows the video handler pattern (vertexVideoHandler.ts): * - Standalone module with clear responsibility * - Uses AI provider for generation * - Returns structured result * * @module presentation/presentationOrchestrator */ import { loadPptxGenJS } from "./slideGenerator.js"; import * as fs from "fs/promises"; import { PPTError, PPT_ERROR_CODES } from "./pptError.js"; import { generateContentPlan, postProcessPlan } from "./contentPlanner.js"; import { SlideGenerator } from "./slideGenerator.js"; import { PPT_GENERATION_TIMEOUT_MS } from "./constants.js"; import { logger } from "../../utils/logger.js"; import { withTimeout, ErrorFactory } from "../../utils/errorHandling.js"; import { SpanSerializer, SpanType, SpanStatus, getMetricsAggregator, } from "../../observability/index.js"; import { generateOutputPath, ensureOutputDirectory, normalizeLogoConfig, getLayoutName, getFailureStage, toError, } from "./utils.js"; // ============================================================================ // MAIN ORCHESTRATION FUNCTION // ============================================================================ /** * Generate a complete PowerPoint presentation * * This is the main entry point for PPT generation. It orchestrates: * 1. Content planning via AI * 2. Individual slide generation (with images) * 3. PPTX assembly and file output * * @param options - Presentation generation options * @returns Promise<PPTGenerationResult> - Result with file path and metadata * * @example * ```typescript * const result = await generatePresentation({ * context: extractPPTContext(options), * provider: aiProvider, * neurolink: neurolinkInstance, * }); * * console.log(`Presentation saved: ${result.filePath}`); * ``` */ export async function generatePresentation(options) { const span = SpanSerializer.createSpan(SpanType.PPT_GENERATION, "ppt.orchestrate", { "ppt.operation": "orchestrate", "ppt.slideCount": options.context.pages, "ppt.theme": typeof options.context.theme === "string" ? options.context.theme : "custom", }); const state = { startTime: Date.now(), contentPlan: null, slides: null, outputPath: null, }; const { context, provider, providerName, modelName, neurolink, imageProvider, imageModel, } = options; logger.info("[PresentationOrchestrator] Starting presentation generation", { topic: context.topic.substring(0, 100), pages: context.pages, theme: context.theme, audience: context.audience, tone: context.tone, generateAIImages: context.generateAIImages, provider: context.provider, }); try { // ========================================================================= // STEP 1: Content Planning // ========================================================================= logger.info("[PresentationOrchestrator] Step 1: Content planning..."); const planResult = await withTimeout(generateContentPlan(context, provider), PPT_GENERATION_TIMEOUT_MS / 2, // Half the total timeout for planning ErrorFactory.toolTimeout("contentPlanning", PPT_GENERATION_TIMEOUT_MS / 2)); // Post-process: ensure title and thank-you slides state.contentPlan = postProcessPlan(planResult); // Update span attributes with post-processed plan values (AI may have changed slide count/theme) span.attributes["ppt.slideCount"] = state.contentPlan.totalSlides; span.attributes["ppt.theme"] = typeof state.contentPlan.theme === "string" ? state.contentPlan.theme : "custom"; logger.info("[PresentationOrchestrator] Content plan ready", { title: state.contentPlan.title, totalSlides: state.contentPlan.totalSlides, theme: state.contentPlan.theme, audience: state.contentPlan.audience, tone: state.contentPlan.tone, }); // ========================================================================= // STEP 2: Slide Generation (with images) // ========================================================================= logger.info("[PresentationOrchestrator] Step 2: Generating slides..."); // Normalize logo - context.logo can be Buffer | string or undefined // Convert undefined to null for normalizeLogoConfig const logoInput = context.logo ?? null; const normalizedLogo = normalizeLogoConfig(logoInput); // Use theme from content plan (AI may have chosen it) const themeForSlideGen = state.contentPlan.theme || context.theme; const slideGeneratorConfig = { theme: themeForSlideGen, generateAIImages: context.generateAIImages, aspectRatio: context.aspectRatio, provider: imageProvider ?? context.provider ?? "vertex", imageModel: imageModel ?? "gemini-2.5-flash-image", logo: normalizedLogo ?? undefined, // Pass user-provided images for slides (takes priority over AI generation) userImages: context.images, neurolink, }; const slideGenerator = new SlideGenerator(slideGeneratorConfig); const batchResult = await withTimeout(slideGenerator.generateSlides(state.contentPlan.slides), PPT_GENERATION_TIMEOUT_MS / 2, ErrorFactory.toolTimeout("slideGeneration", PPT_GENERATION_TIMEOUT_MS / 2)); state.slides = batchResult.slides; logger.info("[PresentationOrchestrator] Slides generated", { totalSlides: state.slides.length, totalImages: batchResult.totalImages, failedImages: batchResult.failedImages, generationTime: batchResult.generationTime, }); // ========================================================================= // STEP 3: PPTX Assembly // ========================================================================= logger.info("[PresentationOrchestrator] Step 3: Assembling PPTX..."); // Create presentation instance // Use pptxgenjs directly - the SlideGenerator.renderSlide handles type conversion internally const PptxGenJS = await loadPptxGenJS(); const pptxInstance = new PptxGenJS(); // Set presentation metadata using pptxgenjs API directly pptxInstance.title = state.contentPlan.title; pptxInstance.subject = `Generated presentation about: ${context.topic.substring(0, 100)}`; pptxInstance.author = "NeuroLink PPT Generator"; pptxInstance.company = "NeuroLink"; pptxInstance.layout = getLayoutName(context.aspectRatio); // Sort slides by number (should already be sorted, but ensure) const sortedSlides = [...state.slides].sort((a, b) => a.slideNumber - b.slideNumber); // Render each slide to the presentation // Note: pptxInstance is structurally compatible with PptxPresentation for (let i = 0; i < sortedSlides.length; i++) { const completeSlide = sortedSlides[i]; slideGenerator.renderSlide(pptxInstance, completeSlide, i + 1, sortedSlides.length); } logger.info("[PresentationOrchestrator] PPTX assembled", { totalSlides: sortedSlides.length, layout: pptxInstance.layout, title: pptxInstance.title, }); // ========================================================================= // STEP 4: File Output // ========================================================================= logger.info("[PresentationOrchestrator] Step 4: Writing file..."); state.outputPath = generateOutputPath(context); await withTimeout(ensureOutputDirectory(state.outputPath), PPT_GENERATION_TIMEOUT_MS / 4, ErrorFactory.toolTimeout("outputDirectory", PPT_GENERATION_TIMEOUT_MS / 4)); // Write the file with timeout await withTimeout(pptxInstance.writeFile({ fileName: state.outputPath }), PPT_GENERATION_TIMEOUT_MS / 4, ErrorFactory.toolTimeout("pptxWrite", PPT_GENERATION_TIMEOUT_MS / 4)); // Get file size (optional, don't fail if can't get it) let fileSize = 0; try { const stats = await withTimeout(fs.stat(state.outputPath), PPT_GENERATION_TIMEOUT_MS / 8, ErrorFactory.toolTimeout("pptxStat", PPT_GENERATION_TIMEOUT_MS / 8)); fileSize = stats.size; } catch { logger.warn("[PresentationOrchestrator] Could not get file size"); } const totalTime = Date.now() - state.startTime; logger.info("[PresentationOrchestrator] Presentation generation complete", { filePath: state.outputPath, fileSize, totalSlides: sortedSlides.length, totalTime, }); // ========================================================================= // STEP 5: Return Result // ========================================================================= const endedSpan = SpanSerializer.endSpan(span, SpanStatus.OK); getMetricsAggregator().recordSpan(endedSpan); neurolink?.recordMetricsSpan(endedSpan); // Use values from content plan (AI may have chosen them if "AI will decide" was passed) const finalTheme = state.contentPlan?.theme || context.theme; const finalAudience = state.contentPlan?.audience || context.audience; const finalTone = state.contentPlan?.tone || context.tone; return { filePath: state.outputPath, totalSlides: sortedSlides.length, format: "pptx", provider: providerName, model: modelName, metadata: { theme: finalTheme, audience: finalAudience, tone: finalTone, imageModel: context.generateAIImages ? (imageModel ?? "gemini-2.5-flash-image") : "", fileSize: fileSize > 0 ? fileSize : 0, }, }; } catch (error) { const endedSpan = SpanSerializer.endSpan(span, SpanStatus.ERROR); endedSpan.statusMessage = error instanceof Error ? error.message : String(error); getMetricsAggregator().recordSpan(endedSpan); neurolink?.recordMetricsSpan(endedSpan); // Re-throw PPTError as-is if (error instanceof PPTError) { logger.error("[PresentationOrchestrator] Generation failed", { error: error.message, code: error.code, context: error.context, }); throw error; } // Wrap other errors const errorObj = toError(error); logger.error("[PresentationOrchestrator] Unexpected error", { error: errorObj.message, stage: getFailureStage(state), }); throw new PPTError(`Presentation generation failed: ${errorObj.message}`, PPT_ERROR_CODES.ASSEMBLY_FAILED, { topic: context.topic.substring(0, 100), stage: getFailureStage(state), elapsedTime: Date.now() - state.startTime, }, errorObj); } } //# sourceMappingURL=presentationOrchestrator.js.map