fix: guide scale adjust with target annotations

This commit is contained in:
134 2026-06-23 22:36:38 +08:00
parent 23b6855b4b
commit 3524a4ea16
2 changed files with 67 additions and 21 deletions

View File

@ -750,8 +750,11 @@ function buildScaleAdjustAnnotationSvg(params: {
const strokeWidth = Math.max(3, Math.round(minSide * 0.0048));
const targetColor = "#00d4ff";
const sourceColor = "#ff3b30";
const connectorColor = "#14c8ff";
const dash = Math.max(12, Math.round(strokeWidth * 2.8));
const gap = Math.max(8, Math.round(strokeWidth * 1.8));
const connectorWidth = Math.max(2, Math.round(strokeWidth * 0.74));
const markerSize = Math.max(8, Math.round(strokeWidth * 3.1));
const regionSvg = regions
.map((region) => {
@ -772,10 +775,58 @@ function buildScaleAdjustAnnotationSvg(params: {
height - badgeSize / 2 - 4
);
const fontSize = Math.max(13, Math.round(badgeSize * 0.52));
const sourceRight = source.left + source.width;
const sourceBottom = source.top + source.height;
const targetRight = target.left + target.width;
const targetBottom = target.top + target.height;
const sourceCorners = [
{ x: source.left, y: source.top },
{ x: sourceRight, y: source.top },
{ x: sourceRight, y: sourceBottom },
{ x: source.left, y: sourceBottom },
];
const targetCorners = [
{ x: target.left, y: target.top },
{ x: targetRight, y: target.top },
{ x: targetRight, y: targetBottom },
{ x: target.left, y: targetBottom },
];
const connectorSvg = sourceCorners
.map((corner, cornerIndex) => {
const targetCorner = targetCorners[cornerIndex];
const distance = Math.hypot(targetCorner.x - corner.x, targetCorner.y - corner.y);
if (distance < Math.max(8, strokeWidth * 2.2)) return "";
const dx = (targetCorner.x - corner.x) / distance;
const dy = (targetCorner.y - corner.y) / distance;
const startInset = Math.min(distance * 0.16, Math.max(5, strokeWidth * 1.8));
const endInset = Math.min(distance * 0.18, Math.max(8, strokeWidth * 2.8));
const x1 = corner.x + dx * startInset;
const y1 = corner.y + dy * startInset;
const x2 = targetCorner.x - dx * endInset;
const y2 = targetCorner.y - dy * endInset;
return `<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${connectorColor}" stroke-width="${connectorWidth}" stroke-linecap="round" opacity="0.86" marker-end="url(#scale-adjust-arrow)" />`;
})
.join("");
const cornerHandleSize = Math.max(7, Math.round(strokeWidth * 2.2));
const sourceCornerSvg = sourceCorners
.map(
(corner) =>
`<circle cx="${corner.x}" cy="${corner.y}" r="${cornerHandleSize * 0.48}" fill="#ffffff" stroke="${sourceColor}" stroke-width="${Math.max(2, Math.round(strokeWidth * 0.7))}" />`
)
.join("");
const targetCornerSvg = targetCorners
.map(
(corner) =>
`<rect x="${corner.x - cornerHandleSize / 2}" y="${corner.y - cornerHandleSize / 2}" width="${cornerHandleSize}" height="${cornerHandleSize}" rx="${Math.max(1, Math.round(cornerHandleSize * 0.18))}" fill="#ffffff" stroke="${targetColor}" stroke-width="${Math.max(2, Math.round(strokeWidth * 0.7))}" />`
)
.join("");
return `
${connectorSvg}
<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}" />
${sourceCornerSvg}
${targetCornerSvg}
<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>
`;
@ -784,6 +835,11 @@ function buildScaleAdjustAnnotationSvg(params: {
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" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="${markerSize}" markerHeight="${markerSize}" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="${connectorColor}" opacity="0.92" />
</marker>
</defs>
${regionSvg}
</svg>
`);
@ -861,7 +917,7 @@ function buildScaleAdjustPrompt(
cropIndex >= 0
? `;输入图片${cropIndex + 3}是该区域的干净局部裁切参考图,只用于识别目标主体的身份、轮廓与原始比例`
: "";
return `区域 ${region.index}${cropNote}:请在输入图片1中找到输入图片2红色实线框所标记范围内最主要、可独立调整的商品/物体主体。红色实线框只是“寻找主体”的范围,不是最终边界,也不是要缩放或贴回的矩形补丁。${targetPositionNote}请直接读取输入图片2中青色虚线框相对红色实线框的真实大小和位置关系,并按这个框线关系调整主体;不要把框线换算成文字百分比来执行。目标虚线框不是要求主体填满矩形或变成矩形比例,请以主体自身轮廓、原始宽高比和视觉中心为准,整体等比调整主体大小和位置。严禁宽度变窄但高度变高,严禁横向挤压、纵向拉伸或改变主体透视形体。如果红框中只覆盖到主体的一部分,请根据连接关系推断该主体的完整可见结构,只调整该主体,不调整矩形框内背景。`;
return `区域 ${region.index}${cropNote}:请在输入图片1中找到输入图片2红色实线框所标记范围内最主要、可独立调整的商品/物体主体。红色实线框只是“寻找主体”的范围,不是最终边界,也不是要缩放或贴回的矩形补丁。${targetPositionNote}请直接读取输入图片2中青色虚线框相对红色实线框的真实大小和位置关系,并按这个框线关系调整主体;四条蓝色箭头从红框四角指向青框四角,表示主体外接轮廓从当前位置迁移到目标大小和目标位置的方向。不要把框线换算成文字百分比来执行。目标虚线框不是要求主体填满矩形或变成矩形比例,请以主体自身轮廓、原始宽高比和视觉中心为准,整体等比调整主体大小和位置。严禁宽度变窄但高度变高,严禁横向挤压、纵向拉伸或改变主体透视形体。如果红框中只覆盖到主体的一部分,请根据连接关系推断该主体的完整可见结构,只调整该主体,不调整矩形框内背景。`;
})
.join("\n");
const cropLines = cropReferences
@ -875,16 +931,16 @@ function buildScaleAdjustPrompt(
"这是一项局部主体比例 inpaint/edit 任务,不是整图缩放、整图裁切或重新构图任务。",
sourceSizeLine,
"输入图片1是原始干净图,也是最终输出必须继承的唯一画面基底。除目标主体尺寸变化与必要背景补全外,输入图片1中的文字、Logo、装饰、排版、背景、光线、构图边界和所有未选区域必须保持不变。",
"输入图片2是白底选区参考图,只用于说明输入图片1上的红色实线框和青色目标虚线框位置;它不包含场景内容,不是输出底图,不得学习或复刻它的框线、编号、半透明遮罩或 UI 标记。",
"输入图片2是白底选区参考图,只用于说明输入图片1上的红色实线框、青色目标虚线框和四角箭头关系;它不包含场景内容,不是输出底图,不得学习或复刻它的框线、箭头、编号、角点、半透明遮罩或 UI 标记。",
cropLines
? `输入图片3及之后是框选区域的干净局部裁切参考图,作用是帮助你确认“要缩放的是哪个主体”。这些裁切图不是输出底图,不得被整体放大、贴回或覆盖到原图上。\n${cropLines}`
: "",
"输入图片2中的红色实线框表示输入图片1里目标主体当前大致位置;它只用于定位主体,不限制调整后的空间。青色虚线框表示用户手画的比例调整后目标大小和目标位置。最终画面不得出现这些框线,主体也不需要填满虚线矩形;必须保持主体自身原始宽高比和透视形体。",
"输入图片2中的红色实线框表示输入图片1里目标主体当前大致位置;它只用于定位主体,不限制调整后的空间。青色虚线框表示用户手画的比例调整后目标大小和目标位置。四条蓝色箭头从红框四角指向青框四角:箭头向外表示放大,箭头向内表示缩小,箭头整体偏移表示移动。最终画面不得出现这些框线、箭头或角点,主体也不需要填满虚线矩形;必须保持主体自身原始宽高比和透视形体。",
"如果系统或模型只能读取一张参考图,必须优先以输入图片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 控件或半透明色块。",
"Highest priority rule: do NOT zoom, crop, resize, reframe, pan, or regenerate the full canvas. Treat image 1 as a locked background plate; edit only the selected subject and the smallest necessary background around its old and new footprint.",
"请不要生成任何彩色框、矩形边框、箭头、连线、角点、编号圆点、提示词气泡、标注文字、UI 控件或半透明色块。",
"不要把用户手画的目标框换算成百分比文字执行;必须直接以输入图片2里青色目标框线本身作为大小和位置参考。",
"",
"任务:先在输入图片1中识别红色实线框范围内最主要、可独立调整的主体物体,再只对这个“主体物体”做局部比例调整。调整后的主体大小和位置必须以输入图片2中对应青色目标框线为准;如果输出看起来和原图一样,就是失败。",
@ -1138,7 +1194,7 @@ export async function POST(request: NextRequest) {
annotatedReferenceUrl: annotatedUpload.url,
annotatedReferenceSentToModel: isScaleAdjustOperation,
annotatedReferenceType: isScaleAdjustOperation
? "white-selection-guide"
? "white-source-target-arrow-guide"
: "source-overlay",
cleanSourceReferenceIndex: 1,
configuredDetailModelId: configuredDetailModel || null,
@ -1162,8 +1218,9 @@ export async function POST(request: NextRequest) {
scaleAdjustTargetRegionCount: regions.filter((region) => region.targetRect)
.length,
scaleAdjustPromptStrategy: isScaleAdjustOperation
? "locked-plate-local-subject-target-box-v4"
? "locked-plate-local-subject-target-box-arrows-v5"
: undefined,
scaleAdjustPostCompositeApplied: false,
scaleAdjustUsesFullFrameOutput: isScaleAdjustOperation,
sourceHeight,
sourceNodeId: body.sourceNodeId || null,
@ -1178,7 +1235,7 @@ export async function POST(request: NextRequest) {
model: resolvedModel || undefined,
negativePrompt:
isScaleAdjustOperation
? "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框、白边、画幅压扁、画面拉伸、整张图缩放、全图放大、全图裁切、全图平移、整体重绘、删除文字、清理文字、改写文字、文字模糊、Logo消失、主体拉伸、主体压扁、主体变窄、主体变高但变窄、横向挤压、纵向拉伸、透视变形、矩形贴片、硬边、拼接线、补丁感、局部裁切、色差边缘、背景断裂、背景错位、窗框错位、文字错位、地毯纹理错位、非主体背景重绘、主体未缩放、整块框选区域缩放"
? "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、箭头、连线、角点、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框、白边、画幅压扁、画面拉伸、整张图缩放、全图放大、全图裁切、全图平移、整体重绘、删除文字、清理文字、改写文字、文字模糊、Logo消失、主体拉伸、主体压扁、主体变窄、主体变高但变窄、横向挤压、纵向拉伸、透视变形、矩形贴片、硬边、拼接线、补丁感、局部裁切、色差边缘、背景断裂、背景错位、窗框错位、文字错位、地毯纹理错位、非主体背景重绘、主体未缩放、整块框选区域缩放"
: "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框、白边、画幅压扁、画面拉伸、主体截断、动物截断、缺头、缺尾巴、缺四肢、贴边裁切、矩形贴片、硬边、拼接线、补丁感、色差边缘、背景错位、地面纹理断裂、椅脚错位",
prompt,
promptExtend: false,
@ -1206,18 +1263,7 @@ export async function POST(request: NextRequest) {
}
: undefined,
localEditComposite: isScaleAdjustOperation
? {
sourceImageUrl,
regions: scaleAdjustCompositeRegions,
expandRegions: false,
maskMode: "diff",
featherMinPx: 4,
featherMaxPx: 18,
featherRatio: 0.006,
diffLowThreshold: 18,
diffHighThreshold: 54,
diffCompareBlurPx: 2.4,
}
? undefined
: {
sourceImageUrl,
regions: regions.map((region) => region.rect),

View File

@ -1974,6 +1974,7 @@ export async function generateCanvasImage(
selectedModelId: selectedModel.model.modelId,
error: caughtError instanceof Error ? caughtError.message : String(caughtError),
});
throw caughtError;
} else {
await recordAiModelHealthFailure({
model: selectedModel.model,
@ -2012,7 +2013,6 @@ export async function generateCanvasImage(
const allowFailover = request.allowFailover !== false;
const shouldFailover =
shouldFailoverAiModelError(caughtError) ||
scaleAdjustRejected ||
(Boolean(debugMeta?.detailFixedModelApplied) && Boolean(caughtError));
const fallbackModel = allowFailover && shouldFailover
? findCanvasFailoverModel({