import "server-only"; import { createHash } from "node:crypto"; import sharp from "sharp"; import { fetchImageBufferFromUrl } from "@/lib/ai/rmbg/service"; import { uploadFile } from "@/lib/storage/service"; import { cacheImageBytes } from "@/lib/storage/image-byte-cache"; import { inferClosestReplacementAspectRatio } from "@/app/(chat)/api/chat/_lib/detail-replacement"; type PreparedImage = { buffer: Buffer; url: string; width: number; height: number }; const staged = new Map }>(); async function prepareImage(url: string, role: "template" | "product"): Promise { const original = await fetchImageBufferFromUrl(url); // Decode both actual files before submitting a paid generation; neither an // advisory HEAD failure nor an HTML error page may remove one of the roles. const { data: buffer, info } = await sharp(original, { failOn: "error" }) .rotate().png().toBuffer({ resolveWithObject: true }); if (info.width < 16 || info.height < 16) throw new Error("图片尺寸过小,请使用清晰的完整图片"); const digest = createHash("sha256").update(buffer).digest("hex"); const key = `${role}:${digest}`; let entry = staged.get(key); if (!entry || entry.expires < Date.now()) { if (staged.size >= 64) staged.delete(staged.keys().next().value!); const promise = (async () => { const uploaded = await uploadFile(`layout-${role}-${digest.slice(0, 20)}.png`, Uint8Array.from(buffer).buffer); await cacheImageBytes(`source::${uploaded.url}`, { buffer, contentType: "image/png" }); return uploaded.url; })(); entry = { expires: Date.now() + 10 * 60_000, url: promise }; staged.set(key, entry); promise.catch(() => { if (staged.get(key) === entry) staged.delete(key); }); } return { buffer, url: await entry.url, width: info.width, height: info.height }; } export async function prepareLayoutReplaceInputs(sourceUrl: string, referenceUrl: string) { if (sourceUrl === referenceUrl) throw new Error("新商品图和目标模版不能是同一张图片"); const [source, template] = await Promise.all([ prepareImage(sourceUrl, "product"), prepareImage(referenceUrl, "template"), ]); if (source.buffer.equals(template.buffer)) throw new Error("新商品图和目标模版不能是同一张图片"); return { source, template, referenceGeometry: { width: template.width, height: template.height, aspectRatio: inferClosestReplacementAspectRatio(template.width, template.height), } }; }