ai-chatbot/tests/routes/canvas-scale-adjust-intent.test.ts

303 lines
13 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { hasScaleAdjustTargetChanged } from "../../components/canvas/nodes/image-node/image-node-local-edit";
import { hasScaleAdjustPositionMoved } from "../../lib/canvas/scale-adjust-geometry";
import {
buildSemanticScaleAdjustCleanupPrompt,
buildSemanticScaleAdjustCompositeRegions,
buildSemanticScaleAdjustExtractionPrompt,
isScaleAdjustSubjectMaskLowConfidence,
} from "../../lib/canvas/scale-adjust-semantic";
function readSource(path: string) {
return readFileSync(resolve(process.cwd(), path), "utf8");
}
test.describe("canvas scale adjust product lock", () => {
test("accepts target movement even when target size stays unchanged", () => {
const source = { height: 0.4, width: 0.3, x: 0.2, y: 0.25 };
expect(hasScaleAdjustTargetChanged(source, source)).toBe(false);
expect(
hasScaleAdjustTargetChanged(source, { ...source, x: 0.46, y: 0.12 })
).toBe(true);
expect(
hasScaleAdjustTargetChanged(source, {
...source,
height: 0.32,
width: 0.24,
})
).toBe(true);
});
test("does not treat center-preserving scale as a position move", () => {
const source = { height: 0.4, width: 0.3, x: 0.2, y: 0.25 };
const centeredShrink = {
height: 0.3,
width: 0.225,
x: 0.2375,
y: 0.3,
};
expect(hasScaleAdjustPositionMoved(source, centeredShrink)).toBe(false);
expect(
hasScaleAdjustPositionMoved(source, { ...centeredShrink, x: 0.45 })
).toBe(true);
});
test("routes spatially incomplete product masks to semantic image editing", () => {
const partialMask = new Uint8Array(10_000);
for (let y = 20; y < 60; y += 1) {
for (let x = 30; x < 60; x += 1) partialMask[y * 100 + x] = 255;
}
const completeMask = new Uint8Array(10_000);
for (let y = 10; y < 90; y += 1) {
for (let x = 15; x < 85; x += 1) completeMask[y * 100 + x] = 255;
}
const croppedMask = new Uint8Array(10_000);
for (let y = 20; y < 100; y += 1) {
for (let x = 20; x < 80; x += 1) croppedMask[y * 100 + x] = 255;
}
expect(
isScaleAdjustSubjectMaskLowConfidence({
alpha: partialMask,
height: 100,
width: 100,
})
).toBe(true);
expect(
isScaleAdjustSubjectMaskLowConfidence({
alpha: completeMask,
height: 100,
width: 100,
})
).toBe(false);
expect(
isScaleAdjustSubjectMaskLowConfidence({
alpha: croppedMask,
height: 100,
width: 100,
})
).toBe(true);
});
test("splits complete-product scaling into cleanup and chroma extraction", () => {
const cleanupPrompt = buildSemanticScaleAdjustCleanupPrompt();
const extractionPrompt = buildSemanticScaleAdjustExtractionPrompt();
expect(cleanupPrompt).toContain("只移除局部特写所展示的整套目标产品组");
expect(cleanupPrompt).toContain("立柱、底座、挂钩、衣物、包");
expect(extractionPrompt).toContain("complete target product group");
expect(extractionPrompt).toContain("every attached or hanging garment");
expect(extractionPrompt).toContain("pure magenta RGB(255,0,255)");
expect(extractionPrompt).toContain("no missing parts");
});
test("composites semantic edits with enough context around the complete product", () => {
const [compositeRegion] = buildSemanticScaleAdjustCompositeRegions([
{
index: 1,
rect: { height: 0.515, width: 0.215, x: 0.295, y: 0.225 },
targetRect: {
height: 0.38625,
width: 0.16125,
x: 0.321875,
y: 0.289375,
},
},
]);
expect(compositeRegion.x).toBeLessThan(0.24);
expect(compositeRegion.y).toBeLessThan(0.11);
expect(compositeRegion.x + compositeRegion.width).toBeGreaterThan(0.57);
expect(compositeRegion.y + compositeRegion.height).toBeGreaterThan(0.85);
});
test("locks semantic extraction to exact target geometry after cleanup", () => {
const generator = readSource("lib/canvas/generate-image.ts");
const semanticProduct = readSource(
"lib/canvas/scale-adjust-semantic-product.ts"
);
expect(generator).toContain("buildSemanticScaleAdjustProductComposite");
expect(generator).toContain("candidateImageUrls");
expect(generator).toContain("imageCount: 2");
expect(semanticProduct).toContain('fit: "fill"');
expect(semanticProduct).toContain("MAGENTA_LOW_DISTANCE");
expect(semanticProduct).toContain("MAGENTA_DESPILL_MIN_ALPHA");
expect(semanticProduct).toContain("fallbackShadowBuffer");
});
test("uses the same movement validation in canvas and detail result flows", () => {
const canvasNode = readSource("components/canvas/nodes/ImageNode.tsx");
const detailWorkspace = readSource(
"components/agents/AgentGenerationWorkspaceDarkV2.tsx"
);
expect(canvasNode).toContain("hasScaleAdjustTargetChanged(");
expect(detailWorkspace).toContain("hasScaleAdjustTargetChanged(");
expect(canvasNode).toContain("青色目标框大小或位置");
expect(detailWorkspace).toContain("目标框大小和位置均未变化");
});
test("uses one dedicated erase step and exact product overlay", () => {
const route = readSource("app/api/canvas/local-edit/route.ts");
expect(route).toContain("buildScaleAdjustProductLockAssets");
expect(route).toContain('objectRemovalMode: "premium"');
expect(route).toContain("objectRemovalMaskImageUrl: modelMaskUpload.url");
expect(route).toContain("maskImageUrl: modelMaskUpload.url");
expect(route).toContain("hasPositionMove");
expect(route).toContain("hasScaleAdjustPositionMoved(");
expect(route).toContain("useSemanticScaleAdjust");
expect(route).toContain("semanticSourceCropUploads");
expect(route).toContain("buildSemanticScaleAdjustCleanupPrompt");
expect(route).toContain("buildSemanticScaleAdjustExtractionPrompt");
expect(route).toContain("buildSemanticScaleAdjustCompositeRegions");
expect(route).toContain('maskMode: "region" as const');
expect(route).toContain("expandRegions: false");
expect(route).toContain("featherRatio: 0.012");
expect(route).toContain('aspectRatio: hasPositionMove ? "auto" : resolvedAspectRatio');
expect(route).toContain("overlayImageUrl: overlayUpload.url");
expect(route).toContain("repairMaskImageUrl: repairMaskUpload.url");
expect(route).toContain("sourceImageUrl");
expect(route).toContain("protectPeople: hasPositionMove");
expect(route).toContain("oldFootprintMaskImageUrl: useSemanticScaleAdjust");
expect(route).toContain(": hasPositionMove");
expect(route).toContain("? sourceSubjectMaskUpload?.url");
expect(route).toContain(
"deterministicProductOverlayApplied: true"
);
expect(route).toContain("scaleAdjustPersonExclusionApplied:");
expect(route).toContain("sourceSubjectMaskImageUrl: sourceSubjectMaskUpload.url");
expect(route).toContain("shadowSourceImageUrl: sourceImageUrl");
expect(route).toContain('"finegrain-erase-product-lock-v1"');
expect(route).toContain('"gpt-mask-background-rebuild-product-lock-v1"');
expect(route).toContain('"gpt-extract-product-lock-v2"');
expect(route).not.toContain("buildScaleAdjustVisualGuide");
expect(route).not.toContain("source-plus-guide-v1");
});
test("routes object removal through the configured fal credential", () => {
const provider = readSource("lib/ai/providers/fal-image.ts");
expect(provider).toContain(
'const FAL_OBJECT_REMOVAL_MODEL_ID = "fal-ai/finegrain-eraser/mask"'
);
expect(provider).toContain("image_url: imageUrls[0]");
expect(provider).toContain("mask_url: objectRemovalMaskImageUrl");
expect(provider).toContain('objectRemovalMode || ""');
});
test("keeps legacy cleanup retry while semantic extraction uses fresh candidates", () => {
const route = readSource("app/api/canvas/local-edit/route.ts");
const generator = readSource("lib/canvas/generate-image.ts");
expect(route).toContain("rejectOldFootprintOnly: true");
expect(route).toContain(
"scaleAdjustSequentialCleanupRetry: !useSemanticScaleAdjust"
);
expect(route).toContain("retryVariants: useSemanticScaleAdjust ? []");
expect(route).toContain('label: "finegrain-cleanup-pass"');
expect(route).toContain("semanticScaleAdjustPreparation");
expect(route).toContain("cleanupModel:");
expect(route).not.toContain("cleanupMaskImageUrl:");
expect(route).toContain("extractionPrompt:");
expect(route).toContain("sourceCropImageUrl:");
expect(route).toContain("relightModel:");
expect(route).toContain('quality: "high"');
expect(generator).toContain(
'scaleAdjustRejection?.reason === "old-footprint-remnant-detected"'
);
expect(generator).toContain(
"SCALE_ADJUST_OLD_FOOTPRINT_TARGET_MARGIN = 0.006"
);
expect(generator).toContain(
"SCALE_ADJUST_OLD_FOOTPRINT_EXPLICIT_SIMILAR_DIFF_THRESHOLD = 20"
);
expect(generator).toContain("scaleAdjustSemanticCleanupAttempt: attempt");
expect(generator).toContain("return runSemanticCleanup(2)");
expect(generator).toContain("referenceImages: [sequentialCleanupReferenceUrl]");
});
test("keeps transparent product images on a deterministic empty plate", () => {
const productLock = readSource("lib/canvas/scale-adjust-product-lock.ts");
expect(productLock).toContain("hasTransparentBackground");
expect(productLock).toContain("sharp(transparentCanvas)");
expect(productLock).toContain("backgroundPlateBuffer");
});
test("protects people and rejects products materially occluded by them", () => {
const productLock = readSource("lib/canvas/scale-adjust-product-lock.ts");
const rmbg = readSource("lib/ai/rmbg/service.ts");
expect(productLock).toContain('foregroundType: "person"');
expect(productLock).toContain("attempt <= 3");
expect(productLock).toContain("executeScaleAdjustRmbgJob({");
expect(productLock).toContain("regionPersonExclusionAlpha");
expect(productLock).toContain("overlapPixelCount >= overlapLimit");
expect(productLock).toContain("目标产品被人物遮挡");
expect(productLock).toContain("personExclusionApplied: Boolean(personExclusionAlpha)");
expect(productLock).toContain("expandedAlphaRaw[index] >= 8 ? 255 : 0");
expect(productLock).toContain(".threshold(8)\n .extractChannel(0)");
expect(rmbg).toContain("foregroundType: foregroundType ??");
});
test("moves the original contact shadow with the exact product overlay", () => {
const generator = readSource("lib/canvas/generate-image.ts");
expect(generator).toContain("buildScaleAdjustTransferredShadow");
expect(generator).toContain("cleanedLuma - sourceLuma - 4");
expect(generator).toContain("transferredShadowBuffer");
expect(generator).toContain("transferredShadowCount");
expect(generator).toContain("if (!(isShrink || isMove)) continue");
expect(generator).toContain(
"const maskHit = explicitMaskRaw\n ? explicitMaskValue > 18"
);
expect(generator).toContain("(!usesExplicitMask &&");
expect(generator).toContain(
"const generatedWidth = compositeBaseBuffer ? overlayWidth : modelWidth"
);
expect(generator).toContain(
".extractChannel(3)\n .blur(Math.max(4"
);
expect(generator).toContain(".extractChannel(0)\n .raw()\n .toBuffer()");
});
test("uses the image edit model to rebuild lighting at the target position", () => {
const route = readSource("app/api/canvas/local-edit/route.ts");
const queue = readSource("lib/canvas/image-task-queue.ts");
const relight = readSource("lib/canvas/scale-adjust-model-relight.ts");
expect(route).toContain("scaleAdjustModelRelight:");
expect(route).toContain("...(hasPositionMove");
expect(route).toContain("overlayImageUrl: overlayUpload.url");
expect(queue).toContain("applyScaleAdjustModelRelight");
expect(relight).toContain('debugLabel: "canvas-scale-adjust-model-relight"');
expect(relight).toContain("maskImageUrl: maskUpload.url");
expect(relight).toContain("生成真实自然的接触阴影与投射阴影");
expect(relight).toContain("findMedianLumaDelta");
expect(relight).toContain("productAlpha[pixelIndex] >= 8 ? 0");
});
test("keeps non-pro detail result scale adjust on the shared backend flow", () => {
const source = readSource(
"components/agents/AgentGenerationWorkspaceDarkV2.tsx"
);
expect(source).toContain(
"const resolvedLocalEditModelId = isScaleAdjust\n ? undefined"
);
expect(source).toContain(
'operation: isScaleAdjust ? "scale-adjust" : undefined'
);
expect(source).toContain("targetRect: region.targetRect");
expect(source).toContain('resultModeType: "detail_generation"');
expect(source).toContain('mode: "scale_adjust"');
});
});