carouselforge / src /lib /renderer /render-carousel.ts
CarouselForge Developer
feat: complete backend engine logic (llm parsing, puppeteer renderer, sqlite analytics)
d9ba1d6
import puppeteer from 'puppeteer-core';
import type { ApiResponse } from '@/types/api';
import type { Slide } from '@/types/carousel';
export interface RenderRequest {
slides: Slide[];
template: string;
palette: string;
variant?: string;
}
export interface RenderResult {
images: string[];
format: string;
}
// OS-specific chromium paths
const CHROMIUM_PATHS: Record<string, string[]> = {
win32: [
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe'
],
linux: [
'/usr/bin/chromium',
'/usr/bin/chromium-browser',
'/usr/bin/google-chrome'
],
darwin: [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
]
};
function getExecutablePath(): string | undefined {
const platform = process.platform as string;
const paths = CHROMIUM_PATHS[platform] || CHROMIUM_PATHS.linux;
const fs = require('fs');
for (const p of paths) {
if (fs.existsSync(p)) return p;
}
return undefined;
}
const PALETTE_COLORS: Record<string, { bg: string, text: string }> = {
blue: { bg: '#2563eb', text: '#ffffff' },
green: { bg: '#16a34a', text: '#ffffff' },
orange: { bg: '#ea580c', text: '#ffffff' },
purple: { bg: '#9333ea', text: '#ffffff' },
mono: { bg: '#111827', text: '#ffffff' },
};
function buildSlideHTML(slide: Slide, palette: string, index: number, total: number): string {
const colors = PALETTE_COLORS[palette] || PALETTE_COLORS.blue;
return `
<html>
<head>
<style>
body {
margin: 0; padding: 0;
width: 1080px; height: 1080px;
background-color: ${colors.bg};
color: ${colors.text};
font-family: system-ui, -apple-system, sans-serif;
display: flex; flex-direction: column; justify-content: center; align-items: center;
text-align: center; padding: 80px; box-sizing: border-box;
}
.title { font-size: 80px; font-weight: 800; margin-bottom: 40px; }
.body { font-size: 40px; font-weight: 400; line-height: 1.4; opacity: 0.9; }
.footer { position: absolute; bottom: 60px; right: 60px; font-size: 24px; font-weight: bold; opacity: 0.7; }
.cta { position: absolute; bottom: 80px; font-size: 40px; font-weight: bold; padding: 20px 40px; border-radius: 100px; background: ${colors.text}; color: ${colors.bg}; }
</style>
</head>
<body>
<div class="title">${slide.headline}</div>
<div class="body">${slide.body}</div>
${slide.cta ? `<div class="cta">${slide.cta}</div>` : ''}
<div class="footer">${index + 1} / ${total}</div>
</body>
</html>
`;
}
export async function renderCarousel(req: RenderRequest): Promise<ApiResponse<RenderResult>> {
let browser;
try {
const executablePath = getExecutablePath();
if (!executablePath) {
throw new Error('Chromium executable not found. Install Chrome/Chromium.');
}
browser = await puppeteer.launch({
executablePath,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage']
});
const page = await browser.newPage();
await page.setViewport({ width: 1080, height: 1080 });
const images: string[] = [];
for (let i = 0; i < req.slides.length; i++) {
const slide = req.slides[i];
const html = buildSlideHTML(slide, req.palette, i, req.slides.length);
await page.setContent(html, { waitUntil: 'load' });
const buffer = await page.screenshot({ type: 'png' });
images.push('data:image/png;base64,' + buffer.toString('base64'));
}
await browser.close();
return {
success: true,
data: { images, format: 'png' },
};
} catch (error) {
if (browser) await browser.close();
console.error('[render] puppeteer failed:', error);
const msg = error instanceof Error ? error.message : String(error);
return {
success: false,
error: \`Failed to render images: \${msg}\`,
};
}
}