fix: soften local edit composite boundaries
This commit is contained in:
parent
f29a26b43f
commit
72e4b926d9
@ -450,6 +450,8 @@ function buildLocalEditPrompt(regions: NormalizedLocalEditRegion[]) {
|
||||
regionLines,
|
||||
referenceLines ? `\n区域专属参考图说明:\n${referenceLines}` : "",
|
||||
"",
|
||||
"新增主体完整性要求:如果区域提示词要求新增动物、人物、商品或其它完整主体,必须把该主体完整放在框选区域的安全内边距内,四周至少预留 8%-12% 缓冲空间;不要让头、身体、四肢、尾巴、产品边缘或阴影贴到选区边界,更不能被边界截断。空间不足时请主动缩小新增主体,而不是裁切主体。",
|
||||
"边缘融合要求:编辑区域与未编辑区域的地面、墙面、桌面、椅脚、阴影、纹理和透视必须连续自然,不得出现错位、断层、硬边、矩形补丁、局部贴片或不同清晰度的边界。",
|
||||
"最终输出必须是干净照片:不得出现任何彩色框、蓝框、绿框、红框、矩形边框、编号、提示词气泡、标注文字或 UI 元素。",
|
||||
"未被列为编辑区域的画面必须逐像素保持原图不变,不允许重绘、改色、补光、换材质、移动物体、改变清晰度或改变背景细节。",
|
||||
"新增内容必须匹配原图的透视、光线、比例、清晰度、色温和真实摄影风格。",
|
||||
@ -648,7 +650,7 @@ export async function POST(request: NextRequest) {
|
||||
negativePrompt:
|
||||
isScaleAdjustOperation
|
||||
? "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框、白边、画幅压扁、画面拉伸、矩形贴片、硬边、拼接线、补丁感、局部裁切、色差边缘、背景断裂、主体未缩放、整块框选区域缩放"
|
||||
: "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框、白边、画幅压扁、画面拉伸",
|
||||
: "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框、白边、画幅压扁、画面拉伸、主体截断、动物截断、缺头、缺尾巴、缺四肢、贴边裁切、矩形贴片、硬边、拼接线、补丁感、色差边缘、背景错位、地面纹理断裂、椅脚错位",
|
||||
prompt,
|
||||
promptExtend: false,
|
||||
referenceImages,
|
||||
|
||||
@ -2284,8 +2284,8 @@ function DetailResultProcessingScanOverlay({
|
||||
<div className="absolute inset-0 z-30 overflow-hidden rounded-[inherit] bg-[#07090f]">
|
||||
<style>{`
|
||||
@keyframes detailResultScanBreatheBlur {
|
||||
0%, 100% { filter: blur(22px) brightness(0.78) saturate(0.86); transform: scale(1.13); }
|
||||
50% { filter: blur(26px) brightness(0.9) saturate(0.94); transform: scale(1.16); }
|
||||
0%, 100% { filter: blur(14px) brightness(0.8) saturate(0.9); transform: scale(1.08); }
|
||||
50% { filter: blur(17px) brightness(0.9) saturate(0.98); transform: scale(1.1); }
|
||||
}
|
||||
@keyframes detailResultScanSweep {
|
||||
0% { top: -7%; opacity: 0; }
|
||||
@ -2358,15 +2358,27 @@ function DetailResultProcessingScanOverlay({
|
||||
"0 0 6px rgba(255,255,255,0.7), 0 0 18px rgba(125,211,252,0.55), 0 0 34px rgba(167,139,250,0.35)",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute right-3 top-3 z-[60] inline-flex items-center gap-1.5 rounded-full border border-white/10 bg-[#07090f]/60 px-2.5 py-1.5 text-[11px] font-medium text-slate-100 shadow-[0_12px_28px_rgba(0,0,0,0.28)] backdrop-blur-md">
|
||||
<div
|
||||
className="absolute right-3 top-3 z-[70] isolate inline-flex items-center gap-1.5 rounded-full border border-white/14 bg-[#080b12]/95 px-2.5 py-1.5 text-[11px] font-semibold leading-none text-white shadow-[0_12px_28px_rgba(0,0,0,0.34)]"
|
||||
style={{
|
||||
WebkitFontSmoothing: "antialiased",
|
||||
textShadow: "0 1px 1px rgba(0,0,0,0.7)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full bg-sky-300"
|
||||
style={{ animation: "detailResultScanPulseRing 1.8s ease-out infinite" }}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
<span className="min-w-[34px] text-right tabular-nums text-slate-400">{progress}%</span>
|
||||
<span className="min-w-[34px] text-right tabular-nums text-slate-300">{progress}%</span>
|
||||
</div>
|
||||
<div className="absolute bottom-3 left-3 z-[60] inline-flex items-center gap-2 rounded-full border border-white/10 bg-[#07090f]/60 px-2.5 py-1.5 text-[11px] font-medium text-slate-400 shadow-[0_12px_28px_rgba(0,0,0,0.25)] backdrop-blur-md">
|
||||
<div
|
||||
className="absolute bottom-3 left-3 z-[70] isolate inline-flex items-center gap-2 rounded-full border border-white/14 bg-[#080b12]/95 px-2.5 py-1.5 text-[11px] font-semibold leading-none text-slate-100 shadow-[0_12px_28px_rgba(0,0,0,0.32)]"
|
||||
style={{
|
||||
WebkitFontSmoothing: "antialiased",
|
||||
textShadow: "0 1px 1px rgba(0,0,0,0.72)",
|
||||
}}
|
||||
>
|
||||
<span className="tabular-nums text-sky-300">{String(stageIndex + 1).padStart(2, "0")}</span>
|
||||
<span>{stageName}</span>
|
||||
</div>
|
||||
|
||||
@ -80,6 +80,12 @@ export interface CanvasGenerateImageResult {
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
const LOCAL_EDIT_COMPOSITE_PADDING_RATIO = 0.14;
|
||||
const LOCAL_EDIT_COMPOSITE_MIN_PADDING_RATIO = 0.012;
|
||||
const LOCAL_EDIT_COMPOSITE_MAX_PADDING_RATIO = 0.06;
|
||||
const LOCAL_EDIT_COMPOSITE_FEATHER_MIN_PX = 10;
|
||||
const LOCAL_EDIT_COMPOSITE_FEATHER_MAX_PX = 44;
|
||||
|
||||
const SUPPORTED_CANVAS_PROVIDER_TYPES = new Set([
|
||||
"gemini",
|
||||
"grsai-gpt-image",
|
||||
@ -153,6 +159,38 @@ function clampRatio(value: number) {
|
||||
return Math.min(1, Math.max(0, value));
|
||||
}
|
||||
|
||||
function expandLocalEditCompositeRegion(region: CanvasLocalEditCompositeRegion) {
|
||||
const padX = clampRatio(
|
||||
Math.min(
|
||||
LOCAL_EDIT_COMPOSITE_MAX_PADDING_RATIO,
|
||||
Math.max(
|
||||
LOCAL_EDIT_COMPOSITE_MIN_PADDING_RATIO,
|
||||
region.width * LOCAL_EDIT_COMPOSITE_PADDING_RATIO
|
||||
)
|
||||
)
|
||||
);
|
||||
const padY = clampRatio(
|
||||
Math.min(
|
||||
LOCAL_EDIT_COMPOSITE_MAX_PADDING_RATIO,
|
||||
Math.max(
|
||||
LOCAL_EDIT_COMPOSITE_MIN_PADDING_RATIO,
|
||||
region.height * LOCAL_EDIT_COMPOSITE_PADDING_RATIO
|
||||
)
|
||||
)
|
||||
);
|
||||
const left = clampRatio(region.x - padX);
|
||||
const top = clampRatio(region.y - padY);
|
||||
const right = clampRatio(region.x + region.width + padX);
|
||||
const bottom = clampRatio(region.y + region.height + padY);
|
||||
|
||||
return {
|
||||
x: left,
|
||||
y: top,
|
||||
width: Math.max(0, right - left),
|
||||
height: Math.max(0, bottom - top),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLocalEditCompositeConfig(
|
||||
value: CanvasGenerateImageRequest["localEditComposite"]
|
||||
): CanvasLocalEditCompositeConfig | null {
|
||||
@ -174,6 +212,7 @@ function normalizeLocalEditCompositeConfig(
|
||||
height: Math.min(height, Math.max(0, 1 - y)),
|
||||
};
|
||||
})
|
||||
.map(expandLocalEditCompositeRegion)
|
||||
.filter((region) => region.width > 0 && region.height > 0);
|
||||
|
||||
return regions.length > 0 ? { sourceImageUrl, regions } : null;
|
||||
@ -185,6 +224,13 @@ function buildLocalEditCompositeMaskSvg(params: {
|
||||
regions: CanvasLocalEditCompositeRegion[];
|
||||
}) {
|
||||
const { width, height, regions } = params;
|
||||
const blur = Math.max(
|
||||
LOCAL_EDIT_COMPOSITE_FEATHER_MIN_PX,
|
||||
Math.min(
|
||||
LOCAL_EDIT_COMPOSITE_FEATHER_MAX_PX,
|
||||
Math.round(Math.min(width, height) * 0.012)
|
||||
)
|
||||
);
|
||||
const rects = regions
|
||||
.map((region) => {
|
||||
const left = Math.max(0, Math.round(region.x * width));
|
||||
@ -197,13 +243,21 @@ function buildLocalEditCompositeMaskSvg(params: {
|
||||
height - top,
|
||||
Math.max(1, Math.round(region.height * height))
|
||||
);
|
||||
return `<rect x="${left}" y="${top}" width="${rectWidth}" height="${rectHeight}" fill="#ffffff" />`;
|
||||
return `<rect x="${left}" y="${top}" width="${rectWidth}" height="${rectHeight}" rx="${Math.round(blur * 0.9)}" fill="#ffffff" />`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
return Buffer.from(`
|
||||
<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<filter id="local-edit-soft-mask" x="-30%" y="-30%" width="160%" height="160%" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="${blur}" />
|
||||
</filter>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="${width}" height="${height}" fill="rgba(0,0,0,0)" />
|
||||
<g filter="url(#local-edit-soft-mask)">
|
||||
${rects}
|
||||
</g>
|
||||
${rects}
|
||||
</svg>
|
||||
`);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user