fix: soften scale shrink layout wipe

This commit is contained in:
134 2026-07-04 11:47:52 +08:00
parent 360cba0873
commit d8e2d65186

View File

@ -107,6 +107,7 @@ const SCALE_ADJUST_CROP_MAX_EDGE = 1536;
const SCALE_ADJUST_PATCH_WORKSPACE_MIN_MARGIN = 0.045;
const SCALE_ADJUST_PATCH_WORKSPACE_MAX_MARGIN = 0.18;
const SCALE_ADJUST_LAYOUT_GUIDE_WASH_COLOR = "#f5f7fb";
const SCALE_ADJUST_LAYOUT_GUIDE_WASH_OPACITY = 0.46;
const SCALE_ADJUST_STRUCTURE_LOCK_MIN_MASK_RATIO = 0.006;
const SCALE_ADJUST_STRUCTURE_LOCK_COMPONENT_MIN_RATIO = 0.00018;
const SCALE_ADJUST_RMBG_MIN_ALPHA_RATIO = 0.012;
@ -1092,29 +1093,73 @@ function buildScaleAdjustPatchMaskSvg(params: { height: number; width: number })
);
}
function formatScaleAdjustWashColor(
background?: { b: number; g: number; r: number } | null
) {
if (!background) return SCALE_ADJUST_LAYOUT_GUIDE_WASH_COLOR;
const r = clamp(Math.round(background.r), 24, 242);
const g = clamp(Math.round(background.g), 24, 242);
const b = clamp(Math.round(background.b), 24, 242);
return `rgb(${r}, ${g}, ${b})`;
}
function buildScaleAdjustSourceFootprintWashSvg(params: {
boxes: Array<{ left: number; top: number; width: number; height: number }>;
boxes: Array<{
background?: { b: number; g: number; r: number } | null;
left: number;
top: number;
width: number;
height: number;
}>;
height: number;
width: number;
}) {
const { boxes, height, width } = params;
const blur = Math.max(4, Math.min(22, Math.round(Math.min(width, height) * 0.016)));
const defs = boxes
.map((box, index) => {
const blur = Math.max(
14,
Math.min(72, Math.round(Math.min(box.width, box.height) * 0.14))
);
return `<filter id="soft-wash-${index}" x="-45%" y="-45%" width="190%" height="190%">
<feGaussianBlur stdDeviation="${blur}" />
</filter>`;
})
.join("");
const rects = boxes
.map((box) => {
.map((box, index) => {
const blur = Math.max(
14,
Math.min(72, Math.round(Math.min(box.width, box.height) * 0.14))
);
const pad = Math.max(blur, Math.round(Math.min(box.width, box.height) * 0.055));
const left = Math.max(0, box.left - blur);
const top = Math.max(0, box.top - blur);
const right = Math.min(width, box.left + box.width + blur);
const bottom = Math.min(height, box.top + box.height + blur);
return `<rect x="${left}" y="${top}" width="${Math.max(1, right - left)}" height="${Math.max(1, bottom - top)}" fill="${SCALE_ADJUST_LAYOUT_GUIDE_WASH_COLOR}" fill-opacity="0.985" filter="url(#soft-wash)" />`;
const rectWidth = Math.max(1, right - left);
const rectHeight = Math.max(1, bottom - top);
const radius = Math.max(
8,
Math.min(Math.round(blur * 1.8), Math.round(rectWidth / 3), Math.round(rectHeight / 3))
);
const innerLeft = Math.max(0, box.left + pad);
const innerTop = Math.max(0, box.top + pad);
const innerRight = Math.min(width, box.left + box.width - pad);
const innerBottom = Math.min(height, box.top + box.height - pad);
const fill = formatScaleAdjustWashColor(box.background);
const inner =
innerRight > innerLeft + 4 && innerBottom > innerTop + 4
? `<rect x="${innerLeft}" y="${innerTop}" width="${innerRight - innerLeft}" height="${innerBottom - innerTop}" rx="${Math.max(4, Math.round(radius * 0.65))}" ry="${Math.max(4, Math.round(radius * 0.65))}" fill="${fill}" fill-opacity="0.18" filter="url(#soft-wash-${index})" />`
: "";
return `<rect x="${left}" y="${top}" width="${rectWidth}" height="${rectHeight}" rx="${radius}" ry="${radius}" fill="${fill}" fill-opacity="${SCALE_ADJUST_LAYOUT_GUIDE_WASH_OPACITY}" filter="url(#soft-wash-${index})" />${inner}`;
})
.join("");
return Buffer.from(
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
<defs>
<filter id="soft-wash" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="${blur}" />
</filter>
${defs}
</defs>
${rects}
</svg>`
@ -2471,14 +2516,16 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
structureLockedDraft = false,
userId,
} = params;
const sourceRaw = structureLockedDraft
? await sharp(sourceBuffer, { failOn: "none" })
.removeAlpha()
.toColorspace("srgb")
.raw()
.toBuffer()
: null;
const sourceRaw =
structureLockedDraft || regions.length > 0
? await sharp(sourceBuffer, { failOn: "none" })
.removeAlpha()
.toColorspace("srgb")
.raw()
.toBuffer()
: null;
const sourceFootprintBoxes: Array<{
background?: { b: number; g: number; r: number } | null;
left: number;
top: number;
width: number;
@ -2536,7 +2583,16 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
continue;
}
sourceFootprintBoxes.push(placement.sourceFootprintBox);
sourceFootprintBoxes.push({
...placement.sourceFootprintBox,
background: sourceRaw
? estimateScaleAdjustBoxBackground({
box: placement.sourceFootprintBox,
raw: sourceRaw,
width: sourceWidth,
})
: null,
});
const resizedCropBuffer = await sharp(sourceBuffer, { failOn: "none" })
.extract(placement.sourceBox)
@ -3450,8 +3506,8 @@ function buildScaleAdjustPatchPrompt(
? "Input image 1 is a rough subject-structure-locked target crop. The selected target subject was moved and scaled using source-photo pixels, so its final size, position, silhouette, material/skin/clothing, perspective, and visible structure are the hard target. If the subject is a person, keep the same person identity, face, body proportions, pose, hair, hands, and clothing while cleaning rough edges. If the subject is furniture/product, keep rods, panels, weave, wheels, material, and product structure. Repair rough background holes, edge halos, blur, occlusion, contact shadows, and surface artifacts without redesigning the subject. Input image 2 is only the clean background and non-target reference, never the old size reference."
: "Input image 1 is a rough subject-structure-locked target crop and the only visual reference. The selected target subject was moved and scaled using source-photo pixels, so its final size, position, silhouette, material/skin/clothing, perspective, and visible structure are the hard target. If the subject is a person, keep the same person identity, face, body proportions, pose, hair, hands, and clothing while cleaning rough edges. If the subject is furniture/product, keep rods, panels, weave, wheels, material, and product structure. Repair rough background holes, edge halos, blur, occlusion, contact shadows, and surface artifacts without redesigning the subject."
: includeSourceReference
? "Input image 1 is the rough edited target-layout crop. It is the hard layout target: the selected subject size and center in the final output must match this draft, but the rough wash, ghosted old footprint, placeholder shadow, blur, and patch artifacts must be cleaned."
: "Input image 1 is the rough edited target-layout crop and the only visual reference. It is the hard layout target: the selected subject size and center in the final output must match this draft. Clean away the rough wash, ghosted old footprint, placeholder shadow, blur, and patch artifacts."
? "Input image 1 is the rough edited target-layout crop. It is the hard layout target: the selected subject size and center in the final output must match this draft, but the rough wash, ghosted old footprint, pale wipe, placeholder shadow, blur, and patch artifacts are only edit instructions and must be cleaned. Never reproduce the pale wipe or any rectangular haze as photo content."
: "Input image 1 is the rough edited target-layout crop and the only visual reference. It is the hard layout target: the selected subject size and center in the final output must match this draft. Clean away the rough wash, ghosted old footprint, pale wipe, placeholder shadow, blur, and patch artifacts. Never reproduce the pale wipe or any rectangular haze as photo content."
: "Input image 1 is the exact crop from the original photo and is the locked crop base.";
const secondInputRole = targetDraftPrimary
? includeSourceReference
@ -3508,8 +3564,8 @@ function buildScaleAdjustPatchPrompt(
: "中文硬约束:输入图1已经用原图像素把目标主体放到目标尺寸和目标位置,目标主体可以是人物、模特、小孩、手臂身体、商品或家具。人物目标必须按输入图1保留同一身份、脸部、身体比例、姿态、发型、手部和服装;商品/家具目标必须保留产品结构、杆件、面板、编织纹理、轮子、材质和透视。不能重新设计或替换目标主体,只清理草稿边缘、黑斑、破洞、脏点、灰块、皮肤/服装/产品表面伪影。";
}
return includeSourceReference
? "中文硬约束:输入图1是目标大小/位置草稿,必须按它的目标尺寸和目标位置输出;输入图2只是原始质感、非目标人物和背景参考,绝对不要把目标主体恢复成输入图2里的旧尺寸。只输出同一裁切范围的干净局部照片,去掉草稿中的泛白、残影和拼接感。"
: "中文硬约束:输入图1是唯一视觉参考和目标大小/位置草稿,必须按它的目标尺寸和目标位置输出。不要找回旧尺寸目标主体。只输出同一裁切范围的干净局部照片,去掉草稿中的泛白、残影和拼接感。";
? "中文硬约束:输入图1是目标大小/位置草稿,必须按它的目标尺寸和目标位置输出;输入图2只是原始质感、非目标人物和背景参考,绝对不要把目标主体恢复成输入图2里的旧尺寸。草稿里的淡色擦除层/雾块只是提示旧位置需要补背景,不是画面内容,最终不能出现矩形雾块、白块、残影或拼接感。"
: "中文硬约束:输入图1是唯一视觉参考和目标大小/位置草稿,必须按它的目标尺寸和目标位置输出。不要找回旧尺寸目标主体。草稿里的淡色擦除层/雾块只是提示旧位置需要补背景,不是画面内容,最终不能出现矩形雾块、白块、残影或拼接感。";
})();
return [
@ -3882,7 +3938,7 @@ export async function POST(request: NextRequest) {
const scaleAdjustTextOnlyNegativePrompt =
"artificial graphic overlay, visible coordinate aid, visible line, visible label, UI element, annotation text, full image redesign, full-frame zoom, crop, pan, pasted crop, hard patch edge, color mismatch, changed sofa, changed lamp, changed wall, changed rug, duplicate subject, duplicate person, duplicate furniture, changed face, deformed person, extra limbs, stretched subject, stretched furniture, distorted furniture";
const scaleAdjustPatchNegativePrompt =
"full room image, changed crop border, artificial graphic overlay, visible coordinate aid, visible line, visible label, UI element, annotation text, guide mark, pasted crop, hard patch edge, color mismatch, gray draft patch, beige wash patch, flat painted area, jagged mask hole, black speckled stain, dark blotch, duplicate subject, duplicate person, duplicate furniture, unchanged target subject, unchanged target person, unchanged target furniture, old-size target subject, changed target identity, changed face, deformed person, broken anatomy, extra limbs, extra fingers, changed locked product, redrawn product interior, different furniture, broken woven panel, dirty woven panel, wheel inside woven panel, stretched subject, stretched furniture, distorted furniture";
"full room image, changed crop border, artificial graphic overlay, visible coordinate aid, visible line, visible label, UI element, annotation text, guide mark, pasted crop, hard patch edge, rectangular haze, white haze block, pale wipe block, color mismatch, gray draft patch, beige wash patch, flat painted area, jagged mask hole, black speckled stain, dark blotch, duplicate subject, duplicate person, duplicate furniture, unchanged target subject, unchanged target person, unchanged target furniture, old-size target subject, changed target identity, changed face, deformed person, broken anatomy, extra limbs, extra fingers, changed locked product, redrawn product interior, different furniture, broken woven panel, dirty woven panel, wheel inside woven panel, stretched subject, stretched furniture, distorted furniture";
const manualRegionReferenceImageUrls = regions.flatMap((region) =>
region.referenceImages.map((reference) => reference.url)
);
@ -4079,7 +4135,7 @@ export async function POST(request: NextRequest) {
? scaleAdjustPatchReference
? scaleAdjustPatchReference.structureLockedDraft
? "ai-patch-workspace-structure-locked-draft-v85-one-shot"
: "ai-patch-workspace-draft-v70-one-shot"
: "ai-patch-workspace-draft-v71-soft-wipe-one-shot"
: useFalScaleAdjustCoordinatePrompt
? "fal-safe-marked-photo-box-guide-no-old-crop-v44-one-shot"
: useScaleAdjustVisualLayoutGuide