@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
445 lines • 15.7 kB
JavaScript
/**
* PPT Utilities
*
* Contains provider utilities and helper functions for PPT generation.
*
* @module ppt/utils
*/
import * as fs from "fs/promises";
import * as path from "path";
import { hasProviderEnvVars } from "../../utils/providerUtils.js";
import { logger } from "../../utils/logger.js";
import { AIProviderName } from "../../constants/enums.js";
import { PPTError, PPT_ERROR_CODES } from "./pptError.js";
// ============================================================================
// CONTEXT EXTRACTION
// ============================================================================
/**
* Extract PPT generation context from GenerateOptions
*/
export function extractPPTContext(options) {
const pptOptions = options.output?.ppt;
if (!pptOptions) {
throw new PPTError("PPT options are required when mode is 'ppt'", PPT_ERROR_CODES.INVALID_INPUT, { field: "output.ppt" });
}
// Handle logo: prioritize logoPath, fallback to input.images[0]
let logo;
// First check logoPath in ppt options
if (pptOptions.logoPath) {
if (Buffer.isBuffer(pptOptions.logoPath) ||
typeof pptOptions.logoPath === "string") {
logo = pptOptions.logoPath;
}
else if (typeof pptOptions.logoPath === "object" &&
"data" in pptOptions.logoPath) {
const data = pptOptions.logoPath.data;
logo =
Buffer.isBuffer(data) || typeof data === "string" ? data : undefined;
}
}
// Extract all user-provided images from input.images
const images = [];
if (options.input?.images) {
for (const imageInput of options.input.images) {
if (Buffer.isBuffer(imageInput) || typeof imageInput === "string") {
images.push(imageInput);
}
else if (typeof imageInput === "object" && "data" in imageInput) {
const data = imageInput.data;
if (Buffer.isBuffer(data) || typeof data === "string") {
images.push(data);
}
}
}
}
// Fallback to input.images[0] if no logoPath (similar to video generation)
if (!logo && images.length > 0) {
logo = images[0];
}
return {
// Get topic from input.text
topic: options.input?.text || "",
// Pages is required
pages: pptOptions.pages,
// If undefined, set to "AI will decide" so AI can choose
theme: pptOptions.theme ?? "AI will decide",
audience: pptOptions.audience ?? "AI will decide",
tone: pptOptions.tone ?? "AI will decide",
generateAIImages: pptOptions.generateAIImages ?? false,
aspectRatio: pptOptions.aspectRatio || "16:9",
outputPath: pptOptions.outputPath,
logo,
// Pass all user images for slide content
images: images.length > 0 ? images : undefined,
provider: options.provider,
model: options.model,
};
}
// ============================================================================
// CONSTANTS
// ============================================================================
/**
* Valid providers for PPT generation.
* These providers support structured output capabilities required for content planning.
*/
export const PPT_VALID_PROVIDERS = [
"vertex",
"openai",
"azure",
"anthropic",
"google-ai",
"bedrock",
];
// ============================================================================
// PROVIDER UTILITIES
// ============================================================================
/**
* Get an effective PPT provider - handles all orchestration logic
*/
export async function getEffectivePPTProvider(currentProvider, currentProviderName, currentModelName, neurolink) {
const { ErrorFactory } = await import("../../utils/errorHandling.js");
const normalizedProvider = currentProviderName.toLowerCase();
if (PPT_VALID_PROVIDERS.includes(normalizedProvider)) {
const providerInstance = currentProvider;
const actualModelName = currentModelName ||
providerInstance?.modelName ||
providerInstance?.getDefaultModel?.() ||
"";
logger.debug("[PPT Utils] Current provider is valid for PPT", {
provider: currentProviderName,
model: actualModelName,
});
return {
provider: currentProvider,
providerName: currentProviderName,
modelName: actualModelName,
wasAutoSelected: false,
};
}
logger.info("[PPT Utils] Current provider not valid for PPT, auto-selecting", {
currentProvider: currentProviderName,
});
for (const provider of PPT_VALID_PROVIDERS) {
if (hasProviderEnvVars(provider)) {
logger.info("[PPT Utils] Auto-selected PPT provider", {
originalProvider: currentProviderName,
selectedProvider: provider,
});
const { AIProviderFactory } = await import("../../core/factory.js");
const { withTimeout } = await import("../../utils/errorHandling.js");
const { PPT_GENERATION_TIMEOUT_MS } = await import("./constants.js");
const createdProvider = await withTimeout(AIProviderFactory.createProvider(provider, undefined, true, neurolink), PPT_GENERATION_TIMEOUT_MS / 4, ErrorFactory.toolTimeout("createProvider", PPT_GENERATION_TIMEOUT_MS / 4));
return {
provider: createdProvider,
providerName: provider,
modelName: createdProvider
.modelName,
wasAutoSelected: true,
};
}
}
throw ErrorFactory.invalidParameters("ppt-generation", new Error(`No PPT-compatible provider available. Configure one of: ${PPT_VALID_PROVIDERS.join(", ")}`), {
currentProvider: currentProviderName,
validProviders: PPT_VALID_PROVIDERS,
});
}
// ============================================================================
// FILE & PATH UTILITIES
// ============================================================================
/**
* Generate output file path for PPT
*/
export function generateOutputPath(context) {
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, "-")
.substring(0, 19);
const sanitizedTopic = context.topic
.substring(0, 30)
.replace(/[^a-zA-Z0-9]/g, "_")
.toLowerCase();
const defaultFileName = `${sanitizedTopic}_${timestamp}.pptx`;
if (context.outputPath) {
if (context.outputPath.endsWith(".pptx")) {
return context.outputPath;
}
return path.join(context.outputPath, defaultFileName);
}
return path.join(process.cwd(), "output", defaultFileName);
}
/**
* Ensure output directory exists
*/
export async function ensureOutputDirectory(filePath) {
const dir = path.dirname(filePath);
try {
const { withTimeout, ErrorFactory } = await import("../../utils/errorHandling.js");
const { PPT_GENERATION_TIMEOUT_MS } = await import("./constants.js");
await withTimeout(fs.mkdir(dir, { recursive: true }), PPT_GENERATION_TIMEOUT_MS / 8, ErrorFactory.toolTimeout("mkdirOutputDirectory", PPT_GENERATION_TIMEOUT_MS / 8));
}
catch (error) {
const originalError = error instanceof Error ? error : new Error(String(error));
throw new PPTError(`Failed to create output directory: ${dir}`, PPT_ERROR_CODES.FILE_WRITE_FAILED, { directory: dir }, originalError);
}
}
// ============================================================================
// TYPE UTILITIES
// ============================================================================
/**
* Check if value is a non-null object
*/
export function isObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
/**
* Type guard for LogoConfig
*/
export function isLogoConfig(logo) {
if (!isObject(logo)) {
return false;
}
return ("data" in logo &&
(Buffer.isBuffer(logo.data) || typeof logo.data === "string"));
}
/**
* Convert LogoConfig or Buffer/string to normalized format
*/
export function normalizeLogoConfig(logo) {
if (logo === null) {
return null;
}
if (Buffer.isBuffer(logo) || typeof logo === "string") {
return {
data: logo,
position: "bottom-right",
width: 1,
height: 0.4,
showOn: "all-slides",
};
}
if (isLogoConfig(logo)) {
return logo;
}
return null;
}
/**
* Get pptxgenjs layout name from aspect ratio
*/
export function getLayoutName(aspectRatio) {
switch (aspectRatio) {
case "16:9":
return "LAYOUT_16x9";
case "4:3":
return "LAYOUT_4x3";
default:
return "LAYOUT_16x9";
}
}
// ============================================================================
// ERROR UTILITIES
// ============================================================================
/**
* Convert unknown error to Error instance
*/
export function toError(error) {
if (error instanceof Error) {
return error;
}
return new Error(String(error));
}
/**
* Determine which stage failed based on orchestration state
*/
export function getFailureStage(state) {
if (state.contentPlan === null) {
return "content-planning";
}
if (state.slides === null) {
return "slide-generation";
}
if (state.outputPath === null) {
return "pptx-assembly";
}
return "file-output";
}
// ============================================================================
// IMAGE VALIDATION UTILITIES
// ============================================================================
/**
* Validate an image buffer and determine its MIME type
*/
export function validateImageBuffer(buffer) {
if (!buffer || buffer.length === 0) {
return {
isValid: false,
mimeType: "",
format: "",
error: "Empty or undefined buffer",
};
}
if (buffer.length < 100) {
return {
isValid: false,
mimeType: "",
format: "",
error: `Buffer too small (${buffer.length} bytes)`,
};
}
// Check magic bytes for different image formats
// JPEG: FF D8 FF
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
return { isValid: true, mimeType: "image/jpeg", format: "JPEG" };
}
// PNG: 89 50 4E 47
if (buffer[0] === 0x89 &&
buffer[1] === 0x50 &&
buffer[2] === 0x4e &&
buffer[3] === 0x47) {
return { isValid: true, mimeType: "image/png", format: "PNG" };
}
// GIF: 47 49 46 (GIF)
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) {
return { isValid: true, mimeType: "image/gif", format: "GIF" };
}
// WebP: RIFF....WEBP
if (buffer[0] === 0x52 &&
buffer[1] === 0x49 &&
buffer[2] === 0x46 &&
buffer[3] === 0x46) {
// Check for WEBP signature at offset 8
if (buffer.length > 12 && buffer.slice(8, 12).toString() === "WEBP") {
return { isValid: true, mimeType: "image/webp", format: "WebP" };
}
}
// BMP: 42 4D (BM)
if (buffer[0] === 0x42 && buffer[1] === 0x4d) {
return { isValid: true, mimeType: "image/bmp", format: "BMP" };
}
// Unknown format - try as PNG (common fallback)
return {
isValid: false,
mimeType: "image/png",
format: "unknown",
error: `Unknown format (magic bytes: ${buffer.slice(0, 4).toString("hex")})`,
};
}
/**
* Convert image buffer to data URL for pptxgenjs
*/
export function bufferToDataUrl(buffer) {
const validation = validateImageBuffer(buffer);
if (!validation.isValid && validation.format === "") {
logger.warn("[bufferToDataUrl] Invalid image buffer", {
error: validation.error,
});
return null;
}
// Use the detected MIME type or fallback to PNG
const mimeType = validation.mimeType || "image/png";
return `data:${mimeType};base64,${buffer.toString("base64")}`;
}
// ============================================================================
// TEXT FORMATTING UTILITIES (Markdown Parsing)
// ============================================================================
/**
* Parse markdown-style formatting in text and return formatted segments
* Supports: **bold**, *italic*, ***bold italic***
*
* @example
* parseMarkdownText("Hello **world**")
* // Returns: [{ text: "Hello " }, { text: "world", bold: true }]
*/
export function parseMarkdownText(text) {
if (!text || typeof text !== "string") {
return [{ text: text || "" }];
}
const segments = [];
// Regex to match **bold**, *italic*, or ***bold italic***
// Pattern: captures text between markers
const pattern = /(\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|\*(.+?)\*)/g;
let lastIndex = 0;
while (true) {
const match = pattern.exec(text);
if (match === null) {
break;
}
// Add text before this match
if (match.index > lastIndex) {
segments.push({ text: text.slice(lastIndex, match.index) });
}
// Determine which group matched
if (match[2]) {
// ***bold italic***
segments.push({ text: match[2], bold: true, italic: true });
}
else if (match[3]) {
// **bold**
segments.push({ text: match[3], bold: true });
}
else if (match[4]) {
// *italic*
segments.push({ text: match[4], italic: true });
}
lastIndex = pattern.lastIndex;
}
// Add remaining text after last match
if (lastIndex < text.length) {
segments.push({ text: text.slice(lastIndex) });
}
// If no matches found, return original text as single segment
if (segments.length === 0) {
segments.push({ text });
}
return segments;
}
/**
* Check if text contains markdown formatting
*/
export function hasMarkdownFormatting(text) {
if (!text || typeof text !== "string") {
return false;
}
return /\*{1,3}[^*]+\*{1,3}/.test(text);
}
/**
* Convert parsed text segments to pptxgenjs text runs array
* This allows mixed formatting within a single bullet point
*/
export function createFormattedTextProps(segments, baseOptions) {
return segments.map((segment) => ({
text: segment.text,
options: {
fontSize: baseOptions.fontSize,
fontFace: baseOptions.fontFace,
color: baseOptions.color,
bold: segment.bold || baseOptions.baseBold || false,
italic: segment.italic || false,
},
}));
}
/**
* Calculate font size based on bullet count
* More bullets = smaller font to fit content
*
* Formula:
* - 1-5 bullets: baseFontSize (18pt)
* - 6-7 bullets: baseFontSize - 2 (16pt)
* - 8-10 bullets: baseFontSize - 4 (14pt)
* - 10+ bullets: cap at 12pt minimum
*/
export function calculateFontSize(bulletCount, baseFontSize = 18) {
const MIN_FONT_SIZE = 12;
if (bulletCount <= 5) {
return baseFontSize;
}
else if (bulletCount <= 7) {
return Math.max(MIN_FONT_SIZE, baseFontSize - 2);
}
else if (bulletCount <= 10) {
return Math.max(MIN_FONT_SIZE, baseFontSize - 4);
}
else {
return MIN_FONT_SIZE;
}
}
//# sourceMappingURL=utils.js.map