130 lines
5 KiB
TypeScript
130 lines
5 KiB
TypeScript
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,
|
||
};
|
||
}
|