fix: honor target boxes in scale adjust guard

This commit is contained in:
134 2026-06-23 19:47:11 +08:00
parent 4d65ba0b79
commit 3760f7ff44
2 changed files with 122 additions and 68 deletions

View File

@ -398,35 +398,13 @@ function scaleRectAroundCenter(
function getScaleAdjustTargetRect( function getScaleAdjustTargetRect(
region: NormalizedLocalEditRegion, region: NormalizedLocalEditRegion,
scalePercent: ScaleAdjustPercent scalePercent?: ScaleAdjustPercent | null
) { ) {
return region.targetRect || scaleRectAroundCenter(region.rect, scalePercent); if (region.targetRect) return region.targetRect;
} if (typeof scalePercent === "number" && Number.isFinite(scalePercent)) {
return scaleRectAroundCenter(region.rect, scalePercent);
function deriveScaleAdjustPercentFromTargetRect(
sourceRect: NormalizedLocalEditRegion["rect"],
targetRect: NormalizedLocalEditRegion["rect"] | null | undefined
) {
if (!targetRect || sourceRect.width <= 0 || sourceRect.height <= 0) {
return null;
} }
const widthRatio = Math.max(0.05, targetRect.width / sourceRect.width); return region.rect;
const heightRatio = Math.max(0.05, targetRect.height / sourceRect.height);
const uniformRatio = Math.sqrt(widthRatio * heightRatio);
const percent = Math.round((uniformRatio - 1) * 100);
if (!Number.isFinite(percent) || percent === 0) return null;
return clamp(percent, MIN_SCALE_ADJUST_PERCENT, MAX_SCALE_ADJUST_PERCENT);
}
function deriveScaleAdjustPercentFromRegions(regions: NormalizedLocalEditRegion[]) {
for (const region of regions) {
const percent = deriveScaleAdjustPercentFromTargetRect(
region.rect,
region.targetRect
);
if (percent !== null) return percent;
}
return null;
} }
function expandRectWithMargins( function expandRectWithMargins(
@ -450,11 +428,31 @@ function expandRectWithMargins(
function getScaleAdjustWorkspaceRect( function getScaleAdjustWorkspaceRect(
sourceRect: NormalizedLocalEditRegion["rect"], sourceRect: NormalizedLocalEditRegion["rect"],
targetRect: NormalizedLocalEditRegion["rect"], targetRect: NormalizedLocalEditRegion["rect"],
scalePercent: ScaleAdjustPercent scalePercent?: ScaleAdjustPercent | null
) { ) {
if (scalePercent <= 0) return targetRect; const sourceWidth = Math.max(0.0001, sourceRect.width);
const sourceHeight = Math.max(0.0001, sourceRect.height);
const widthGrowthRatio = Math.max(0, targetRect.width / sourceWidth - 1);
const heightGrowthRatio = Math.max(0, targetRect.height / sourceHeight - 1);
const areaGrowthRatio = Math.max(
0,
Math.sqrt(
Math.max(0.0001, targetRect.width * targetRect.height) /
Math.max(0.0001, sourceWidth * sourceHeight)
) - 1
);
const explicitGrowthRatio = Math.max(
widthGrowthRatio,
heightGrowthRatio,
areaGrowthRatio
);
const fallbackScaleRatio =
typeof scalePercent === "number" && Number.isFinite(scalePercent)
? Math.min(1, Math.max(0, scalePercent / 100))
: 0;
const scaleRatio = Math.max(explicitGrowthRatio, fallbackScaleRatio);
if (scaleRatio <= 0) return targetRect;
const scaleRatio = Math.min(1, Math.max(0, scalePercent / 100));
const marginX = Math.min( const marginX = Math.min(
0.12, 0.12,
Math.max(0.018, sourceRect.width * scaleRatio * 0.22) Math.max(0.018, sourceRect.width * scaleRatio * 0.22)
@ -469,7 +467,7 @@ function getScaleAdjustWorkspaceRect(
function buildScaleAdjustCompositeRegions( function buildScaleAdjustCompositeRegions(
regions: NormalizedLocalEditRegion[], regions: NormalizedLocalEditRegion[],
scalePercent: ScaleAdjustPercent scalePercent?: ScaleAdjustPercent | null
) { ) {
return regions.flatMap((region) => { return regions.flatMap((region) => {
const targetRect = getScaleAdjustTargetRect(region, scalePercent); const targetRect = getScaleAdjustTargetRect(region, scalePercent);
@ -478,7 +476,8 @@ function buildScaleAdjustCompositeRegions(
targetRect, targetRect,
scalePercent scalePercent
); );
return scalePercent > 0 return workspaceRect.width > targetRect.width ||
workspaceRect.height > targetRect.height
? [region.rect, targetRect, workspaceRect] ? [region.rect, targetRect, workspaceRect]
: [region.rect, targetRect]; : [region.rect, targetRect];
}); });
@ -691,7 +690,7 @@ function buildScaleAdjustAnnotationSvg(params: {
width: number; width: number;
height: number; height: number;
regions: NormalizedLocalEditRegion[]; regions: NormalizedLocalEditRegion[];
scalePercent: ScaleAdjustPercent; scalePercent?: ScaleAdjustPercent | null;
}) { }) {
const { width, height, regions, scalePercent } = params; const { width, height, regions, scalePercent } = params;
const minSide = Math.max(1, Math.min(width, height)); const minSide = Math.max(1, Math.min(width, height));
@ -782,7 +781,7 @@ function buildLocalEditPrompt(regions: NormalizedLocalEditRegion[]) {
function buildScaleAdjustPrompt( function buildScaleAdjustPrompt(
regions: NormalizedLocalEditRegion[], regions: NormalizedLocalEditRegion[],
scalePercent: ScaleAdjustPercent, scalePercent: ScaleAdjustPercent | null | undefined,
cropReferences: ScaleAdjustCropReference[] = [], cropReferences: ScaleAdjustCropReference[] = [],
sourceSize?: { width: number; height: number } sourceSize?: { width: number; height: number }
) { ) {
@ -925,21 +924,27 @@ export async function POST(request: NextRequest) {
{ status: 400 } { status: 400 }
); );
} }
if (operation === SCALE_ADJUST_OPERATION && scalePercent === null) { const hasScaleAdjustTargetRects =
scalePercent = deriveScaleAdjustPercentFromRegions(regions); operation === SCALE_ADJUST_OPERATION &&
if (scalePercent === null) { regions.length > 0 &&
return NextResponse.json( regions.every((region) => Boolean(region.targetRect));
{ if (
success: false, operation === SCALE_ADJUST_OPERATION &&
error: "请提供比例调整幅度,或为每个区域提供有效的目标框 targetRect", !hasScaleAdjustTargetRects &&
}, scalePercent === null
{ status: 400 } ) {
); return NextResponse.json(
} {
success: false,
error: "请为每个区域提供有效的目标框 targetRect,或提供比例调整幅度 scalePercent",
},
{ status: 400 }
);
} }
const shouldUseScaleAdjustDetailModel = const shouldUseScaleAdjustDetailModel =
operation === SCALE_ADJUST_OPERATION && scalePercent !== null; operation === SCALE_ADJUST_OPERATION &&
(hasScaleAdjustTargetRects || scalePercent !== null);
const [pointsConfig, siteSettingsForModel, scaleAdjustModelCandidates] = const [pointsConfig, siteSettingsForModel, scaleAdjustModelCandidates] =
await Promise.all([ await Promise.all([
getPointsSystemConfig(), getPointsSystemConfig(),
@ -968,14 +973,15 @@ export async function POST(request: NextRequest) {
} }
const isScaleAdjustOperation = const isScaleAdjustOperation =
operation === SCALE_ADJUST_OPERATION && scalePercent !== null; operation === SCALE_ADJUST_OPERATION &&
(hasScaleAdjustTargetRects || scalePercent !== null);
const displayPrompt = buildLocalEditDisplayPrompt({ operation, scalePercent }); const displayPrompt = buildLocalEditDisplayPrompt({ operation, scalePercent });
const annotationSvg = isScaleAdjustOperation const annotationSvg = isScaleAdjustOperation
? buildScaleAdjustAnnotationSvg({ ? buildScaleAdjustAnnotationSvg({
height: sourceHeight, height: sourceHeight,
regions, regions,
scalePercent: scalePercent as ScaleAdjustPercent, scalePercent,
width: sourceWidth, width: sourceWidth,
}) })
: buildAnnotationSvg({ : buildAnnotationSvg({
@ -1015,7 +1021,7 @@ export async function POST(request: NextRequest) {
isScaleAdjustOperation isScaleAdjustOperation
? buildScaleAdjustPrompt( ? buildScaleAdjustPrompt(
regions, regions,
scalePercent as ScaleAdjustPercent, scalePercent,
scaleAdjustCropReferences, scaleAdjustCropReferences,
{ width: sourceWidth, height: sourceHeight } { width: sourceWidth, height: sourceHeight }
) )
@ -1067,10 +1073,7 @@ export async function POST(request: NextRequest) {
} }
const resolvedModel = detailFixedModel || requestedModel; const resolvedModel = detailFixedModel || requestedModel;
const scaleAdjustCompositeRegions = isScaleAdjustOperation const scaleAdjustCompositeRegions = isScaleAdjustOperation
? buildScaleAdjustCompositeRegions( ? buildScaleAdjustCompositeRegions(regions, scalePercent)
regions,
scalePercent as ScaleAdjustPercent
)
: []; : [];
const imageRequest: CanvasGenerateImageRequest = { const imageRequest: CanvasGenerateImageRequest = {
@ -1100,6 +1103,9 @@ export async function POST(request: NextRequest) {
regionCount: regions.length, regionCount: regions.length,
scalePercent, scalePercent,
scaleAdjustCropReferenceCount: scaleAdjustCropReferenceImageUrls.length, scaleAdjustCropReferenceCount: scaleAdjustCropReferenceImageUrls.length,
scaleAdjustProtectionMode: hasScaleAdjustTargetRects
? "target-rect"
: "scale-percent",
scaleAdjustTargetRegionCount: regions.filter((region) => region.targetRect) scaleAdjustTargetRegionCount: regions.filter((region) => region.targetRect)
.length, .length,
scaleAdjustPromptStrategy: isScaleAdjustOperation scaleAdjustPromptStrategy: isScaleAdjustOperation
@ -1129,7 +1135,21 @@ export async function POST(request: NextRequest) {
protectedRegions: scaleAdjustCompositeRegions, protectedRegions: scaleAdjustCompositeRegions,
sourceImageUrl, sourceImageUrl,
regions: regions.map((region) => region.rect), regions: regions.map((region) => region.rect),
scalePercent: scalePercent as ScaleAdjustPercent, ...(typeof scalePercent === "number"
? { scalePercent: scalePercent as ScaleAdjustPercent }
: {}),
...(hasScaleAdjustTargetRects
? {
targetRegions: regions
.map((region) => region.targetRect)
.filter(
(
rect
): rect is NonNullable<NormalizedLocalEditRegion["targetRect"]> =>
Boolean(rect)
),
}
: {}),
} }
: undefined, : undefined,
localEditComposite: isScaleAdjustOperation localEditComposite: isScaleAdjustOperation

View File

@ -54,7 +54,8 @@ export interface CanvasScaleAdjustVerificationConfig {
protectedRegions?: CanvasLocalEditCompositeRegion[]; protectedRegions?: CanvasLocalEditCompositeRegion[];
sourceImageUrl: string; sourceImageUrl: string;
regions: CanvasLocalEditCompositeRegion[]; regions: CanvasLocalEditCompositeRegion[];
scalePercent: number; targetRegions?: CanvasLocalEditCompositeRegion[];
scalePercent?: number;
} }
export interface CanvasGenerateImageRequest { export interface CanvasGenerateImageRequest {
@ -331,13 +332,23 @@ function normalizeScaleAdjustVerificationConfig(
if (!value || typeof value !== "object") return null; if (!value || typeof value !== "object") return null;
const sourceImageUrl = String(value.sourceImageUrl || "").trim(); const sourceImageUrl = String(value.sourceImageUrl || "").trim();
if (!/^https?:\/\//i.test(sourceImageUrl)) return null; if (!/^https?:\/\//i.test(sourceImageUrl)) return null;
const scalePercent = Number(value.scalePercent);
if (!Number.isFinite(scalePercent) || Math.abs(scalePercent) < 1) return null;
if (!Array.isArray(value.regions) || value.regions.length === 0) return null; if (!Array.isArray(value.regions) || value.regions.length === 0) return null;
const rawScalePercent = Number(value.scalePercent);
const scalePercent =
Number.isFinite(rawScalePercent) && Math.abs(rawScalePercent) >= 1
? rawScalePercent
: undefined;
const regions = value.regions const regions = value.regions
.map((region) => normalizeCanvasRegion(region)) .map((region) => normalizeCanvasRegion(region))
.filter((region): region is CanvasLocalEditCompositeRegion => Boolean(region)); .filter((region): region is CanvasLocalEditCompositeRegion => Boolean(region));
const targetRegions = Array.isArray((value as { targetRegions?: unknown[] }).targetRegions)
? (value as { targetRegions?: unknown[] }).targetRegions
?.map((region) => normalizeCanvasRegion(region))
.filter((region): region is CanvasLocalEditCompositeRegion =>
Boolean(region)
) || []
: [];
const protectedRegions = Array.isArray(value.protectedRegions) const protectedRegions = Array.isArray(value.protectedRegions)
? value.protectedRegions ? value.protectedRegions
.map((region) => normalizeCanvasRegion(region)) .map((region) => normalizeCanvasRegion(region))
@ -346,11 +357,13 @@ function normalizeScaleAdjustVerificationConfig(
) )
: []; : [];
return regions.length > 0 return regions.length > 0 &&
(targetRegions.length > 0 || typeof scalePercent === "number")
? { ? {
sourceImageUrl, sourceImageUrl,
regions, regions,
scalePercent, ...(targetRegions.length > 0 ? { targetRegions } : {}),
...(typeof scalePercent === "number" ? { scalePercent } : {}),
...(protectedRegions.length > 0 ? { protectedRegions } : {}), ...(protectedRegions.length > 0 ? { protectedRegions } : {}),
} }
: null; : null;
@ -425,13 +438,32 @@ function getScaleAdjustProtectedRegions(
expandCanvasRegion(region, SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN) expandCanvasRegion(region, SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN)
); );
} }
return verification.regions.flatMap((region) => [ if (verification.targetRegions?.length) {
expandCanvasRegion(region, SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN), return verification.regions.flatMap((region, index) => {
expandCanvasRegion( const targetRegion = verification.targetRegions?.[index];
scaleRegionAroundCenter(region, verification.scalePercent), return targetRegion
SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN ? [
), expandCanvasRegion(region, SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN),
]); expandCanvasRegion(
targetRegion,
SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN
),
]
: [expandCanvasRegion(region, SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN)];
});
}
if (typeof verification.scalePercent === "number") {
return verification.regions.flatMap((region) => [
expandCanvasRegion(region, SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN),
expandCanvasRegion(
scaleRegionAroundCenter(region, verification.scalePercent as number),
SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN
),
]);
}
return verification.regions.map((region) =>
expandCanvasRegion(region, SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN)
);
} }
function getScaleAdjustOutsideSampleRegions( function getScaleAdjustOutsideSampleRegions(
@ -756,7 +788,8 @@ async function applyScaleAdjustVerificationToImages(params: {
debugLabel, debugLabel,
imageCount: images.length, imageCount: images.length,
regionCount: verification.regions.length, regionCount: verification.regions.length,
scalePercent: verification.scalePercent, scalePercent: verification.scalePercent ?? null,
targetRegionCount: verification.targetRegions?.length || 0,
}); });
return images; return images;
} }
@ -810,7 +843,8 @@ async function applyScaleAdjustVerificationToImages(params: {
protectedRegionCount: verification.protectedRegions?.length || 0, protectedRegionCount: verification.protectedRegions?.length || 0,
regionCount: verification.regions.length, regionCount: verification.regions.length,
regionDiffScores: diffScores.map((score) => Number(score.toFixed(2))), regionDiffScores: diffScores.map((score) => Number(score.toFixed(2))),
scalePercent: verification.scalePercent, scalePercent: verification.scalePercent ?? null,
targetRegionCount: verification.targetRegions?.length || 0,
}); });
if (outsideChanged) { if (outsideChanged) {