diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts
index 3ef7d7b09..ea3f26604 100644
--- a/app/api/canvas/local-edit/route.ts
+++ b/app/api/canvas/local-edit/route.ts
@@ -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 ``;
- })
- .join("");
+ const fontSize = Math.max(13, Math.round(badgeSize * 0.52));
return `
-
-
- ${arrows}
-
-
- ${region.index}
-
- 目标:${escapeXml(actionLabel)},主体约 ${escapeXml(targetLabel)}
- 按主体原始宽高比等比变化,禁止拉伸变形
+
+
+
+ ${region.index}
`;
})
.join("");
return Buffer.from(`
`);
@@ -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,