nexus/server/src/services/renderers/wallpaper-renderer.ts

130 lines
5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sharp from "sharp";
import { puterChatComplete } from "../puter-inference.js";
import type { RenderResult, WallpaperBundle, AppIconBundle } from "./types.js";
// ─── Platform dimensions ────────────────────────────────────────────────────────
export const PLATFORM_DIMENSIONS: Record<
string,
{ width: number; height: number; label: string }
> = {
"desktop-hd": { width: 2560, height: 1440, label: "Desktop HD (2560×1440)" },
"desktop-fhd": { width: 1920, height: 1080, label: "Desktop FHD (1920×1080)" },
"desktop-4k": { width: 3840, height: 2160, label: "Desktop 4K (3840×2160)" },
"mobile-portrait": { width: 1080, height: 1920, label: "Mobile Portrait (1080×1920)" },
"mobile-landscape": { width: 1920, height: 1080, label: "Mobile Landscape (1920×1080)" },
"og-image": { width: 1200, height: 630, label: "OG Image (1200×630)" },
"twitter-card": { width: 1200, height: 628, label: "Twitter Card (1200×628)" },
"instagram-post": { width: 1080, height: 1080, label: "Instagram Post (1080×1080)" },
"instagram-banner": { width: 1080, height: 566, label: "Instagram Banner (1080×566)" },
"linkedin-banner": { width: 1584, height: 396, label: "LinkedIn Banner (1584×396)" },
"app-icon": { width: 1024, height: 1024, label: "App Icon (1024×1024)" },
"favicon": { width: 32, height: 32, label: "Favicon (32×32)" },
};
export const APP_ICON_SIZES = [1024, 512, 256, 64, 32] as const;
// ─── SVG generation ─────────────────────────────────────────────────────────────
function buildWallpaperSystemPrompt(width: number, height: number): string {
return [
`You are an SVG artwork generator.`,
`Output ONLY valid SVG — no markdown fences, no explanation, no surrounding text.`,
`Use viewBox="0 0 ${width} ${height}" matching the target dimensions exactly.`,
`Create a visually rich scene based on the user prompt.`,
`Use gradients, shapes, patterns, and organic forms to fill the canvas.`,
`Do NOT include any text elements (<text>, <tspan>).`,
`Do NOT include external images or scripts.`,
`The SVG must start with <svg and end with </svg>.`,
].join("\n");
}
function stripMarkdownFences(raw: string): string {
return raw
.trim()
.replace(/^```(?:svg|xml)?\s*/i, "")
.replace(/\s*```$/, "")
.trim();
}
// ─── Rasterization helpers ──────────────────────────────────────────────────────
async function rasterizeSvgToPng(
svgString: string,
width: number,
height: number,
): Promise<Buffer> {
return sharp(Buffer.from(svgString), { density: 300 })
.resize(width, height, { fit: "fill" })
.png({ compressionLevel: 9 })
.toBuffer();
}
// ─── Main renderer ──────────────────────────────────────────────────────────────
export async function renderWallpaper(
input: Record<string, unknown>,
): Promise<RenderResult> {
const prompt =
typeof input.prompt === "string" ? input.prompt : "abstract digital art";
const platform =
typeof input.platform === "string" ? input.platform : "desktop-fhd";
const dims = PLATFORM_DIMENSIONS[platform];
if (!dims) {
throw new Error(
`renderWallpaper: unknown platform "${platform}". ` +
`Available: ${Object.keys(PLATFORM_DIMENSIONS).join(", ")}`,
);
}
const { width, height } = dims;
const systemPrompt = buildWallpaperSystemPrompt(width, height);
const rawSvg = await puterChatComplete([
{ role: "system", content: systemPrompt },
{ role: "user", content: prompt },
]);
const svgString = stripMarkdownFences(rawSvg);
// App icon / favicon → multi-size bundle
if (platform === "app-icon" || platform === "favicon") {
const sizes: AppIconBundle["sizes"] = [];
for (const size of APP_ICON_SIZES) {
const pngBuffer = await rasterizeSvgToPng(svgString, size, size);
sizes.push({ size, pngBase64: pngBuffer.toString("base64") });
}
const bundle: AppIconBundle = {
type: "app-icon-bundle",
sizes,
prompt,
};
return {
filename: "app-icon-bundle.json",
contentType: "application/json",
buffer: Buffer.from(JSON.stringify(bundle)),
};
}
// All other platforms → single PNG at exact target dimensions
const pngBuffer = await rasterizeSvgToPng(svgString, width, height);
const bundle: WallpaperBundle = {
type: "wallpaper-bundle",
platform,
width,
height,
pngBase64: pngBuffer.toString("base64"),
prompt,
};
return {
filename: `wallpaper-${platform}.png`,
contentType: "image/png",
buffer: pngBuffer,
};
}