import { chromium } from "playwright-core"; import { Writable } from "stream"; import archiver from "archiver"; import sharp from "sharp"; import { puterChatComplete } from "../puter-inference.js"; import { resolveBrowserPath } from "./diagram-renderer.js"; import { validateAndCleanSvg } from "./icon-renderer.js"; import type { RenderResult, BrandKitBundle } from "./types.js"; // ─── Brand specification ──────────────────────────────────────────────────────── interface BrandSpec { name: string; tagline: string; primaryColor: string; secondaryColor: string; fontStyle: string; industry: string; logoDescription: string; } // ─── Social platform dimensions ───────────────────────────────────────────────── const SOCIAL_DIMENSIONS: Record = { "twitter-profile": { width: 400, height: 400 }, "twitter-banner": { width: 1500, height: 500 }, "linkedin-profile": { width: 400, height: 400 }, "linkedin-banner": { width: 1584, height: 396 }, "instagram-profile": { width: 1080, height: 1080 }, }; // ─── Strip markdown fences ────────────────────────────────────────────────────── function stripMarkdownFences(raw: string): string { return raw .trim() .replace(/^```(?:json|svg|xml|html)?\s*/i, "") .replace(/\s*```$/, "") .trim(); } // ─── Step 1: Extract brand spec from prompt ───────────────────────────────────── async function extractBrandSpec(prompt: string): Promise { const systemPrompt = [ "You are a brand identity strategist.", "Extract or invent a brand specification from the user's prompt.", "Respond with ONLY a JSON object — no markdown fences, no explanation.", "Required fields:", ' "name": brand name (string)', ' "tagline": short catchy tagline (string)', ' "primaryColor": primary brand color as hex (e.g. "#FF5733")', ' "secondaryColor": secondary brand color as hex (e.g. "#33C1FF")', ' "fontStyle": one of "sans", "serif", or "mono"', ' "industry": industry sector (string)', ' "logoDescription": brief description of a simple geometric logo mark (string)', ].join("\n"); const raw = await puterChatComplete([ { role: "system", content: systemPrompt }, { role: "user", content: prompt }, ]); try { const cleaned = stripMarkdownFences(raw); const parsed = JSON.parse(cleaned) as Partial; return { name: parsed.name ?? "Brand", tagline: parsed.tagline ?? "Building the future", primaryColor: parsed.primaryColor ?? "#3B82F6", secondaryColor: parsed.secondaryColor ?? "#10B981", fontStyle: parsed.fontStyle ?? "sans", industry: parsed.industry ?? "technology", logoDescription: parsed.logoDescription ?? "A simple geometric mark", }; } catch { // Fallback defaults if parsing fails return { name: "Brand", tagline: "Building the future", primaryColor: "#3B82F6", secondaryColor: "#10B981", fontStyle: "sans", industry: "technology", logoDescription: "A simple geometric mark", }; } } // ─── Step 2: Generate logo SVG via LLM ───────────────────────────────────────── async function generateLogoSvg(spec: BrandSpec): Promise { const systemPrompt = [ "You are an SVG logo designer.", "Output ONLY valid SVG. No text explanations. No markdown fences.", "Simple geometric shapes only. viewBox 0 0 512 512.", `Use only these colors: primary=${spec.primaryColor}, secondary=${spec.secondaryColor}`, "The SVG must start with .", "No text elements, no external resources, no scripts.", ].join("\n"); const raw = await puterChatComplete([ { role: "system", content: systemPrompt }, { role: "user", content: `Create a logo mark for: ${spec.logoDescription}` }, ]); const cleaned = stripMarkdownFences(raw); const { svg } = validateAndCleanSvg(cleaned); return svg; } // ─── Step 3: Rasterize logo to avatar sizes ───────────────────────────────────── async function rasterizeAvatars(logoSvg: string): Promise> { const sizes = [512, 256, 128, 64, 32]; const avatarPngs: Record = {}; for (const size of sizes) { const pngBuffer = await sharp(Buffer.from(logoSvg)) .resize(size, size) .png() .toBuffer(); avatarPngs[String(size)] = pngBuffer.toString("base64"); } return avatarPngs; } // ─── Step 4: Generate social platform images ──────────────────────────────────── async function generateSocialImages( spec: BrandSpec, logoSvg: string, ): Promise> { const socialImages: Record = {}; for (const [platform, dims] of Object.entries(SOCIAL_DIMENSIONS)) { const { width, height } = dims; // Build SVG template: colored background + centered logo const logoScale = Math.min(width, height) * 0.5; const logoX = (width - logoScale) / 2; const logoY = (height - logoScale) / 2; const socialSvg = [ ``, ` `, ` `, ``, ].join("\n"); const pngBuffer = await sharp(Buffer.from(socialSvg), { density: 72 }) .resize(width, height, { fit: "fill" }) .png({ compressionLevel: 9 }) .toBuffer(); socialImages[platform] = pngBuffer.toString("base64"); } return socialImages; } // ─── Step 5: Generate email signature and letterhead HTML ─────────────────────── async function generateTemplates( spec: BrandSpec, ): Promise<{ signature: string; letterhead: string }> { const signatureSystemPrompt = [ "You are a professional HTML email signature designer.", "Generate a professional HTML email signature with name, tagline, colors.", "Inline CSS only. No external resources. Keep under 200 lines.", "Output ONLY the HTML — no markdown fences, no explanation.", `Brand: name="${spec.name}", tagline="${spec.tagline}", primary=${spec.primaryColor}, secondary=${spec.secondaryColor}`, ].join("\n"); const letterheadSystemPrompt = [ "You are a professional HTML document designer.", "Generate a professional HTML letterhead template with header, footer, placeholder body.", "Inline CSS only. No external resources.", "Output ONLY the HTML — no markdown fences, no explanation.", `Brand: name="${spec.name}", tagline="${spec.tagline}", primary=${spec.primaryColor}, secondary=${spec.secondaryColor}`, ].join("\n"); const [rawSignature, rawLetterhead] = await Promise.all([ puterChatComplete([ { role: "system", content: signatureSystemPrompt }, { role: "user", content: `Create an email signature for ${spec.name}` }, ]), puterChatComplete([ { role: "system", content: letterheadSystemPrompt }, { role: "user", content: `Create a letterhead template for ${spec.name}` }, ]), ]); return { signature: stripMarkdownFences(rawSignature), letterhead: stripMarkdownFences(rawLetterhead), }; } // ─── Step 6: Generate brand guidelines PDF via Playwright ─────────────────────── async function generateGuidelinesPdf( spec: BrandSpec, browser: Awaited>, ): Promise { const guidelinesSystemPrompt = [ "You are a professional brand designer.", "Generate a brand guidelines document as HTML.", "Include: brand name, tagline, color palette with hex codes, typography guidance, logo usage rules, spacing guidelines.", "Inline CSS only. No external resources. Use web-safe fonts.", "Output ONLY a complete HTML document starting with — no markdown fences, no explanation.", `Brand: name="${spec.name}", tagline="${spec.tagline}", primary=${spec.primaryColor}, secondary=${spec.secondaryColor}, fontStyle=${spec.fontStyle}, industry=${spec.industry}`, ].join("\n"); const rawHtml = await puterChatComplete([ { role: "system", content: guidelinesSystemPrompt }, { role: "user", content: `Create brand guidelines for ${spec.name}` }, ]); const cleanHtml = stripMarkdownFences(rawHtml); const page = await browser.newPage(); const pdfUint8 = await (async () => { await page.setContent(cleanHtml, { waitUntil: "domcontentloaded" }); return page.pdf({ format: "A4", printBackground: true, margin: { top: "20mm", bottom: "20mm", left: "20mm", right: "20mm" }, }); })(); return Buffer.from(pdfUint8); } // ─── Step 7: Package all assets into a ZIP buffer ────────────────────────────── interface ZipAssets { logoSvg: string; avatarPngs: Record; socialImages: Record; signature: string; letterhead: string; guidelinesPdf: Buffer; } async function buildZip(assets: ZipAssets): Promise { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; const sink = new Writable({ write(chunk: Buffer, _enc, cb) { chunks.push(chunk); cb(); }, }); const archive = archiver("zip", { zlib: { level: 6 } }); archive.on("error", reject); sink.on("finish", () => resolve(Buffer.concat(chunks))); archive.pipe(sink); // Logo SVG archive.append(Buffer.from(assets.logoSvg), { name: "brand-kit/logo/logo.svg" }); // Avatar PNGs for (const size of ["512", "256", "128", "64", "32"]) { const data = assets.avatarPngs[size]; if (data) { archive.append(Buffer.from(data, "base64"), { name: `brand-kit/logo/logo-${size}.png`, }); } } // Social images for (const [platform, data] of Object.entries(assets.socialImages)) { if (data) { archive.append(Buffer.from(data, "base64"), { name: `brand-kit/social/${platform}.png`, }); } } // Templates archive.append(Buffer.from(assets.signature), { name: "brand-kit/templates/email-signature.html", }); archive.append(Buffer.from(assets.letterhead), { name: "brand-kit/templates/letterhead.html", }); // Guidelines PDF archive.append(assets.guidelinesPdf, { name: "brand-kit/guidelines.pdf" }); void archive.finalize(); }); } // ─── Main exported renderer ───────────────────────────────────────────────────── /** * Render a full brand identity kit from a single conversation prompt. * * Orchestrates: spec extraction → logo SVG → avatar rasterization → * social images → email signature + letterhead → guidelines PDF → ZIP package. * * Returns a RenderResult containing a BrandKitBundle JSON bundle. */ export async function renderBrandKit( input: Record, ): Promise { const prompt = typeof input.prompt === "string" ? input.prompt : "Create a brand identity"; // Step 1: Extract brand spec from prompt const spec = await extractBrandSpec(prompt); // Step 2: Generate logo SVG via LLM const logoSvg = await generateLogoSvg(spec); // Step 3: Rasterize logo to 5 avatar sizes const avatarPngs = await rasterizeAvatars(logoSvg); // Step 4: Generate social platform images (colored background + centered logo) const socialImages = await generateSocialImages(spec, logoSvg); // Step 5: Generate email signature and letterhead HTML const { signature, letterhead } = await generateTemplates(spec); // Step 6: Open Playwright browser ONCE for the entire brand kit job const executablePath = resolveBrowserPath(); const browser = await chromium.launch({ executablePath, headless: true, args: ["--no-sandbox", "--disable-setuid-sandbox"], }); let guidelinesPdf: Buffer; try { guidelinesPdf = await generateGuidelinesPdf(spec, browser); } finally { await browser.close(); } // Step 7: Package everything into a ZIP const zipBuffer = await buildZip({ logoSvg, avatarPngs, socialImages, signature, letterhead, guidelinesPdf, }); // Assemble the BrandKitBundle const bundle: BrandKitBundle = { type: "brand-kit-bundle", spec: { name: spec.name, tagline: spec.tagline, primaryColor: spec.primaryColor, secondaryColor: spec.secondaryColor, fontStyle: spec.fontStyle, industry: spec.industry, }, logoSvgBase64: Buffer.from(logoSvg).toString("base64"), avatarPngs, socialImages, signatureHtml: signature, letterheadHtml: letterhead, guidelinesPdfBase64: guidelinesPdf.toString("base64"), zipBase64: zipBuffer.toString("base64"), }; return { filename: "brand-kit-bundle.json", contentType: "application/json", buffer: Buffer.from(JSON.stringify(bundle)), }; }