fix: guide scale adjustment target sizing
This commit is contained in:
parent
72e4b926d9
commit
43769c5b58
@ -288,6 +288,26 @@ function rectToPixels(
|
||||
return { left, top, width, height };
|
||||
}
|
||||
|
||||
function scaleRectAroundCenter(
|
||||
rect: NormalizedLocalEditRegion["rect"],
|
||||
scalePercent: ScaleAdjustPercent
|
||||
) {
|
||||
const scale = Math.max(0.1, 1 + scalePercent / 100);
|
||||
const targetWidth = Math.min(1, rect.width * scale);
|
||||
const targetHeight = Math.min(1, rect.height * scale);
|
||||
const centerX = rect.x + rect.width / 2;
|
||||
const centerY = rect.y + rect.height / 2;
|
||||
const x = clamp(centerX - targetWidth / 2, 0, Math.max(0, 1 - targetWidth));
|
||||
const y = clamp(centerY - targetHeight / 2, 0, Math.max(0, 1 - targetHeight));
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
};
|
||||
}
|
||||
|
||||
function wrapText(text: string, maxChars: number, maxLines = 3) {
|
||||
const compact = text.replace(/\s+/g, " ").trim();
|
||||
if (!compact) return [];
|
||||
@ -416,6 +436,99 @@ function buildAnnotationSvg(params: {
|
||||
`);
|
||||
}
|
||||
|
||||
function buildScaleAdjustAnnotationSvg(params: {
|
||||
width: number;
|
||||
height: number;
|
||||
regions: NormalizedLocalEditRegion[];
|
||||
scalePercent: ScaleAdjustPercent;
|
||||
}) {
|
||||
const { width, height, regions, scalePercent } = params;
|
||||
const minSide = Math.max(1, Math.min(width, height));
|
||||
const strokeWidth = Math.max(4, Math.round(minSide * 0.006));
|
||||
const fontSize = Math.max(22, Math.round(minSide * 0.024));
|
||||
const smallFontSize = Math.max(18, Math.round(fontSize * 0.78));
|
||||
const targetColor = scalePercent > 0 ? "#00d4ff" : "#00c853";
|
||||
const sourceColor = "#ff3b30";
|
||||
const actionLabel = getScaleAdjustLabel(scalePercent);
|
||||
const targetLabel = getScaleAdjustTargetLabel(scalePercent);
|
||||
const dash = Math.max(12, Math.round(strokeWidth * 2.8));
|
||||
const gap = Math.max(8, Math.round(strokeWidth * 1.8));
|
||||
|
||||
const regionSvg = regions
|
||||
.map((region) => {
|
||||
const source = region.pixelRect;
|
||||
const targetRect = scaleRectAroundCenter(region.rect, scalePercent);
|
||||
const target = rectToPixels(targetRect, width, height);
|
||||
const sourceCx = source.left + source.width / 2;
|
||||
const sourceCy = source.top + source.height / 2;
|
||||
const targetCx = target.left + target.width / 2;
|
||||
const targetCy = target.top + target.height / 2;
|
||||
const badgeSize = Math.max(32, Math.round(fontSize * 1.3));
|
||||
const labelWidth = Math.min(
|
||||
Math.max(260, Math.round(fontSize * 12.8)),
|
||||
Math.max(180, width - 24)
|
||||
);
|
||||
const labelHeight = Math.round(fontSize * 2.55);
|
||||
const labelX = clamp(
|
||||
target.left,
|
||||
12,
|
||||
Math.max(12, width - labelWidth - 12)
|
||||
);
|
||||
const labelY = clamp(
|
||||
target.top - labelHeight - Math.round(fontSize * 0.55),
|
||||
12,
|
||||
Math.max(12, height - labelHeight - 12)
|
||||
);
|
||||
|
||||
const sourceCorners = [
|
||||
[source.left, source.top],
|
||||
[source.left + source.width, source.top],
|
||||
[source.left + source.width, source.top + source.height],
|
||||
[source.left, source.top + source.height],
|
||||
];
|
||||
const targetCorners = [
|
||||
[target.left, target.top],
|
||||
[target.left + target.width, target.top],
|
||||
[target.left + target.width, target.top + target.height],
|
||||
[target.left, target.top + target.height],
|
||||
];
|
||||
const arrows = sourceCorners
|
||||
.map(([sx, sy], index) => {
|
||||
const [tx, ty] = targetCorners[index];
|
||||
const fromX = scalePercent > 0 ? sx : tx;
|
||||
const fromY = scalePercent > 0 ? sy : ty;
|
||||
const toX = scalePercent > 0 ? tx : sx;
|
||||
const toY = scalePercent > 0 ? ty : sy;
|
||||
return `<line x1="${fromX}" y1="${fromY}" x2="${toX}" y2="${toY}" stroke="${targetColor}" stroke-width="${Math.max(3, strokeWidth * 0.72)}" stroke-linecap="round" opacity="0.82" marker-end="url(#scale-adjust-arrow)" />`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<rect x="${source.left}" y="${source.top}" width="${source.width}" height="${source.height}" rx="${Math.round(strokeWidth * 2)}" fill="${sourceColor}" opacity="0.07" stroke="${sourceColor}" stroke-width="${strokeWidth}" />
|
||||
<rect x="${target.left}" y="${target.top}" width="${target.width}" height="${target.height}" rx="${Math.round(strokeWidth * 2.5)}" fill="${targetColor}" opacity="0.09" stroke="${targetColor}" stroke-width="${Math.max(4, Math.round(strokeWidth * 1.15))}" stroke-dasharray="${dash} ${gap}" />
|
||||
${arrows}
|
||||
<line x1="${sourceCx}" y1="${sourceCy}" x2="${targetCx}" y2="${targetCy}" stroke="${targetColor}" stroke-width="${Math.max(2, Math.round(strokeWidth * 0.45))}" opacity="0.5" />
|
||||
<circle cx="${clamp(source.left, badgeSize / 2 + 6, width - badgeSize / 2 - 6)}" cy="${clamp(source.top, badgeSize / 2 + 6, height - badgeSize / 2 - 6)}" r="${badgeSize / 2}" fill="${sourceColor}" stroke="#ffffff" stroke-width="${Math.max(2, Math.round(strokeWidth * 0.45))}" />
|
||||
<text x="${clamp(source.left, badgeSize / 2 + 6, width - badgeSize / 2 - 6)}" y="${clamp(source.top, badgeSize / 2 + 6, height - badgeSize / 2 - 6) + smallFontSize * 0.38}" text-anchor="middle" fill="#ffffff" font-size="${smallFontSize}" font-family="Arial, Helvetica, sans-serif" font-weight="800">${region.index}</text>
|
||||
<rect x="${labelX}" y="${labelY}" width="${labelWidth}" height="${labelHeight}" rx="${Math.round(fontSize * 0.55)}" fill="#061017" opacity="0.86" stroke="${targetColor}" stroke-width="${Math.max(2, Math.round(strokeWidth * 0.5))}" />
|
||||
<text x="${labelX + Math.round(fontSize * 0.7)}" y="${labelY + Math.round(fontSize * 1.03)}" fill="#ffffff" font-size="${fontSize}" font-family="Arial, Helvetica, sans-serif" font-weight="800">目标:${escapeXml(actionLabel)},主体约 ${escapeXml(targetLabel)}</text>
|
||||
<text x="${labelX + Math.round(fontSize * 0.7)}" y="${labelY + Math.round(fontSize * 2.03)}" fill="${targetColor}" font-size="${smallFontSize}" font-family="Arial, Helvetica, sans-serif" font-weight="700">让主体轮廓接近虚线框,不要保持原红框大小</text>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
return Buffer.from(`
|
||||
<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<marker id="scale-adjust-arrow" markerWidth="12" markerHeight="12" refX="9" refY="6" orient="auto" markerUnits="strokeWidth">
|
||||
<path d="M2,2 L10,6 L2,10 Z" fill="${targetColor}" />
|
||||
</marker>
|
||||
</defs>
|
||||
${regionSvg}
|
||||
</svg>
|
||||
`);
|
||||
}
|
||||
|
||||
function buildLocalEditPrompt(regions: NormalizedLocalEditRegion[]) {
|
||||
const regionLines = regions
|
||||
.map((region) => {
|
||||
@ -471,17 +584,23 @@ function buildScaleAdjustPrompt(
|
||||
const top = Math.round(region.rect.y * 1000) / 10;
|
||||
const width = Math.round(region.rect.width * 1000) / 10;
|
||||
const height = Math.round(region.rect.height * 1000) / 10;
|
||||
const targetRect = scaleRectAroundCenter(region.rect, scalePercent);
|
||||
const targetLeft = Math.round(targetRect.x * 1000) / 10;
|
||||
const targetTop = Math.round(targetRect.y * 1000) / 10;
|
||||
const targetWidth = Math.round(targetRect.width * 1000) / 10;
|
||||
const targetHeight = Math.round(targetRect.height * 1000) / 10;
|
||||
const pixel = region.pixelRect;
|
||||
const right = pixel.left + pixel.width;
|
||||
const bottom = pixel.top + pixel.height;
|
||||
const centerX = pixel.left + Math.round(pixel.width / 2);
|
||||
const centerY = pixel.top + Math.round(pixel.height / 2);
|
||||
return `区域 ${region.index}(相对位置 x=${left}%,y=${top}%,w=${width}%,h=${height}%;像素范围 left=${pixel.left}, top=${pixel.top}, right=${right}, bottom=${bottom}, center=(${centerX}, ${centerY})):该矩形只是“寻找目标主体”的范围,不是要缩放或贴回的矩形补丁。请识别此范围内最主要的商品/物体主体,以主体自身轮廓和视觉中心为准,执行整体等比${actionLabel},目标视觉尺寸必须约为原主体的 ${targetLabel}。如果框中只覆盖到主体的一部分,请根据连接关系推断该主体的完整可见结构,只调整该主体,不调整矩形框内背景。`;
|
||||
return `区域 ${region.index}(原始红色实线框:x=${left}%,y=${top}%,w=${width}%,h=${height}%;目标虚线框:x=${targetLeft}%,y=${targetTop}%,w=${targetWidth}%,h=${targetHeight}%;像素范围 left=${pixel.left}, top=${pixel.top}, right=${right}, bottom=${bottom}, center=(${centerX}, ${centerY})):红色实线框只是“寻找目标主体”的范围,不是要缩放或贴回的矩形补丁。请识别此范围内最主要的商品/物体主体,以主体自身轮廓和视觉中心为准,执行整体等比${actionLabel},目标主体最终外接轮廓必须接近第 1 张图中的青绿色/绿色虚线目标框,视觉尺寸约为原主体的 ${targetLabel}。如果框中只覆盖到主体的一部分,请根据连接关系推断该主体的完整可见结构,只调整该主体,不调整矩形框内背景。`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return [
|
||||
"输入图片1是定位参考图:它只用于告诉你用户框选的目标主体位置。输入图片1里的彩色框、编号、标签、半透明遮罩、文字和 UI 只是定位标记,绝不能出现在最终画面中。",
|
||||
"输入图片1中的红色实线框表示目标主体当前大致位置;青绿色或绿色虚线框表示比例调整后的目标视觉轮廓。最终画面不得出现这些框线,但目标主体大小必须接近虚线框所表达的新尺寸。",
|
||||
"输入图片2是原始干净图,也是最终输出必须继承的唯一画面基底。如果系统或模型只能读取一张参考图,则必须使用输入图片1中的框选位置定位目标,同时主动去除所有定位标记并恢复干净画面。",
|
||||
"系统已根据用户框选区域计算出下方每个区域的原图坐标;框选区域只是定位目标主体的搜索范围,不是最终画面的裁切块、贴片或蒙版边界。",
|
||||
"最终结果必须保持原始干净图的画幅比例、构图边界、镜头透视、光线、色温、材质质感和清晰度。",
|
||||
@ -491,10 +610,10 @@ function buildScaleAdjustPrompt(
|
||||
regionLines,
|
||||
"",
|
||||
"关键要求:",
|
||||
`1. 只调整目标主体物体本身的整体视觉比例,必须等比缩放到约 ${targetLabel},不得保留原尺寸;请把主体轮廓宽度和高度都调整到约 ${targetLabel},而不是仅改变亮度、清晰度、颜色或画面裁切。`,
|
||||
`1. 只调整目标主体物体本身的整体视觉比例,必须等比缩放到约 ${targetLabel},不得保留原尺寸;请把主体轮廓宽度和高度都调整到约 ${targetLabel},并尽量贴近第 1 张图的虚线目标框,而不是仅改变亮度、清晰度、颜色或画面裁切。`,
|
||||
"2. 严禁把整个矩形框选区域当成图片块整体缩放、平移、复制、裁切或贴回;最终画面不得出现矩形拼接边缘、色差硬边、补丁感、裁切线或局部贴片痕迹。",
|
||||
`3. 缩小时,目标主体应围绕原位置视觉中心自然变小到约 ${targetLabel};主体缩小后,原来被遮挡的背景、桌面、墙面、阴影、纹理和透视必须被重新生成并无缝补全,选区内应出现明显新增留白或背景。`,
|
||||
`4. 放大时,目标主体应围绕原位置视觉中心自然变大到约 ${targetLabel};边缘与遮挡关系要合理,不能变形,也不能产生突兀裁切。`,
|
||||
`4. 放大时,目标主体应围绕原位置视觉中心自然变大到约 ${targetLabel};主体边缘应越过原始红框并接近虚线目标框,边缘与遮挡关系要合理,不能变形,也不能产生突兀裁切。`,
|
||||
"5. 框选区域内非目标主体的背景、墙面、桌面、植物、文字和其它物体应保持原场景逻辑,不要跟着主体一起缩放。",
|
||||
"6. 未受比例调整影响的其它画面区域必须尽量保持原图不变,不允许整体重绘、改色、补光、移动物体或改变背景细节。",
|
||||
"7. 最终输出必须是一张完整、干净、无缝的照片,不得出现任何选区框、说明文字、对比线、比例数字或 UI 元素。",
|
||||
@ -587,12 +706,19 @@ export async function POST(request: NextRequest) {
|
||||
operation === SCALE_ADJUST_OPERATION && scalePercent !== null;
|
||||
const displayPrompt = buildLocalEditDisplayPrompt({ operation, scalePercent });
|
||||
|
||||
const annotationSvg = buildAnnotationSvg({
|
||||
height: sourceHeight,
|
||||
hidePromptText: isScaleAdjustOperation,
|
||||
regions,
|
||||
width: sourceWidth,
|
||||
});
|
||||
const annotationSvg = isScaleAdjustOperation
|
||||
? buildScaleAdjustAnnotationSvg({
|
||||
height: sourceHeight,
|
||||
regions,
|
||||
scalePercent: scalePercent as ScaleAdjustPercent,
|
||||
width: sourceWidth,
|
||||
})
|
||||
: buildAnnotationSvg({
|
||||
height: sourceHeight,
|
||||
hidePromptText: false,
|
||||
regions,
|
||||
width: sourceWidth,
|
||||
});
|
||||
const annotatedBuffer = await sharp(sourceBuffer, { failOn: "none" })
|
||||
.composite([{ input: annotationSvg, left: 0, top: 0 }])
|
||||
.png({ compressionLevel: 6 })
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user