48 lines
2.5 KiB
TypeScript
48 lines
2.5 KiB
TypeScript
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<string, { expires: number; url: Promise<string> }>();
|
|
|
|
async function prepareImage(url: string, role: "template" | "product"): Promise<PreparedImage> {
|
|
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),
|
|
} };
|
|
}
|