fix: guide scale adjust as localized edit

This commit is contained in:
134 2026-06-22 11:08:51 +08:00
parent 072fac14cc
commit 8519aa70e7

View File

@ -600,13 +600,9 @@ function buildScaleAdjustAnnotationSvg(params: {
}) {
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 strokeWidth = Math.max(3, Math.round(minSide * 0.0048));
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));
@ -615,71 +611,31 @@ function buildScaleAdjustAnnotationSvg(params: {
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 radius = Math.max(8, Math.round(strokeWidth * 2));
const badgeSize = Math.max(22, Math.round(minSide * 0.022));
const badgeX = clamp(
source.left + Math.round(badgeSize * 0.45),
badgeSize / 2 + 4,
width - badgeSize / 2 - 4
);
const labelHeight = Math.round(fontSize * 2.55);
const labelX = clamp(
target.left,
12,
Math.max(12, width - labelWidth - 12)
const badgeY = clamp(
source.top + Math.round(badgeSize * 0.45),
badgeSize / 2 + 4,
height - badgeSize / 2 - 4
);
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("");
const fontSize = Math.max(13, Math.round(badgeSize * 0.52));
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>
<rect x="${source.left}" y="${source.top}" width="${source.width}" height="${source.height}" rx="${radius}" fill="${sourceColor}" opacity="0.025" stroke="${sourceColor}" stroke-width="${strokeWidth}" />
<rect x="${target.left}" y="${target.top}" width="${target.width}" height="${target.height}" rx="${radius}" fill="${targetColor}" opacity="0.035" stroke="${targetColor}" stroke-width="${Math.max(3, Math.round(strokeWidth * 1.05))}" stroke-dasharray="${dash} ${gap}" />
<circle cx="${badgeX}" cy="${badgeY}" r="${badgeSize / 2}" fill="${sourceColor}" opacity="0.92" stroke="#ffffff" stroke-width="${Math.max(1.5, Math.round(strokeWidth * 0.36))}" />
<text x="${badgeX}" y="${badgeY + fontSize * 0.36}" text-anchor="middle" fill="#ffffff" font-size="${fontSize}" font-family="Arial, Helvetica, sans-serif" font-weight="800">${region.index}</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>
`);
@ -731,10 +687,14 @@ function buildLocalEditPrompt(regions: NormalizedLocalEditRegion[]) {
function buildScaleAdjustPrompt(
regions: NormalizedLocalEditRegion[],
scalePercent: ScaleAdjustPercent,
cropReferences: ScaleAdjustCropReference[] = []
cropReferences: ScaleAdjustCropReference[] = [],
sourceSize?: { width: number; height: number }
) {
const actionLabel = getScaleAdjustLabel(scalePercent);
const targetLabel = getScaleAdjustTargetLabel(scalePercent);
const sourceSizeLine = sourceSize
? `最终输出必须保持与输入图片1完全相同的画幅和像素尺寸:${sourceSize.width} x ${sourceSize.height} px。`
: "最终输出必须保持与输入图片1完全相同的画幅和像素尺寸。";
const regionLines = regions
.map((region) => {
const left = Math.round(region.rect.x * 1000) / 10;
@ -769,8 +729,10 @@ function buildScaleAdjustPrompt(
.join("\n");
return [
"这是一项局部主体比例 inpaint/edit 任务,不是整图缩放、整图裁切或重新构图任务。",
sourceSizeLine,
"输入图片1是原始干净图,也是最终输出必须继承的唯一画面基底。除目标主体尺寸变化与必要背景补全外,输入图片1中的文字、Logo、装饰、排版、背景、光线、构图边界和所有未选区域必须保持不变。",
"输入图片2是定位参考图:它只用于告诉你用户框选的目标主体位置和目标尺寸范围,不是输出底图。输入图片2里的彩色框、编号、标签、半透明遮罩、文字和 UI 只是定位标记,绝不能出现在最终画面中,也不得影响输入图片1的文字和排版。",
"输入图片2是低噪声定位参考图:红色实线框用于寻找当前主体,青绿色或绿色虚线框用于表达目标尺寸范围。输入图片2不是输出底图,不得学习或复刻它的框线、编号、半透明遮罩或 UI 标记。",
cropLines
? `输入图片3及之后是框选区域的干净局部裁切参考图,作用是帮助你确认“要缩放的是哪个主体”。这些裁切图不是输出底图,不得被整体放大、贴回或覆盖到原图上。\n${cropLines}`
: "",
@ -778,9 +740,10 @@ function buildScaleAdjustPrompt(
"如果系统或模型只能读取一张参考图,必须优先以输入图片1为输出基底,不得清理、改写或删除输入图片1中框选区域外的任何文字和图形。",
"系统已根据用户框选区域计算出下方每个区域的原图坐标;框选区域只是定位目标主体的搜索范围,不是最终画面的裁切块、贴片或蒙版边界。",
"最终结果必须保持原始干净图的画幅比例、构图边界、镜头透视、光线、色温、材质质感和清晰度;严禁整张图整体缩放、裁切、平移、重新排版或重新生成。",
"Highest priority rule: do NOT zoom, crop, resize, reframe, pan, or regenerate the full canvas. Treat image 1 as a locked background plate and only inpaint the selected subject footprint.",
"请不要生成任何彩色框、矩形边框、编号圆点、提示词气泡、标注文字、UI 控件或半透明色块。",
"",
`任务:对用户框选的主体物体做“比例调整”,统一执行${actionLabel}。目标主体调整后的视觉尺寸必须约为原主体的 ${targetLabel},普通用户肉眼应能清楚看出主体尺寸已经变化;如果输出看起来和原图一样,就是失败。`,
`任务:先在输入图片1中识别红色实线框附近最主要、可独立缩放的主体物体,再只对这个主体做局部比例调整,统一执行${actionLabel}。目标主体调整后的视觉尺寸必须约为原主体的 ${targetLabel},普通用户肉眼应能清楚看出主体尺寸已经变化;如果输出看起来和原图一样,就是失败。`,
regionLines,
"",
"关键要求:",
@ -935,7 +898,8 @@ export async function POST(request: NextRequest) {
? buildScaleAdjustPrompt(
regions,
scalePercent as ScaleAdjustPercent,
scaleAdjustCropReferences
scaleAdjustCropReferences,
{ width: sourceWidth, height: sourceHeight }
)
: buildLocalEditPrompt(regions);
const manualRegionReferenceImageUrls = regions.flatMap((region) =>
@ -1015,6 +979,9 @@ export async function POST(request: NextRequest) {
regionCount: regions.length,
scalePercent,
scaleAdjustCropReferenceCount: scaleAdjustCropReferenceImageUrls.length,
scaleAdjustPromptStrategy: isScaleAdjustOperation
? "locked-plate-local-subject-v2"
: undefined,
scaleAdjustUsesFullFrameOutput: isScaleAdjustOperation,
sourceHeight,
sourceNodeId: body.sourceNodeId || null,