From ce4e056f20a7af6b87d0fff6ba78368c31ef83aa Mon Sep 17 00:00:00 2001 From: 134 Date: Fri, 3 Jul 2026 18:12:01 +0800 Subject: [PATCH] fix: improve structure locked scale drafts --- app/api/canvas/local-edit/route.ts | 100 ++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 8 deletions(-) diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index b3f87dbd7..371914965 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -1149,6 +1149,51 @@ function isScaleAdjustLikelyWhiteFabricPixel( ); } +function isScaleAdjustPersonLikePrompt(prompt: string) { + const text = normalizeScaleAdjustModelText(prompt); + return /person|people|human|woman|man|girl|boy|model|face|body|hand|arm|leg|clothing|人物|人像|真人|模特|女人|男人|女孩|男孩|小孩|脸|身体|手|胳膊|腿|衣服/.test( + text + ); +} + +function hasScaleAdjustPersonPixelSignal(params: { + box: { left: number; top: number; width: number; height: number }; + raw: Buffer; + width: number; +}) { + const { box, raw, width } = params; + let sampled = 0; + let skin = 0; + let whiteFabric = 0; + const step = Math.max(1, Math.floor(Math.min(box.width, box.height) / 260)); + + for (let localY = 0; localY < box.height; localY += step) { + const y = box.top + localY; + for (let localX = 0; localX < box.width; localX += step) { + const x = box.left + localX; + const rawIndex = getScaleAdjustRawPixelIndex(width, x, y); + const r = raw[rawIndex] || 0; + const g = raw[rawIndex + 1] || 0; + const b = raw[rawIndex + 2] || 0; + const { luma, saturation } = getScaleAdjustPixelStats(r, g, b); + sampled += 1; + if (isScaleAdjustLikelySkinPixel(r, g, b)) { + skin += 1; + } else if ( + isScaleAdjustLikelyWhiteFabricPixel(r, g, b, luma, saturation) && + luma < 242 + ) { + whiteFabric += 1; + } + } + } + + if (sampled <= 0) return false; + const skinRatio = skin / sampled; + const fabricRatio = whiteFabric / sampled; + return skin >= 18 && skinRatio >= 0.0025 && fabricRatio >= 0.035; +} + function estimateScaleAdjustBoxBackground(params: { box: { left: number; top: number; width: number; height: number }; raw: Buffer; @@ -1230,10 +1275,11 @@ function dilateScaleAdjustStructureMask(params: { function buildScaleAdjustStructureInitialMask(params: { box: { left: number; top: number; width: number; height: number }; + includePersonPixels?: boolean; raw: Buffer; width: number; }) { - const { box, raw, width } = params; + const { box, includePersonPixels = false, raw, width } = params; const background = estimateScaleAdjustBoxBackground(params); const mask = new Uint8Array(box.width * box.height); @@ -1247,16 +1293,38 @@ function buildScaleAdjustStructureInitialMask(params: { const g = raw[rawIndex + 1] || 0; const b = raw[rawIndex + 2] || 0; const { luma, saturation } = getScaleAdjustPixelStats(r, g, b); - if (isScaleAdjustLikelySkinPixel(r, g, b)) continue; - if (isScaleAdjustLikelyWhiteFabricPixel(r, g, b, luma, saturation)) { - continue; - } - + const likelySkin = isScaleAdjustLikelySkinPixel(r, g, b); + const likelyWhiteFabric = isScaleAdjustLikelyWhiteFabricPixel( + r, + g, + b, + luma, + saturation + ); const colorDistance = Math.sqrt( (r - background.r) ** 2 + (g - background.g) ** 2 + (b - background.b) ** 2 ); + + if (includePersonPixels && likelySkin) { + mask[localY * box.width + localX] = 255; + continue; + } + if ( + includePersonPixels && + likelyWhiteFabric && + luma < 246 && + (colorDistance > 12 || luma < background.luma - 4) + ) { + mask[localY * box.width + localX] = 255; + continue; + } + if (likelySkin) continue; + if (likelyWhiteFabric) { + continue; + } + const darkStructure = luma < Math.min(136, background.luma - 38); const saturatedStructure = saturation > 0.16 && luma < 218 && colorDistance > 38; @@ -1596,14 +1664,22 @@ async function buildScaleAdjustMaskWashPatch(params: { } async function buildScaleAdjustStructureLockedComposite(params: { + includePersonPixels?: boolean; placement: ReturnType; raw: Buffer; sourceBuffer: Buffer; sourceWidth: number; }) { - const { placement, raw, sourceBuffer, sourceWidth } = params; + const { + includePersonPixels = false, + placement, + raw, + sourceBuffer, + sourceWidth, + } = params; const initialMask = buildScaleAdjustStructureInitialMask({ box: placement.sourceBox, + includePersonPixels, raw, width: sourceWidth, }); @@ -1860,6 +1936,13 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: { (targetWidthFactor > 1.08 || targetHeightFactor > 1.08); const structureLockedComposite = shouldUseStructureLock ? await buildScaleAdjustStructureLockedComposite({ + includePersonPixels: + isScaleAdjustPersonLikePrompt(region.prompt) || + hasScaleAdjustPersonPixelSignal({ + box: placement.sourceBox, + raw: sourceRaw, + width: sourceWidth, + }), placement, raw: sourceRaw, sourceBuffer, @@ -3154,7 +3237,8 @@ export async function POST(request: NextRequest) { : []; const useScaleAdjustPatchTargetDraftPrimary = Boolean(scaleAdjustPatchReference); const includeScaleAdjustPatchSourceReference = - scaleAdjustPatchReference?.structureLockedDraft === true; + Boolean(scaleAdjustPatchReference) && + scaleAdjustPatchReference?.structureLockedDraft !== true; const prompt = isScaleAdjustOperation ? scaleAdjustPatchReference ? buildScaleAdjustPatchPrompt(