From 75cfa4c00fac73bdd462f8584f41a50dd5c864a0 Mon Sep 17 00:00:00 2001 From: 134 Date: Tue, 23 Jun 2026 17:02:48 +0800 Subject: [PATCH] fix: drive scale adjust from target box guides --- app/api/canvas/local-edit/route.ts | 52 +++++---------- .../agents/AgentGenerationWorkspaceDarkV2.tsx | 65 ++++--------------- components/canvas/nodes/ImageNode.tsx | 44 +++---------- 3 files changed, 37 insertions(+), 124 deletions(-) diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index 9179ca7ba..a4e0d1b2c 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -309,20 +309,12 @@ function normalizeScaleAdjustPercent(input: unknown): ScaleAdjustPercent { throw new Error("请选择有效的比例调整幅度"); } -function getScaleAdjustLabel(scalePercent: ScaleAdjustPercent) { - return scalePercent > 0 ? `放大 ${scalePercent}%` : `缩小 ${Math.abs(scalePercent)}%`; -} - -function getScaleAdjustTargetLabel(scalePercent: ScaleAdjustPercent) { - return `${Math.round((1 + scalePercent / 100) * 100)}%`; -} - function buildLocalEditDisplayPrompt(params: { operation: LocalEditOperation; scalePercent: ScaleAdjustPercent | null; }) { - if (params.operation === SCALE_ADJUST_OPERATION && params.scalePercent !== null) { - return `比例调整:${getScaleAdjustLabel(params.scalePercent)}`; + if (params.operation === SCALE_ADJUST_OPERATION) { + return "比例调整:按目标框调整"; } return "局部编辑处理"; } @@ -704,7 +696,7 @@ function buildScaleAdjustAnnotationSvg(params: { const { width, height, regions, scalePercent } = params; const minSide = Math.max(1, Math.min(width, height)); const strokeWidth = Math.max(3, Math.round(minSide * 0.0048)); - const targetColor = scalePercent > 0 ? "#00d4ff" : "#00c853"; + const targetColor = "#00d4ff"; const sourceColor = "#ff3b30"; const dash = Math.max(12, Math.round(strokeWidth * 2.8)); const gap = Math.max(8, Math.round(strokeWidth * 1.8)); @@ -794,8 +786,6 @@ function buildScaleAdjustPrompt( 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完全相同的画幅和像素尺寸。"; @@ -809,26 +799,17 @@ function buildScaleAdjustPrompt( const sourceCenterY = region.rect.y + region.rect.height / 2; const targetCenterX = targetRect.x + targetRect.width / 2; const targetCenterY = targetRect.y + targetRect.height / 2; - 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 regionScalePercent = - deriveScaleAdjustPercentFromTargetRect(region.rect, targetRect) || - scalePercent; - const regionActionLabel = getScaleAdjustLabel(regionScalePercent); - const regionTargetLabel = getScaleAdjustTargetLabel(regionScalePercent); const moved = Math.hypot(targetCenterX - sourceCenterX, targetCenterY - sourceCenterY) > 0.01; const targetPositionNote = moved - ? `青色虚线框是用户指定的最终目标位置和目标大小,目标主体调整后的视觉中心应移动到青色虚线框中心附近;目标框坐标 x=${targetLeft}%,y=${targetTop}%,w=${targetWidth}%,h=${targetHeight}%。` - : `青色虚线框是用户指定的最终目标大小范围,目标框坐标 x=${targetLeft}%,y=${targetTop}%,w=${targetWidth}%,h=${targetHeight}%。`; + ? "青色虚线框同时指定用户希望的最终目标大小和目标位置,目标主体调整后的视觉中心应移动到青色虚线框中心附近。" + : "青色虚线框指定用户希望的最终目标大小,目标主体调整后应围绕当前主体位置自然放置。"; const cropNote = cropIndex >= 0 ? `;输入图片${cropIndex + 3}是该区域的干净局部裁切参考图,只用于识别目标主体的身份、轮廓与原始比例` : ""; - return `区域 ${region.index}${cropNote}:请在输入图片1中找到输入图片2红色实线框所标记范围内最主要、可独立缩放的商品/物体主体。红色实线框只是“寻找主体”的范围,不是放大后的硬边界,也不是要缩放或贴回的矩形补丁;放大时主体可以自然超出红色实线框,并由周边背景补全衔接。${targetPositionNote}目标虚线框不是要求主体填满矩形或变成矩形比例,请以主体自身轮廓、原始宽高比和视觉中心为准,执行整体等比${regionActionLabel},宽度与高度必须使用同一个缩放倍率,目标主体视觉尺寸约为原主体的 ${regionTargetLabel}。严禁宽度变窄但高度变高,严禁横向挤压、纵向拉伸或改变主体透视形体。如果框中只覆盖到主体的一部分,请根据连接关系推断该主体的完整可见结构,只调整该主体,不调整矩形框内背景。`; + return `区域 ${region.index}${cropNote}:请在输入图片1中找到输入图片2红色实线框所标记范围内最主要、可独立调整的商品/物体主体。红色实线框只是“寻找主体”的范围,不是最终边界,也不是要缩放或贴回的矩形补丁。${targetPositionNote}请直接读取输入图片2中青色虚线框相对红色实线框的真实大小和位置关系,并按这个框线关系调整主体;不要把框线换算成文字百分比来执行。目标虚线框不是要求主体填满矩形或变成矩形比例,请以主体自身轮廓、原始宽高比和视觉中心为准,整体等比调整主体大小和位置。严禁宽度变窄但高度变高,严禁横向挤压、纵向拉伸或改变主体透视形体。如果红框中只覆盖到主体的一部分,请根据连接关系推断该主体的完整可见结构,只调整该主体,不调整矩形框内背景。`; }) .join("\n"); const cropLines = cropReferences @@ -842,27 +823,28 @@ 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 控件或半透明色块。", + "不要把用户手画的目标框换算成百分比文字执行;必须直接以输入图片2里青色目标框线本身作为大小和位置参考。", "", - `任务:先在输入图片1中识别红色实线框范围内最主要、可独立缩放的主体物体,再只对这个“主体物体”做局部比例调整,统一执行${actionLabel}。目标主体调整后的视觉尺寸必须约为原主体的 ${targetLabel},普通用户肉眼应能清楚看出主体尺寸已经变化;如果输出看起来和原图一样,就是失败。`, + "任务:先在输入图片1中识别红色实线框范围内最主要、可独立调整的主体物体,再只对这个“主体物体”做局部比例调整。调整后的主体大小和位置必须以输入图片2中对应青色目标框线为准;如果输出看起来和原图一样,就是失败。", regionLines, "", "关键要求:", - `1. 只调整目标主体物体本身的整体视觉比例,必须等比缩放到约 ${targetLabel},不得保留原尺寸;主体轮廓宽度和高度必须使用同一个倍率同步变化,宽高比必须与原主体一致,不能为了贴合虚线框而拉宽、压窄、拉高或压扁。`, + "1. 只调整目标主体物体本身的整体视觉比例,不得保留原尺寸;主体轮廓宽度和高度必须同步等比变化,宽高比必须与原主体一致,不能为了贴合虚线框而拉宽、压窄、拉高或压扁。", "2. 严禁把整个矩形框选区域、输入图片1整张图或任意裁切图当成图片块整体缩放、平移、复制、裁切或贴回;最终画面不得出现矩形拼接边缘、色差硬边、补丁感、裁切线或局部贴片痕迹。", "2.1 严禁把输入图片1整张图整体放大、缩小、裁切或重新构图;如果画面中的标题、Logo、页面文字、边框、背景家具或非目标主体发生整体位移/缩放/消失,必须视为失败并重新生成。", - `3. 缩小时,目标主体应自然变小到约 ${targetLabel};如果青色目标框中心与红框中心不同,允许主体同步移动到青色目标框中心附近。主体缩小后,原来被遮挡的背景、桌面、墙面、阴影、纹理和透视必须被重新生成并无缝补全,选区内应出现明显新增留白或背景。`, - `4. 放大时,目标主体应自然变大到约 ${targetLabel};如果青色目标框中心与红框中心不同,允许主体同步移动到青色目标框中心附近。允许主体自然超出用户手画的原始红框,使用周围真实背景完成边缘、阴影和遮挡衔接。宽度和高度都要同步变大,不能出现“高度变高但宽度变窄”的非等比变形;主体边缘与遮挡关系要合理,不能产生突兀裁切。`, - "5. 如果主体靠近画布边界导致目标尺寸空间不足,请用能完整放入画面的最大统一倍率整体缩放;绝不能为了塞进画面而只压缩宽度或只拉伸高度。", + "3. 如果输入图片2中的青色目标框明显小于红色主体框,目标主体应自然变小,主体缩小后露出的背景、桌面、墙面、阴影、纹理和透视必须被重新生成并无缝补全,选区内应出现明显新增留白或背景。", + "4. 如果输入图片2中的青色目标框明显大于红色主体框,目标主体应自然变大;允许主体自然超出用户手画的原始红框,使用周围真实背景完成边缘、阴影和遮挡衔接。宽度和高度都要同步变大,不能出现“高度变高但宽度变窄”的非等比变形;主体边缘与遮挡关系要合理,不能产生突兀裁切。", + "5. 如果青色目标框中心与红色主体框中心不同,允许主体同步移动到青色目标框中心附近;如果主体靠近画布边界导致目标空间不足,请用能完整放入画面的最大统一倍率整体调整,绝不能为了塞进画面而只压缩宽度或只拉伸高度。", "6. 框选区域内非目标主体的背景、墙面、桌面、植物、文字和其它物体应保持原场景逻辑,不要跟着主体一起缩放。", "6.1 比例调整后的主体边缘必须和周围环境自然融合,选区边界不能被肉眼看出;不得出现一整块矩形区域清晰度、颜色、光照、纹理或透视突然变化。", "6.2 框选区域通常会包含主体周围的背景留白,背景留白不是目标主体。请锁定这些非主体背景的窗框、文字、地毯纹理、墙面、桌面边缘和透视线,只重建主体缩放后被遮挡或露出的极小必要区域;不要把框选矩形当作整块背景重新生成。", @@ -926,10 +908,8 @@ export async function POST(request: NextRequest) { try { regions = normalizeRegions(body.regions, sourceWidth, sourceHeight, { fallbackPrompt: - operation === SCALE_ADJUST_OPERATION && scalePercent !== null - ? `比例调整:${getScaleAdjustLabel(scalePercent)}` - : operation === SCALE_ADJUST_OPERATION - ? "比例调整" + operation === SCALE_ADJUST_OPERATION + ? "比例调整:按目标框调整" : undefined, requirePrompt: operation !== SCALE_ADJUST_OPERATION, }); diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index ae202e977..e586941a7 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -246,7 +246,6 @@ type DetailResultLocalEditTarget = { }; type DetailResultLocalEditMode = "local_edit" | "scale_adjust"; -type DetailResultScalePercent = number; type DetailResultLocalEditRect = { x: number; @@ -271,7 +270,6 @@ type DetailResultLocalEditRegionInteraction = { type DetailResultLocalEditSubmitParams = { imageUrl: string; mode: DetailResultLocalEditMode; - scalePercent?: DetailResultScalePercent; title: string; regions: LocalEditRegion[]; }; @@ -283,7 +281,6 @@ type DetailResultLocalEditPreviewResult = { resultImageUrl: string; mode: DetailResultLocalEditMode; title: string; - scalePercent?: DetailResultScalePercent; }; const MAX_CHAT_TEXT_PART_LENGTH = 2000; @@ -841,31 +838,17 @@ async function uploadDetailLocalEditReferenceImage( }; } -function getDetailScaleAdjustLabel(scalePercent: DetailResultScalePercent) { - return scalePercent > 0 ? `放大 ${scalePercent}%` : `缩小 ${Math.abs(scalePercent)}%`; -} - -function getDetailScaleAdjustTargetLabel(scalePercent: DetailResultScalePercent) { - return `${Math.max(5, Math.round((1 + scalePercent / 100) * 100))}%`; -} - function getDetailScaleAdjustRegionPrompt( - scalePercent: DetailResultScalePercent | null, + _scalePercent: number | null, hasTargetPosition = false ) { - if (!scalePercent) return "比例调整"; - const actionLabel = getDetailScaleAdjustLabel(scalePercent); - const targetLabel = getDetailScaleAdjustTargetLabel(scalePercent); - const scaleVerb = scalePercent > 0 ? "放大" : "缩小"; return [ - `对框选范围内的主物体执行等比${actionLabel},调整后主体视觉尺寸必须约为原来的 ${targetLabel},需要肉眼明显看出已经${scaleVerb}。`, + "按目标框线对框选范围内的主物体执行等比比例调整。", hasTargetPosition - ? "主体框只用于识别要调整的主体;青色目标框表示调整后的目标大小和目标位置,放大后的主体可以自然超出主体框,并由周边背景补全衔接。" - : "框选区域只用于识别要调整的主体,不是放大后的边界;放大时主体可以自然超出原始框选范围,并由周边背景补全衔接。", + ? "主体框只用于识别要调整的主体;青色目标框表示调整后的目标大小和目标位置,主体可以自然超出主体框,并由周边背景补全衔接。" + : "框选区域只用于识别要调整的主体,不是调整后的边界;主体可以自然超出原始框选范围,并由周边背景补全衔接。", "只改变目标主体的尺寸,不改变主体外观、颜色、材质、图案和文字内容;不要把整个框选矩形当成图片块缩放。", - scalePercent < 0 - ? "主体缩小后露出的背景、桌面、墙面、阴影和遮挡关系必须自然补全。" - : "主体放大后边缘、遮挡和透视必须自然衔接。", + "目标主体调整后露出或覆盖的背景、桌面、墙面、阴影和遮挡关系必须自然补全。", ].join(" "); } @@ -1019,10 +1002,7 @@ function DetailResultLocalEditDialog({ ? { ...region, targetRect: nextRect, - prompt: getDetailScaleAdjustRegionPrompt( - deriveDetailScalePercentFromRects(region.rect, nextRect), - true - ), + prompt: getDetailScaleAdjustRegionPrompt(null, true), } : { ...region, @@ -1030,7 +1010,7 @@ function DetailResultLocalEditDialog({ prompt: isScaleAdjustMode && region.targetRect ? getDetailScaleAdjustRegionPrompt( - deriveDetailScalePercentFromRects(nextRect, region.targetRect), + null, true ) : region.prompt, @@ -1249,13 +1229,6 @@ function DetailResultLocalEditDialog({ onSubmit({ imageUrl: target.imageUrl, mode, - scalePercent: - isScaleAdjustMode && validRegions[0] - ? deriveDetailScalePercentFromRects( - validRegions[0].rect, - validRegions[0].targetRect - ) || undefined - : undefined, title: target.title, regions: validRegions, }); @@ -1293,10 +1266,7 @@ function DetailResultLocalEditDialog({ ? { ...region, targetRect: nextRect, - prompt: getDetailScaleAdjustRegionPrompt( - deriveDetailScalePercentFromRects(region.rect, nextRect), - true - ), + prompt: getDetailScaleAdjustRegionPrompt(null, true), } : region ) @@ -1730,10 +1700,6 @@ function DetailResultLocalEditDialog({ .filter((region) => region.targetRect) .map((region) => { const targetRect = region.targetRect as DetailResultLocalEditRect; - const scalePercent = deriveDetailScalePercentFromRects( - region.rect, - targetRect - ); const isActive = region.id === activeRegionId; return (