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(` - - - - - ${regionSvg} `); @@ -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,