fix: strengthen scale adjust semantic guide

This commit is contained in:
134 2026-07-02 16:26:05 +08:00
parent fb4ef202d8
commit 2ad8bc0073

View File

@ -843,10 +843,10 @@ function getScaleAdjustVisualGuidePlacement(params: {
function buildScaleAdjustPatchMaskSvg(params: { height: number; width: number }) {
const { height, width } = params;
const blur = Math.max(2, Math.min(18, Math.round(Math.min(width, height) * 0.055)));
const blur = Math.max(2, Math.min(14, Math.round(Math.min(width, height) * 0.035)));
const inset = Math.min(
Math.floor(Math.min(width, height) / 3),
Math.max(2, blur * 2)
Math.floor(Math.min(width, height) / 8),
Math.max(1, Math.round(blur * 0.7))
);
const rectWidth = Math.max(1, width - inset * 2);
const rectHeight = Math.max(1, height - inset * 2);
@ -858,7 +858,7 @@ function buildScaleAdjustPatchMaskSvg(params: { height: number; width: number })
<feGaussianBlur stdDeviation="${blur}" />
</filter>
</defs>
<rect x="${inset}" y="${inset}" width="${rectWidth}" height="${rectHeight}" rx="${blur}" ry="${blur}" fill="#fff" filter="url(#soft-edge)" />
<rect x="${inset}" y="${inset}" width="${rectWidth}" height="${rectHeight}" rx="${Math.max(1, blur)}" ry="${Math.max(1, blur)}" fill="#fff" filter="url(#soft-edge)" />
</svg>`
);
}
@ -876,7 +876,7 @@ function buildScaleAdjustSourceFootprintWashSvg(params: {
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.56" filter="url(#soft-wash)" />`;
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.88" filter="url(#soft-wash)" />`;
})
.join("");
@ -892,6 +892,54 @@ function buildScaleAdjustSourceFootprintWashSvg(params: {
);
}
function buildScaleAdjustTargetFootprintShadowSvg(params: {
height: number;
placements: Array<ReturnType<typeof getScaleAdjustVisualGuidePlacement>>;
width: number;
}) {
const { height, placements, width } = params;
const minSide = Math.max(1, Math.min(width, height));
const blur = Math.max(4, Math.min(18, Math.round(minSide * 0.014)));
const rects = placements
.map((placement) => {
const pad = Math.max(2, Math.round(blur * 0.45));
const left = clamp(
placement.placedLeft - pad,
0,
Math.max(0, width - 1)
);
const top = clamp(
placement.placedTop - pad,
0,
Math.max(0, height - 1)
);
const right = clamp(
placement.placedLeft + placement.placedWidth + pad,
left + 1,
width
);
const bottom = clamp(
placement.placedTop + placement.placedHeight + pad,
top + 1,
height
);
const radius = Math.max(4, Math.round(blur * 1.2));
return `<rect x="${left}" y="${top}" width="${Math.max(1, right - left)}" height="${Math.max(1, bottom - top)}" rx="${radius}" ry="${radius}" fill="#111827" fill-opacity="0.13" filter="url(#target-shadow)" />`;
})
.join("");
return Buffer.from(
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
<defs>
<filter id="target-shadow" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="${blur}" />
</filter>
</defs>
${rects}
</svg>`
);
}
async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
regions: NormalizedLocalEditRegion[];
scalePercent?: ScaleAdjustPercent | null;
@ -907,6 +955,8 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
width: number;
height: number;
}> = [];
const placements: Array<ReturnType<typeof getScaleAdjustVisualGuidePlacement>> =
[];
const composites: Array<{ input: Buffer; left: number; top: number }> = [];
for (const region of regions) {
@ -917,6 +967,7 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
width: sourceWidth,
});
sourceFootprintBoxes.push(placement.sourceBox);
placements.push(placement);
const resizedCropBuffer = await sharp(sourceBuffer, { failOn: "none" })
.extract(placement.sourceBox)
@ -958,9 +1009,22 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
},
]
: [];
const targetShadowComposite = placements.length
? [
{
input: buildScaleAdjustTargetFootprintShadowSvg({
height: sourceHeight,
placements,
width: sourceWidth,
}),
left: 0,
top: 0,
},
]
: [];
return sharp(sourceBuffer, { failOn: "none" })
.composite([...washComposite, ...composites])
.composite([...washComposite, ...targetShadowComposite, ...composites])
.png({ compressionLevel: 6 })
.toBuffer();
}
@ -1285,11 +1349,11 @@ function buildScaleAdjustPrompt(
"这是一项局部主体比例 inpaint/edit 任务,不是整图缩放、整图裁切或重新构图任务。",
sourceSizeLine,
"输入图片1是原始干净图,也是最终输出必须继承的唯一画面基底。除目标主体尺寸变化与必要背景补全外,输入图片1中的文字、Logo、装饰、排版、背景、光线、构图边界和所有未选区域必须保持不变。",
"输入图片2是一张与输入图片1尺寸和画幅完全相同的完整画面目标尺寸提示图,不是输出底图。它保留原图完整画幅作为稳定参考,只在源框旧主体占位内做轻微淡化来提示“旧主体需要被目标尺寸替换”,并把源框里的同一目标主体粗略缩放后叠放到用户希望的最终位置和大小。请只读取目标主体的最终视觉占位和原画幅关系;输入图片2里仍可见的原尺寸主体/背景只用于锁定原画幅,不能表示最终还要保留原尺寸主体。不得学习、复刻或输出淡化痕迹、矩形裁切边缘、拼接痕迹或参考图粗糙画质。",
"输入图片2是一张与输入图片1尺寸和画幅完全相同的完整画面编辑意图图,不是输出底图。它保留原图完整画幅作为稳定参考,并把源框旧主体占位强淡化为“这里的原尺寸主体需要消失并自然修复”的指令;同时把同一目标主体粗略缩放后放在用户希望的最终位置和大小。请只读取目标主体的最终视觉占位、中心位置和原画幅关系;输入图片2里被淡化的旧主体不能表示最终还要保留原尺寸主体。不得学习、复刻或输出淡化痕迹、阴影占位、矩形裁切边缘、拼接痕迹或参考图粗糙画质。",
cropLines
? `输入图片3及之后是框选区域的干净局部裁切参考图,作用是帮助你确认“要缩放的是哪个主体”。这些裁切图不是输出底图,不得被整体放大、贴回或覆盖到原图上。\n${cropLines}`
: "",
"输入图片2中的完整画面目标尺寸提示是强约束:它展示目标主体调整后的大致视觉占位、中心位置,以及整张图不应缩放/裁切/平移的稳定画幅关系。最终输出必须回到输入图片1的真实照片环境中完成自然编辑,不得直接粘贴 input2 的矩形图块;主体不需要填满目标矩形,必须保持主体自身原始宽高比和透视形体。",
"输入图片2中的完整画面编辑意图是强约束:淡化的 OLD 占位表示旧尺寸主体要被移除/修复,清晰的 TARGET 占位表示最终主体大小和中心位置;整张图不应缩放、裁切或平移。最终输出必须回到输入图片1的真实照片环境中完成自然编辑,不得直接粘贴 input2 的矩形图块;主体不需要填满目标矩形,必须保持主体自身原始宽高比和透视形体。",
"如果系统或模型只能读取一张参考图,必须优先以输入图片1为输出基底,不得清理、改写或删除输入图片1中框选区域外的任何文字和图形。",
"系统已根据用户框选区域计算出下方每个区域的原图坐标;框选区域只是定位目标主体的搜索范围,不是最终画面的裁切块、贴片、蒙版边界,也不是放大后的空间上限,更不是要整体缩放的矩形图层。",
"最终结果必须保持原始干净图的画幅比例、构图边界、镜头透视、光线、色温、材质质感和清晰度;严禁整张图整体缩放、裁切、平移、重新排版或重新生成。",
@ -1374,7 +1438,7 @@ function buildScaleAdjustCoordinatePrompt(
"中文硬约束:只调整 OLD 框中的主要家具主体。TARGET 框是最终占位;如果是缩小,OLD 框里超出 TARGET 的旧家具必须消失并补成自然背景;如果是放大,同一件家具必须明显变大。禁止整图缩放、整图裁切、整图重绘。",
"一次成功硬约束:不要保守返回原尺寸。目标家具的可见轮廓必须明显从 OLD 原始占位变到 TARGET 占位;如果最终看起来和原图一样,就是失败。",
`Return one seamless realistic photo with the same frame and pixel size as input image 1: ${sourceSize.width} x ${sourceSize.height} px.`,
"Input image 2 is a full-frame target-size placement hint with the same canvas as input image 1, not output content and not a photo layer. It keeps the original room frame visible as the locked reference, softly mutes only the OLD source footprint to indicate replacement, and overlays the same furniture at the required TARGET footprint. Read only the final furniture footprint, center, and locked frame relationship from input image 2. If the original-size furniture is still visible behind the rough hint, ignore that old footprint in the final; the final must contain only the TARGET-size furniture. Never copy muted wash, rectangular crop edges, rough patch quality, seams, or guide artifacts into the output.",
"Input image 2 is a full-frame semantic edit map with the same canvas as input image 1, not output content and not a photo layer. It keeps the original room frame visible as the locked reference, strongly fades the OLD source footprint to mean remove/heal the old full-size furniture, and shows the same furniture at the required TARGET footprint. Read only the final furniture footprint, center, and locked frame relationship from input image 2. If any original-size furniture is still faintly visible in the faded OLD area, treat it as removed in the final; the final must contain only the TARGET-size furniture. Never copy faded wash, placeholder shadow, rectangular crop edges, rough patch quality, seams, or guide artifacts into the output.",
cropLines ? `${cropLines}` : "",
"",
"Task:",
@ -1390,7 +1454,7 @@ function buildScaleAdjustCoordinatePrompt(
"Region requirements:",
regionLines,
"",
"Success check: compare the final furniture footprint with input image 1 and the full-frame target-size placement hint in input image 2. The selected furniture size must clearly change from the original and align much closer to TARGET than OLD, while the rest of the photo stays stable.",
"Success check: compare the final furniture footprint with input image 1 and the full-frame semantic edit map in input image 2. The selected furniture size must clearly change from the original and align much closer to TARGET than OLD, while the rest of the photo stays stable.",
"Do not paste rectangular image areas. Do not output layout-reference overlays, muted wash, coordinate-map lines, labels, boxes, seams, hard patch edges, haze, color mismatch, full-canvas zoom, crop, pan, or redesign.",
"Return only the clean edited photo.",
].join("\n");
@ -1399,13 +1463,13 @@ function buildScaleAdjustCoordinatePrompt(
function buildScaleAdjustCoordinateSystemPrompt() {
return [
"You are an image editing model for local furniture scale adjustment.",
"Input image 1 is the only final photo base. Input image 2 is a full-frame target-size placement hint, not a photo layer.",
"Input image 1 is the only final photo base. Input image 2 is a full-frame semantic edit map, not a photo layer.",
"Follow both English and Chinese instructions. The Chinese lines are hard constraints for avoiding unchanged furniture and full-canvas zoom.",
"Generate a clean realistic final photo by editing the target furniture itself and naturally rebuilding only the tiny surrounding background that becomes visible.",
"Prefer a clear visible size change over a conservative unchanged result; unchanged furniture is a failed edit.",
"When shrinking, remove the old full-size furniture footprint outside the target footprint and heal it as natural background. Large furniture is editable and must not remain locked at original size.",
"Do not paste, composite, clone, crop, resize, or copy rectangular image patches from any input.",
"Never output guide artifacts from the layout reference: no muted wash, overlay patch, rough crop rectangle, lines, labels, blocks, seams, rectangular borders, arrows, masks, UI marks, or annotations.",
"Never output guide artifacts from the edit map: no faded wash, placeholder shadow, overlay patch, rough crop rectangle, lines, labels, blocks, seams, rectangular borders, arrows, masks, UI marks, or annotations.",
"Keep the camera frame, lighting, wall, floor, rug, windows, background furniture, text, logos, and all non-target regions stable.",
].join("\n");
}
@ -1629,7 +1693,7 @@ export async function POST(request: NextRequest) {
)
: buildLocalEditPrompt(regions);
const scaleAdjustCoordinateNegativePrompt =
"visible annotation, guide overlay, translucent overlay, target placement overlay, muted wash, text label, arrows, boxes, rectangle border, UI marks, pasted patch, rough layout patch, hard crop edge, seam, hard rectangular edge, color mismatch, different-resolution area, whole image zoom, whole image crop, whole image pan, whole image redesign, unchanged target furniture, stretched furniture, squashed furniture, duplicate furniture, second copy, extra legs, extra rails, side panels, shelves, cabinets, lattice panels";
"visible annotation, guide overlay, translucent overlay, target placement overlay, faded wash, placeholder shadow, text label, arrows, boxes, rectangle border, UI marks, pasted patch, rough layout patch, hard crop edge, seam, hard rectangular edge, color mismatch, different-resolution area, whole image zoom, whole image crop, whole image pan, whole image redesign, unchanged target furniture, old-size target furniture, stretched furniture, squashed furniture, duplicate furniture, second copy, extra legs, extra rails, side panels, shelves, cabinets, lattice panels";
const manualRegionReferenceImageUrls = regions.flatMap((region) =>
region.referenceImages.map((reference) => reference.url)
);
@ -1683,7 +1747,7 @@ export async function POST(request: NextRequest) {
annotatedReferenceUrl: annotatedUpload.url,
annotatedReferenceSentToModel: isScaleAdjustOperation,
annotatedReferenceType: isScaleAdjustOperation
? "scale-adjust-full-frame-target-size-placement-hint"
? "scale-adjust-full-frame-semantic-edit-map"
: "source-overlay",
cleanSourceReferenceIndex: 1,
configuredDetailModelId: configuredDetailModel || null,
@ -1727,8 +1791,8 @@ export async function POST(request: NextRequest) {
.length,
scaleAdjustPromptStrategy: isScaleAdjustOperation
? useScaleAdjustCoordinateGuide
? "same-name-locked-source-tight-muted-target-placement-hint-v36-one-shot"
: "locked-plate-tight-muted-target-placement-hint-v13"
? "same-name-locked-source-strong-erasure-semantic-target-map-v37-one-shot"
: "locked-plate-strong-erasure-semantic-target-map-v14"
: undefined,
scaleAdjustPostCompositeApplied: false,
scaleAdjustUsesFullFrameOutput: isScaleAdjustOperation,