ai-chatbot/tests/routes/replacement-layout-lock.test.ts

155 lines
3.9 KiB
TypeScript

import { expect, test } from "@playwright/test";
import sharp from "sharp";
import {
buildReplacementInputBoundingBoxList,
compositeReplacementWithReferenceLocks,
expandReplacementSubjectBoundingBoxForGeneration,
parseReplacementLayoutLockRegions,
} from "@/lib/ai/replacement-layout-lock";
test("parses normalized subject and protected text regions", () => {
const result = parseReplacementLayoutLockRegions(`
\`\`\`json
{
"subjectBoundingBox": [200, 250, 800, 900],
"textBoundingBoxes": [[100, 80, 700, 160], [20, 20, 20, 40], [-20, 900, 1100, 980]]
}
\`\`\`
`);
expect(result).toEqual({
subjectBoundingBox: [200, 250, 800, 900],
textBoundingBoxes: [
[100, 80, 700, 160],
[0, 900, 1000, 980],
],
analyzed: true,
});
});
test("marks incomplete layout analysis for a safe fallback", () => {
expect(
parseReplacementLayoutLockRegions({
subjectBoundingBox: [200, 250, 800, 900],
}),
).toEqual({
subjectBoundingBox: [200, 250, 800, 900],
textBoundingBoxes: [],
analyzed: false,
});
});
test("expands the replacement subject horizontally and aligns it with the layout input", () => {
expect(
expandReplacementSubjectBoundingBoxForGeneration([379, 240, 628, 917]),
).toEqual([354, 230, 653, 927]);
expect(
buildReplacementInputBoundingBoxList({
imageUrls: [
"https://example.com/product.png",
"https://example.com/layout.png",
],
referenceImageUrl: "https://example.com/layout.png",
subjectBoundingBox: [379, 240, 628, 917],
}),
).toEqual([[], [[354, 230, 653, 927]]]);
});
test("keeps the reference outside the subject and inside protected text", async () => {
const width = 100;
const height = 100;
const reference = await sharp({
create: {
width,
height,
channels: 3,
background: { r: 255, g: 0, b: 0 },
},
})
.png()
.toBuffer();
const generated = await sharp({
create: {
width,
height,
channels: 3,
background: { r: 0, g: 0, b: 255 },
},
})
.png()
.toBuffer();
const result = await compositeReplacementWithReferenceLocks({
generatedBuffer: generated,
referenceBuffer: reference,
width,
height,
preserveReferenceOutsideSubject: true,
subjectBoundingBox: [300, 300, 700, 700],
textBoundingBoxes: [[450, 450, 550, 550]],
});
const { data, info } = await sharp(result)
.removeAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
const pixel = (x: number, y: number) =>
Array.from(
data.subarray(
(y * width + x) * info.channels,
(y * width + x) * info.channels + 3,
),
);
expect(pixel(5, 5)).toEqual([255, 0, 0]);
expect(pixel(35, 50)).toEqual([0, 0, 255]);
expect(pixel(50, 50)).toEqual([255, 0, 0]);
});
test("restores original text pixels while retaining a regenerated scene", async () => {
const width = 100;
const height = 100;
const reference = await sharp({
create: {
width,
height,
channels: 3,
background: { r: 255, g: 0, b: 0 },
},
})
.png()
.toBuffer();
const generated = await sharp({
create: {
width,
height,
channels: 3,
background: { r: 0, g: 0, b: 255 },
},
})
.png()
.toBuffer();
const result = await compositeReplacementWithReferenceLocks({
generatedBuffer: generated,
referenceBuffer: reference,
width,
height,
preserveReferenceOutsideSubject: false,
textBoundingBoxes: [[400, 400, 600, 600]],
});
const { data, info } = await sharp(result)
.removeAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
const pixel = (x: number, y: number) =>
Array.from(
data.subarray(
(y * width + x) * info.channels,
(y * width + x) * info.channels + 3,
),
);
expect(pixel(5, 5)).toEqual([0, 0, 255]);
expect(pixel(50, 50)).toEqual([255, 0, 0]);
});